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; @BeforeEach @SuppressWarnings("unchecked") void setUp() { defRepository = mock(FlwDefRepository.class); instanceService = mock(FlowInstanceService.class); assigneeResolver = mock(AssigneeResolver.class); service = new OapiFlowService(defRepository, mock(FlwInstanceRepository.class), 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)); } 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()); } }