feat(p3.1): 应用内审批后端代理——SvcApprovalClient + /api/apply/approval/ 7 端点
build / build-push (push) Successful in 54s
build / build-push (push) Successful in 54s
- SvcBizTypes:bizType 终值/存量单一取值点(svc-hall=H-1 实核 appKey=100008 注册行 app_code; svc_item=存量,双值双查先新后旧——方案B,P3.2 落地前的读面兼容口径) - SvcApprovalClient(沿 NoticeApprovalClient 形态,YschOpenCall 服务目录命名目标): todo/byBiz 双值双查合并、approve/reject 写面、embed 记录页 instanceId 形态、taskPageUrl - SvcApprovalController:Todo/ByBiz/RecordUrl/TaskPageUrl/Approve/Reject/My · 定位链=requireOperator→findByIdAndSchId(会话校)→ApplyViewGuard→byBiz 归属预检(P1-01) · 身份三字段只取 CAS 会话,请求参数出现身份字段一律拒(P1-10 择一取严) · 写面参数形态与中心同构(taskId/applyId 查询参数,opinion/edgeName JSON body) · My 端点复用本地 myApplies + FlowPage 信封显式映射(total/list→total/content+页元数据) · 不代理 /oapi/flow/instances(全校视角);本地 flow_status 仍只由回调推进 - 测试 30 例全绿(client 10 + MockMvc standalone 20,显式装 BoaUserArgumentResolver/ GlobalExceptionHandler/ApiResponseAdvice;真实 CasSsoApiRedirectFilter 过滤链未登录=200+code302; 拒绝路径断言审批出站=0);全量回归 157/157 绿(Stage S2/P3.1)
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
package com.yunhai.svchall.service.impl;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.svchall.service.FlowClient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* P3.1 应用内审批出站契约测试:端点 URL、参数拼装与<b>双值双查</b>(P3.2 方案B)行为。
|
||||
* 读面 bizType 必须按 {@code [svc-hall, svc_item]} 先终值后存量各查一次(todo 合并、byBiz 择命中);
|
||||
* 写面/换址形态与中心端点同构(taskId 在路径、opinion/edgeName 在 body、embed 记录页 instanceId 形态)。
|
||||
*/
|
||||
@DisplayName("SvcApprovalClient - oapi 契约与双值双查")
|
||||
class SvcApprovalClientTest {
|
||||
|
||||
private static final String TARGET = "flow-center";
|
||||
|
||||
private YschOpenCall openCall;
|
||||
private FlowClient flowClient;
|
||||
private SvcApprovalClient client;
|
||||
|
||||
/** 固定返回的 ObjectProvider 桩(构造注入用,沿 Notice FlowCenterClientTest 形态) */
|
||||
static class FixedProvider<T> implements org.springframework.beans.factory.ObjectProvider<T> {
|
||||
private final T value;
|
||||
|
||||
FixedProvider(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getObject() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getObject(Object... args) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getIfAvailable() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getIfUnique() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
openCall = mock(YschOpenCall.class);
|
||||
flowClient = mock(FlowClient.class);
|
||||
when(flowClient.enabled()).thenReturn(true);
|
||||
client = new SvcApprovalClient(new FixedProvider<>(openCall), flowClient);
|
||||
}
|
||||
|
||||
|
||||
private static Map<String, Object> taskIdRow(int id) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("taskId", id);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Map<String, Object> page(long total, Object... rows) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("total", total);
|
||||
m.put("content", Arrays.asList(rows));
|
||||
m.put("pageIndex", 1);
|
||||
m.put("pageSize", 10);
|
||||
m.put("totalPages", 1);
|
||||
m.put("hasNext", false);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Map<String, Object> byBizData(Long instanceId, Object... tasks) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("instanceId", instanceId);
|
||||
m.put("bizType", "svc-hall");
|
||||
m.put("bizId", "42");
|
||||
m.put("status", 10);
|
||||
m.put("currentNodes", "n1");
|
||||
m.put("tasks", Arrays.asList(tasks));
|
||||
return m;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("todo:双值双查(先 svc-hall 后 svc_item)+ 分页合并")
|
||||
void todoQueriesBothBizTypesInOrderAndMerges() {
|
||||
Map<String, Object> cur = page(2, taskIdRow(1), taskIdRow(2));
|
||||
Map<String, Object> legacy = page(1, taskIdRow(3));
|
||||
when(openCall.get(eq(TARGET), eq("/oapi/flow/tasks/todo"), anyMap(), eq(Map.class)))
|
||||
.thenReturn(cur, legacy);
|
||||
|
||||
Map<String, Object> out = client.todoTasks(3, 10006, 1, 10);
|
||||
|
||||
ArgumentCaptor<Map<String, String>> q1 = ArgumentCaptor.forClass(Map.class);
|
||||
ArgumentCaptor<Map<String, String>> q2 = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall, times(2)).get(eq(TARGET), eq("/oapi/flow/tasks/todo"),
|
||||
q1.capture(), eq(Map.class));
|
||||
verify(openCall, times(2)).get(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), anyMap(), any());
|
||||
assertEquals("svc-hall", q1.getAllValues().get(0).get("bizType"));
|
||||
assertEquals("svc_item", q1.getAllValues().get(1).get("bizType"));
|
||||
assertEquals("3", q1.getAllValues().get(0).get("schId"));
|
||||
assertEquals("10006", q1.getAllValues().get(0).get("assigneeId"));
|
||||
assertEquals(3L, out.get("total"));
|
||||
assertEquals(3, ((List<?>) out.get("content")).size());
|
||||
assertEquals(1L, out.get("totalPages"));
|
||||
assertEquals(false, out.get("hasNext"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byBiz:终值命中即返回(不查存量则不消耗第二次出站——命中即短路)")
|
||||
void byBizReturnsCurrentHit() {
|
||||
Map<String, Object> cur = byBizData(139L, taskIdRow(442));
|
||||
when(openCall.get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"), anyMap(), eq(Map.class)))
|
||||
.thenReturn(cur);
|
||||
Map<String, Object> out = client.byBiz(3, 10006, "42");
|
||||
assertEquals(139L, ((Number) out.get("instanceId")).longValue());
|
||||
verify(openCall, times(1)).get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byBiz:终值 miss(instanceId=null)时续查存量并返回命中行")
|
||||
void byBizFallsBackToLegacy() {
|
||||
Map<String, Object> curMiss = byBizData(null);
|
||||
Map<String, Object> legacyHit = byBizData(9011L);
|
||||
legacyHit.put("bizType", "svc_item");
|
||||
when(openCall.get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"), anyMap(), eq(Map.class)))
|
||||
.thenReturn(curMiss, legacyHit);
|
||||
Map<String, Object> out = client.byBiz(3, 10006, "42");
|
||||
assertEquals(9011L, ((Number) out.get("instanceId")).longValue());
|
||||
ArgumentCaptor<Map<String, String>> q = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall, times(2)).get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"), q.capture(), eq(Map.class));
|
||||
assertEquals("svc-hall", q.getAllValues().get(0).get("bizType"));
|
||||
assertEquals("svc_item", q.getAllValues().get(1).get("bizType"));
|
||||
assertEquals("42", q.getAllValues().get(0).get("bizId"));
|
||||
assertEquals("10006", q.getAllValues().get(0).get("operatorId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byBiz:双查皆空返回首查应答(保持空 tasks 不探测语义)")
|
||||
void byBizBothMissReturnsFirst() {
|
||||
Map<String, Object> curMiss = byBizData(null);
|
||||
Map<String, Object> legacyMiss = byBizData(null);
|
||||
when(openCall.get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"), anyMap(), eq(Map.class)))
|
||||
.thenReturn(curMiss, legacyMiss);
|
||||
Map<String, Object> out = client.byBiz(3, 10006, "42");
|
||||
assertTrue(((List<?>) out.get("tasks")).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("approve:taskId 入路径、身份入查询串、opinion/edgeName 入 body 且 trim")
|
||||
void approveAssembly() {
|
||||
client.approve(442L, 3, 10006, "张三", " 同意 ", " 退回 ");
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, String>> q = ArgumentCaptor.forClass(Map.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, Object>> body = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).post(eq(TARGET), eq("/oapi/flow/task/442/approve"),
|
||||
q.capture(), body.capture(), eq(Void.class));
|
||||
assertEquals("3", q.getValue().get("schId"));
|
||||
assertEquals("10006", q.getValue().get("operatorId"));
|
||||
assertEquals("张三", q.getValue().get("operatorName"));
|
||||
assertEquals("同意", body.getValue().get("opinion"));
|
||||
assertEquals("退回", body.getValue().get("edgeName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("approve:空 opinion/edgeName 不入 body")
|
||||
void approveOmitsBlankFields() {
|
||||
client.approve(442L, 3, 10006, "张三", " ", null);
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, Object>> body = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).post(eq(TARGET), eq("/oapi/flow/task/442/approve"),
|
||||
anyMap(), body.capture(), eq(Void.class));
|
||||
assertTrue(body.getValue().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("reject:opinion 入 body;路径 /task/{id}/reject")
|
||||
void rejectAssembly() {
|
||||
client.reject(443L, 3, 10006, "张三", "不同意");
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, Object>> body = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).post(eq(TARGET), eq("/oapi/flow/task/443/reject"),
|
||||
anyMap(), body.capture(), eq(Void.class));
|
||||
assertEquals("不同意", body.getValue().get("opinion"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("embedRecordUrl:mode=record + instanceId 形态(非 bizType+bizId)")
|
||||
void embedRecordUrlUsesInstanceIdForm() {
|
||||
client.embedRecordUrl(3, 139L);
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, String>> q = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).get(eq(TARGET), eq("/oapi/flow/embed/url"), q.capture(), eq(Map.class));
|
||||
assertEquals("record", q.getValue().get("mode"));
|
||||
assertEquals("139", q.getValue().get("instanceId"));
|
||||
assertEquals("3", q.getValue().get("schId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("taskPageUrl:mode=task + taskId")
|
||||
void taskPageUrlAssembly() {
|
||||
client.taskPageUrl(3, 442L);
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, String>> q = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).get(eq(TARGET), eq("/oapi/flow/embed/url"), q.capture(), eq(Map.class));
|
||||
assertEquals("task", q.getValue().get("mode"));
|
||||
assertEquals("442", q.getValue().get("taskId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("流程中心未接入:一律 FORBIDDEN 且零出站")
|
||||
void disabledRefusesAll() {
|
||||
when(flowClient.enabled()).thenReturn(false);
|
||||
assertThrows(BizException.class, () -> client.todoTasks(3, 10006, 1, 10));
|
||||
assertThrows(BizException.class, () -> client.byBiz(3, 10006, "42"));
|
||||
assertThrows(BizException.class, () -> client.approve(1L, 3, 1, "n", null, null));
|
||||
assertThrows(BizException.class, () -> client.embedRecordUrl(3, 1L));
|
||||
verify(openCall, never()).get(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), anyMap(), any());
|
||||
verify(openCall, never()).post(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), anyMap(), any(), any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user