Files
flow-center-service/src/test/java/com/yunhai/flowcenter/service/OapiFlowApplyDefVersionTest.java
T
chenxing d72128672f
build / build-push (push) Failing after 55s
feat(fl2): 落地部门目录 / 退回重提(D14) / 授权管理后端(基线 origin/main)
在 b369527 之上增量移植 FL2.0 后端能力,不改既有线级停发/指派预览/流程图入口:

- 部门目录:FlowDepartmentController(/admin/department/{groups,options,resolve,members})
  + UserBaseDepartmentDirectory(先按会话校域解析引用,再读成员;目录不可用 fail-closed)
- 退回重提 D14:POST /oapi/flow/instance/{instanceId}/resubmit,仅原提交人 CAS 路径放行,
  OAPI 侧 submitterId 路径继续 fail-closed
- 授权管理:/api/flow/admin/usage-grant/{schoolList,summary}(校域取管理员会话,无 schId 入参)
- 来源主体:解析器派生 ub-app:{applicationInfo.id},不再读取 UserBase 的 sourcePrincipalId 字段
- 回调来源策略:SELF 身份强校验;NULL 来源不广播;复合幂等键 (event_id,reg_id)
- 测试夹具:许可/主体/开通桩按新契约修正,FlowEngineIT/FormalApplyCommandIT/
  CallbackDispatchIT/OapiFlowContractIT 共 117 例在真实 MySQL 8 通过

不含:FlSourceAuthorizationTest(其断言的两个 Repository 派生方法属另一实现线)
2026-10-30 10:15:12 +08:00

181 lines
8.5 KiB
Java

