Local review candidate only. Explicitly selected inherited prerequisites and prior repair patches; unrelated WIP and independent MQ fixes are excluded. No push or deployment authorized.
313 lines
17 KiB
Java
313 lines
17 KiB
Java
package com.yunhai.svchall.service.impl;
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||
import com.yunhai.platform.openapi.flow.YschFlowOpenApi;
|
||
import com.yunhai.platform.openapi.flow.impl.DefaultOpenApiFlowClient;
|
||
import com.yunhai.platform.openapi.flow.model.FlowApplyBody;
|
||
import com.yunhai.platform.openapi.flow.model.FlowCallbackRegBody;
|
||
import com.yunhai.svchall.model.svc.SvcModels;
|
||
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.Map;
|
||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||
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.ArgumentMatchers.isNull;
|
||
import static org.mockito.Mockito.mock;
|
||
import static org.mockito.Mockito.verify;
|
||
import static org.mockito.Mockito.when;
|
||
|
||
/**
|
||
* svc-hall → flow-center 出站契约测试:服务目录显式配置 {@code flowcenter} 时,
|
||
* <b>全部</b>出站(listFlows / getFlow / apply / instance / callback·reg)必须走 SDK 命名目标
|
||
* (门面 {@link YschFlowOpenApi} 或预绑 {@code BoundOpenCall}),不再回落应用专属
|
||
* {@code svchall.flow.base-url}(《子应用配置优化方案》§4.2 零专属地址配置)。<br><br>
|
||
* 此前只有 listFlows/getFlow 走门面,apply/instance/registerCallback 仍用 ad-hoc
|
||
* {@code TargetSpec.of(baseUrl,"")}——地址仍来自应用配置,且 {@code hasTarget} 探测与真实调用
|
||
* 分支不一致(探测说"目录已配",调用却打 legacy 地址)。
|
||
*/
|
||
@DisplayName("svc-hall FlowCenterClient - 服务目录命名目标(全端点)")
|
||
class FlowCenterClientFacadeTest {
|
||
|
||
private static final String TARGET = "flow-center";
|
||
private static final String LEGACY_BASE = "http://flow-center-service:10008/flow-center";
|
||
|
||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||
|
||
private YschOpenCall openCall;
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
openCall = mock(YschOpenCall.class);
|
||
when(openCall.hasTarget(TARGET)).thenReturn(true);
|
||
// 门面静态入口:setCall 由被测类构造器完成,setClient 由 starter 完成——单测手工对齐装配
|
||
YschFlowOpenApi.setCall(openCall);
|
||
YschFlowOpenApi.setClient(new DefaultOpenApiFlowClient(openCall));
|
||
}
|
||
|
||
private FlowCenterClient client() {
|
||
return new FlowCenterClient(openCall, objectMapper, true, LEGACY_BASE);
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("apply:走门面 → 命名目标 + SDK FlowApplyBody 子类 + 出参 instanceId/duplicated")
|
||
void apply_goesThroughFacade() {
|
||
SvcModels.ApplyResultVo stub = new SvcModels.ApplyResultVo();
|
||
stub.setInstanceId(5L);
|
||
stub.setDuplicated(true);
|
||
when(openCall.post(eq(TARGET), eq("/oapi/flow/apply"), anyMap(),
|
||
any(FlowApplyBody.class), eq(SvcModels.ApplyResultVo.class))).thenReturn(stub);
|
||
|
||
FlowClient.ApplyResult result = client().apply(3L, "guid-1", "svc_item", "1", "刘文娟的教师入职",
|
||
"http://base.yunhai/svc-hall/front/#/apply-detail?id=1", "{\"a\":1}", 10133, "刘文娟", null);
|
||
|
||
assertEquals(Long.valueOf(5L), result.getInstanceId());
|
||
assertTrue(result.isDuplicated());
|
||
|
||
ArgumentCaptor<FlowApplyBody> body = ArgumentCaptor.forClass(FlowApplyBody.class);
|
||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||
verify(openCall).post(eq(TARGET), eq("/oapi/flow/apply"), query.capture(),
|
||
body.capture(), eq(SvcModels.ApplyResultVo.class));
|
||
assertEquals("3", query.getValue().get("schId"));
|
||
assertEquals("guid-1", body.getValue().getFlowGuid());
|
||
assertEquals("svc_item", body.getValue().getBizType());
|
||
assertEquals("1", body.getValue().getBizId());
|
||
assertEquals(Integer.valueOf(10133), body.getValue().getSubmitterId());
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("apply:formDataJson 解析为 formData 对象;空串不传")
|
||
void apply_parsesFormData() {
|
||
SvcModels.ApplyResultVo stub = new SvcModels.ApplyResultVo();
|
||
stub.setInstanceId(6L);
|
||
when(openCall.post(eq(TARGET), eq("/oapi/flow/apply"), anyMap(),
|
||
any(FlowApplyBody.class), eq(SvcModels.ApplyResultVo.class))).thenReturn(stub);
|
||
|
||
client().apply(3L, "g", "svc_item", "2", "t", "u", null, 1, "n", "{\"x\":\"y\"}");
|
||
|
||
ArgumentCaptor<FlowApplyBody> body = ArgumentCaptor.forClass(FlowApplyBody.class);
|
||
verify(openCall).post(eq(TARGET), eq("/oapi/flow/apply"), anyMap(),
|
||
body.capture(), eq(SvcModels.ApplyResultVo.class));
|
||
assertEquals("y", body.getValue().getFormData().get("x"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("getInstanceByBiz:走预绑命名目标(Map 全字段透传,不用 ad-hoc spec)")
|
||
void instance_usesBoundTarget() {
|
||
when(openCall.get(eq(TARGET), eq("/oapi/flow/instance"), anyMap(), eq(Map.class)))
|
||
.thenReturn(java.util.Collections.singletonMap("status", 20));
|
||
|
||
Map<String, Object> out = client().getInstanceByBiz(3L, "svc_item", "2");
|
||
|
||
assertEquals(Integer.valueOf(20), out.get("status"));
|
||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||
verify(openCall).get(eq(TARGET), eq("/oapi/flow/instance"), query.capture(), eq(Map.class));
|
||
assertEquals("svc_item", query.getValue().get("bizType"));
|
||
assertEquals("2", query.getValue().get("bizId"));
|
||
assertEquals("3", query.getValue().get("schId"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("registerCallback:走门面 + basePath 从回调 URL 反推(/svc-hall 剥离口径)")
|
||
void registerCallback_goesThroughFacade() {
|
||
SvcModels.CallbackRegVo stub = new SvcModels.CallbackRegVo();
|
||
stub.setSecret("secret-1");
|
||
when(openCall.post(eq(TARGET), eq("/oapi/flow/callback/reg"), anyMap(),
|
||
any(FlowCallbackRegBody.class), eq(SvcModels.CallbackRegVo.class))).thenReturn(stub);
|
||
|
||
String secret = client().registerCallback(3L, "guid-1",
|
||
"http://open-gateway-service:10013/svc-hall/oapi/flow/callback", null);
|
||
|
||
assertEquals("secret-1", secret);
|
||
ArgumentCaptor<FlowCallbackRegBody> body = ArgumentCaptor.forClass(FlowCallbackRegBody.class);
|
||
verify(openCall).post(eq(TARGET), eq("/oapi/flow/callback/reg"), anyMap(),
|
||
body.capture(), eq(SvcModels.CallbackRegVo.class));
|
||
assertEquals("/svc-hall", body.getValue().getBasePath());
|
||
assertEquals("http://open-gateway-service:10013/svc-hall/oapi/flow/callback", body.getValue().getCallbackUrl());
|
||
assertNull(body.getValue().getEventTypes());
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("listFlows/getFlow:门面命名目标(回归既有行为)")
|
||
void listAndDetail_useFacade() {
|
||
when(openCall.getList(eq(TARGET), eq("/oapi/flow/list"), anyMap(), eq(SvcModels.FlowSummaryVo.class)))
|
||
.thenReturn(java.util.Collections.singletonList(new SvcModels.FlowSummaryVo()));
|
||
when(openCall.get(eq(TARGET), eq("/oapi/flow/guid-1"), anyMap(), eq(SvcModels.FlowSummaryVo.class)))
|
||
.thenReturn(new SvcModels.FlowSummaryVo());
|
||
|
||
assertEquals(1, client().listFlows(3L).size());
|
||
org.junit.jupiter.api.Assertions.assertNotNull(client().getFlow(3L, "guid-1"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("撤回:走门面 → /oapi/flow/instance/{id}/withdraw + schId/submitterId(对端复核身份)")
|
||
void withdraw_goesThroughFacade() {
|
||
client().withdraw(3L, 77L, 201);
|
||
|
||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||
verify(openCall).post(eq(TARGET), eq("/oapi/flow/instance/77/withdraw"), query.capture(),
|
||
isNull(), eq(Void.class));
|
||
assertEquals("3", query.getValue().get("schId"));
|
||
assertEquals("201", query.getValue().get("submitterId"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("进度:走门面 → /oapi/flow/progress?instanceId=&schId=")
|
||
void progress_goesThroughFacade() {
|
||
SvcModels.FlowProgressVo stub = new SvcModels.FlowProgressVo();
|
||
stub.setInstanceId(77L);
|
||
when(openCall.get(eq(TARGET), eq("/oapi/flow/progress"), anyMap(),
|
||
eq(SvcModels.FlowProgressVo.class))).thenReturn(stub);
|
||
|
||
assertEquals(Long.valueOf(77L), client().progress(3L, 77L, null, null).getInstanceId());
|
||
|
||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||
verify(openCall).get(eq(TARGET), eq("/oapi/flow/progress"), query.capture(),
|
||
eq(SvcModels.FlowProgressVo.class));
|
||
assertEquals("77", query.getValue().get("instanceId"));
|
||
assertEquals("3", query.getValue().get("schId"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("流程图:换图 + 取图两步都在后端;token 不出后端(前端拿不到,也不需要 flow-center 地址)")
|
||
void diagramImage_tokenStaysInBackend() {
|
||
SvcModels.FlowDiagramVo url = new SvcModels.FlowDiagramVo();
|
||
url.setImgUrl("/open/flow/diagram/img?token=tok-abc");
|
||
url.setExpiresIn(300);
|
||
when(openCall.get(eq(TARGET), eq("/oapi/flow/diagram/url"), anyMap(),
|
||
eq(SvcModels.FlowDiagramVo.class))).thenReturn(url);
|
||
byte[] png = new byte[]{(byte) 0x89, 'P', 'N', 'G'};
|
||
when(openCall.download(eq(TARGET), eq("/open/flow/diagram/img"),
|
||
eq(java.util.Collections.singletonMap("token", "tok-abc")))).thenReturn(png);
|
||
|
||
assertEquals(png, client().diagramImage(3L, 77L, null, "dept"));
|
||
|
||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||
verify(openCall).get(eq(TARGET), eq("/oapi/flow/diagram/url"), query.capture(),
|
||
eq(SvcModels.FlowDiagramVo.class));
|
||
assertEquals("77", query.getValue().get("instanceId"));
|
||
assertEquals("dept", query.getValue().get("mode"));
|
||
verify(openCall).download(eq(TARGET), eq("/open/flow/diagram/img"),
|
||
eq(java.util.Collections.singletonMap("token", "tok-abc")));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("tokenOf:只认 ?token= 段,容忍尾部其它参数;无 token 返回 null(不拼出脏地址)")
|
||
void tokenOf_parsesTokenOnly() {
|
||
assertEquals("abc", FlowCenterClient.tokenOf("/open/flow/diagram/img?token=abc"));
|
||
assertEquals("abc", FlowCenterClient.tokenOf("/open/flow/diagram/img?token=abc&x=1"));
|
||
assertEquals("abc", FlowCenterClient.tokenOf("http://h/flow-center/open/flow/diagram/img?token=abc"));
|
||
org.junit.jupiter.api.Assertions.assertNull(FlowCenterClient.tokenOf("/open/flow/diagram/img"));
|
||
org.junit.jupiter.api.Assertions.assertNull(FlowCenterClient.tokenOf(null));
|
||
org.junit.jupiter.api.Assertions.assertNull(FlowCenterClient.tokenOf("/x?token="));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("目录未配:撤回落 legacy base-url(ad-hoc TargetSpec POST,行为不变)")
|
||
void withdraw_fallsBackToLegacy() {
|
||
YschOpenCall legacyCall = mock(YschOpenCall.class);
|
||
when(legacyCall.hasTarget(TARGET)).thenReturn(false);
|
||
|
||
new FlowCenterClient(legacyCall, objectMapper, true, LEGACY_BASE).withdraw(3L, 77L, 201);
|
||
|
||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||
verify(legacyCall).post(any(com.yunhai.platform.openapi.core.TargetSpec.class),
|
||
eq("/oapi/flow/instance/77/withdraw"), query.capture(), isNull(), eq(Void.class));
|
||
assertEquals("201", query.getValue().get("submitterId"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("enabled=false:三个新方法一律抛「未接入」(不静默成功让前端误判)")
|
||
void disabledClient_rejectsNewActions() {
|
||
YschOpenCall noDirectory = mock(YschOpenCall.class);
|
||
when(noDirectory.hasTarget(TARGET)).thenReturn(false);
|
||
FlowCenterClient c = new FlowCenterClient(noDirectory, objectMapper, false, "");
|
||
|
||
com.yunhai.platform.apistd.BizException withdrawEx =
|
||
org.junit.jupiter.api.Assertions.assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||
() -> c.withdraw(3L, 77L, 201));
|
||
org.junit.jupiter.api.Assertions.assertTrue(withdrawEx.getMessage().contains("流程中心未接入"));
|
||
org.junit.jupiter.api.Assertions.assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||
() -> c.progress(3L, 77L, null, null));
|
||
org.junit.jupiter.api.Assertions.assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||
() -> c.diagramImage(3L, 77L, null, null));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("目录未配:回落 legacy svchall.flow.base-url(迁移期兼容,行为不变)")
|
||
void fallsBackToLegacyWhenDirectoryMissing() {
|
||
YschOpenCall legacyCall = mock(YschOpenCall.class);
|
||
when(legacyCall.hasTarget(TARGET)).thenReturn(false);
|
||
when(legacyCall.post(any(com.yunhai.platform.openapi.core.TargetSpec.class),
|
||
eq("/oapi/flow/apply"), anyMap(), any(), eq(com.fasterxml.jackson.databind.JsonNode.class)))
|
||
.thenReturn(objectMapper.createObjectNode().put("instanceId", 9));
|
||
|
||
FlowClient.ApplyResult result = new FlowCenterClient(legacyCall, objectMapper, true, LEGACY_BASE)
|
||
.apply(3L, "g", "svc_item", "1", "t", "u", null, 1, "n", null);
|
||
|
||
assertEquals(Long.valueOf(9L), result.getInstanceId());
|
||
assertFalse(result.isDuplicated());
|
||
verify(legacyCall).post(any(com.yunhai.platform.openapi.core.TargetSpec.class),
|
||
eq("/oapi/flow/apply"), anyMap(), any(), eq(com.fasterxml.jackson.databind.JsonNode.class));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("目录与 legacy 键皆未配且 enabled=true:构造期 fail-fast(不静默打 localhost)")
|
||
void misconfiguredFailsFast() {
|
||
YschOpenCall noDirectory = mock(YschOpenCall.class);
|
||
when(noDirectory.hasTarget(TARGET)).thenReturn(false);
|
||
|
||
IllegalStateException e = assertThrows(IllegalStateException.class,
|
||
() -> new FlowCenterClient(noDirectory, objectMapper, true, ""));
|
||
assertTrue(e.getMessage().contains("yh.open.services." + TARGET), e.getMessage());
|
||
assertTrue(e.getMessage().contains("svchall.flow.base-url"), e.getMessage());
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("enabled=false 且地址全缺:不 fail-fast(显式未接入)")
|
||
void disabledWithoutAddressIsAllowed() {
|
||
YschOpenCall noDirectory = mock(YschOpenCall.class);
|
||
when(noDirectory.hasTarget(TARGET)).thenReturn(false);
|
||
|
||
FlowCenterClient c = new FlowCenterClient(noDirectory, objectMapper, false, "");
|
||
assertFalse(c.enabled());
|
||
com.yunhai.platform.apistd.BizException ex =
|
||
org.junit.jupiter.api.Assertions.assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||
() -> c.listFlows(3L));
|
||
assertTrue(ex.getMessage().contains("流程中心未接入"), ex.getMessage());
|
||
}
|
||
@Test
|
||
void v2ResultRetainsBranchRoundAndRealTerminalStatus() {
|
||
com.yunhai.svchall.model.FcApplyModels.FlowOutbound outbound = new com.yunhai.svchall.model.FcApplyModels.FlowOutbound();
|
||
outbound.setBizId("42"); outbound.setRequestToken("token-1");
|
||
when(openCall.post(eq(TARGET), eq("/oapi/flow/apply/v2"), anyMap(), any(), eq(com.fasterxml.jackson.databind.JsonNode.class)))
|
||
.thenReturn(objectMapper.createObjectNode().put("instanceId", 555).put("duplicated", true)
|
||
.put("branch", "SAME_REQUEST").put("applyRound", 1).put("status", 30).put("version", 4));
|
||
FlowClient.ApplyResult result = client().applyWithIdentity(3L, outbound);
|
||
assertEquals("SAME_REQUEST", result.getBranch()); assertEquals(Integer.valueOf(1), result.getApplyRound());
|
||
assertEquals(Integer.valueOf(30), result.getStatus()); assertEquals(Integer.valueOf(4), result.getVersion());
|
||
assertEquals(Long.valueOf(555), result.getInstanceId());
|
||
}
|
||
|
||
@Test
|
||
void v2DoesNotTruncateFractionalRound() {
|
||
com.yunhai.svchall.model.FcApplyModels.FlowOutbound outbound = new com.yunhai.svchall.model.FcApplyModels.FlowOutbound();
|
||
outbound.setBizId("42"); outbound.setRequestToken("token-1");
|
||
when(openCall.post(eq(TARGET), eq("/oapi/flow/apply/v2"), anyMap(), any(), eq(com.fasterxml.jackson.databind.JsonNode.class)))
|
||
.thenReturn(objectMapper.createObjectNode().put("instanceId", 555).put("branch", "RESTARTED").put("applyRound", 2.5));
|
||
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> client().applyWithIdentity(3L, outbound));
|
||
}
|
||
|
||
}
|