package com.yunhai.flowcenter.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.AssigneeResolver;
import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.model.OapiModels;
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
import com.yunhai.flowcenter.repository.FlwCcRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwTaskRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.base.OpenApiAppClient;
import com.yunhai.platform.openapi.base.OpenApiUserClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import java.util.Optional;
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.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* S2.7 版本竞态(⭐):apply 可选回带预览时的定义版本,改版即人类可读拒绝。
* 三态必测——不传(存量调用方,行为逐字不变)、传对(放行)、传旧(拒绝且不落任何引擎动作)。
* 另钉住 preview 回带 defVersion(调用方拿得到的前提)。
*/
@DisplayName("apply 期望定义版本(版本竞态人类可读报错)")
class OapiFlowApplyDefVersionTest {
private static final Long SCH_ID = 1L;
private static final String FLOW_GUID = "g-assign";
private FlwDefRepository defRepository;
private FlowInstanceService instanceService;
private AssigneeResolver assigneeResolver;
private OapiFlowService service;
private FlwInstanceRepository instances;
@BeforeEach
@SuppressWarnings("unchecked")
void setUp() {
defRepository = mock(FlwDefRepository.class);
instanceService = mock(FlowInstanceService.class);
assigneeResolver = mock(AssigneeResolver.class);
instances = mock(FlwInstanceRepository.class);
service = new OapiFlowService(defRepository, instances,
mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
mock(FlwCallbackRegRepository.class), instanceService, mock(FlowTaskService.class),
mock(FlowEngine.class), assigneeResolver, new ObjectMapper(),
mock(OpenApiUserClient.class), mock(OpenApiAppClient.class),
mock(DiagramTokenService.class), mock(ObjectProvider.class));
// 本校校验在单测里关闭(@Value 字段未注入=默认 false),引擎出参给一个既有办件
when(instanceService.apply(any(FlowInstanceService.ApplyCommand.class)))
.thenReturn(new FlowInstanceService.ApplyOutcome(500L, false));
// F01/F02:来源主体解析与使用许可闸在生产装配里必然存在;单测手工构造服务时必须显式注入
// 测试替身,否则 apply 会因"闸门未装配/来源不可解析"被 fail-closed 拒(那是期望行为,不是靶子)。
FlowSourcePrincipalResolver stubPrincipals = mock(FlowSourcePrincipalResolver.class);
when(stubPrincipals.resolve(org.mockito.ArgumentMatchers.anyString()))
.thenReturn(new FlowSourcePrincipalResolver.Principal(
"ub-app:18", 18, "100018", "flow-test"));
org.springframework.test.util.ReflectionTestUtils.setField(service, "sourcePrincipalResolver", stubPrincipals);
service.setUsageGrantGuard(mock(FlowUsageGrantGuard.class));
}
private static FlwDef publishedDef(int version) {
FlwDef def = new FlwDef();
def.setId(9L);
def.setSchId(SCH_ID);
def.setFlowGuid(FLOW_GUID);
def.setVersion(version);
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
def.setContentJson("{\"nodes\":[], \"edges\":[], \"start\":\"n1\"}");
return def;
}
private static OapiModels.ApplyBody applyBody(Integer expectedDefVersion) {
OapiModels.ApplyBody body = new OapiModels.ApplyBody();
body.setFlowGuid(FLOW_GUID);
body.setBizType("notice");
body.setBizId("77");
body.setBizTitle("标题");
body.setSubmitterId(21);
body.setSubmitterName("甲");
body.setExpectedDefVersion(expectedDefVersion);
return body;
}
private void stubDef(int version) {
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
eq(SCH_ID), eq(FLOW_GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
.thenReturn(Optional.of(publishedDef(version)));
}
@Test
void withoutExpectedVersion_behavesExactlyAsBefore() {
stubDef(3);
OapiModels.ApplyResultVo vo = service.apply(SCH_ID, "100009", applyBody(null));
assertEquals(Long.valueOf(500L), vo.getInstanceId());
verify(instanceService).apply(any(FlowInstanceService.ApplyCommand.class));
}
@Test
void matchingVersion_passesThrough() {
stubDef(3);
OapiModels.ApplyResultVo vo = service.apply(SCH_ID, "100009", applyBody(3));
assertEquals(Long.valueOf(500L), vo.getInstanceId());
verify(instanceService).apply(any(FlowInstanceService.ApplyCommand.class));
}
/** 改版后按旧预览结果提交:人类可读文案,且引擎与指派校验都不被触发(不污染幂等键) */
@Test
void staleExpectedVersion_rejectedWithReadableMessage_beforeAnyEngineAction() {
stubDef(4);
BizException e = assertThrows(BizException.class,
() -> service.apply(SCH_ID, "100009", applyBody(3)));
assertTrue(e.getMessage().contains("流程已更新,请刷新后重新选择审批人"), e.getMessage());
assertTrue(e.getMessage().contains("期望定义版本 3") && e.getMessage().contains("当前已发布版本 4"),
e.getMessage());
verify(instanceService, never()).apply(any(FlowInstanceService.ApplyCommand.class));
}
@Test
void previewCarriesDefVersionSoCallerCanEchoIt() {
stubDef(7);
when(assigneeResolver.resolve(anyLong(), any())).thenReturn(java.util.Collections.emptyList());
OapiModels.ApplyPreviewVo vo = service.applyPreview(SCH_ID, FLOW_GUID, 21);
assertEquals(Integer.valueOf(7), vo.getDefVersion());
assertEquals(FLOW_GUID, vo.getFlowGuid());
assertTrue(vo.getNodes() == null || vo.getNodes().isEmpty());
}
/** 存量兼容:无指派节点的流程 preview 仍返回空节点集(defVersion 不影响既有消费方) */
@Test
void previewKeepsEmptyNodesForFlowWithoutAssignPolicy() {
stubDef(1);
OapiModels.ApplyPreviewVo vo = service.applyPreview(SCH_ID, FLOW_GUID, 21);
assertEquals(Integer.valueOf(1), vo.getDefVersion());
assertTrue(vo.getNodes() == null || vo.getNodes().isEmpty());
}
@Test
void v2ResponseCarriesRealRejectedStatusAndStableBranch() {
stubDef(3);
com.yunhai.flowcenter.model.FlowApplyRetryModels.ApplyRequestV2 body = new com.yunhai.flowcenter.model.FlowApplyRetryModels.ApplyRequestV2();
body.setFlowGuid(FLOW_GUID); body.setBizType("svc-hall"); body.setBizId("42"); body.setSubmitterId(21);
body.setRequestToken("token-1"); body.setAttempt(1);
when(instanceService.applyGuarded(any(), any())).thenReturn(
new FlowInstanceService.GuardedOutcome(500L, true, "SAME_REQUEST", 1));
com.yunhai.flowcenter.entity.FlwInstance row = new com.yunhai.flowcenter.entity.FlwInstance();
row.setId(500L); row.setStatus(30); row.setVersion(4); row.setApplyRound(1);
when(instances.findById(500L)).thenReturn(Optional.of(row));
com.yunhai.flowcenter.model.FlowApplyRetryModels.ApplyResultV2 result = service.applyV2(SCH_ID, "100009", body);
assertEquals("SAME_REQUEST", result.getBranch()); assertEquals(Integer.valueOf(1), result.getApplyRound());
assertEquals(Integer.valueOf(30), result.getStatus()); assertEquals(Integer.valueOf(4), result.getVersion());
}
}