build / build-push (push) Successful in 2m0s
requireLine() 存在性判断取反,导致已存在的流程线被拒绝、不存在的线反被放行, 线级停发/恢复线上 100% 失败。改为查询为空时拒绝。 - FlowDefAdminService.java:302 补 ! 号(唯一生产代码改动) - FlDefLineOfflineTest 改用真实 FlowDefAdminService(删除业务副本 spy),补非法 GUID/空值/空白/错误学校/trim/在途隔离断言 - FlNewInstancePathTest 新增新实例路径定向测试
282 lines
16 KiB
Diff
282 lines
16 KiB
Diff
diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
|
index da109e3..3837c1a 100644
|
|
--- a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
|
+++ b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
|
@@ -299,7 +299,7 @@ public class FlowDefAdminService {
|
|
|
|
private void requireLine(Long schId, String flowGuid) {
|
|
if (flowGuid == null || flowGuid.trim().isEmpty()
|
|
- || defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
|
+ || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
|
.isPresent()) {
|
|
throw new BizException(ErrorCode.PARAM_ERROR, "流程线不存在:" + flowGuid);
|
|
}
|
|
diff --git a/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java b/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
|
index 76bbf34..3cff9e9 100644
|
|
--- a/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
|
+++ b/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
|
@@ -18,6 +18,9 @@ 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.junit.jupiter.params.ParameterizedTest;
|
|
+import org.junit.jupiter.params.provider.NullAndEmptySource;
|
|
+import org.junit.jupiter.params.provider.ValueSource;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.springframework.beans.factory.ObjectProvider;
|
|
import org.springframework.test.util.ReflectionTestUtils;
|
|
@@ -33,7 +36,9 @@ import static org.mockito.ArgumentMatchers.anyString;
|
|
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.verifyNoInteractions;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
/**
|
|
@@ -53,43 +58,17 @@ class FlDefLineOfflineTest {
|
|
private static final String GUID = "g-line";
|
|
|
|
private FlwDefRepository defRepository;
|
|
- private FlwDefAdminServiceSpy adminService;
|
|
+ private FlowDefAdminService adminService;
|
|
private OapiFlowService oapiService;
|
|
private FlwInstanceRepository instances;
|
|
private com.yunhai.flowcenter.service.FlowInstanceService instanceService;
|
|
|
|
- /** 仅暴露 protected/私有依赖注入的薄壳:真实 FlowDefAdminService 构造依赖过多,用 spy 隔离外部协作 */
|
|
- static class FlwDefAdminServiceSpy {
|
|
- final FlwDefRepository defRepository;
|
|
- FlwDefAdminServiceSpy(FlwDefRepository defRepository) {
|
|
- this.defRepository = defRepository;
|
|
- }
|
|
- int offlineLine(Long schId, String flowGuid) {
|
|
- if (flowGuid == null || flowGuid.trim().isEmpty()
|
|
- || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
|
- .isPresent()) {
|
|
- throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
|
- "流程线不存在:" + flowGuid);
|
|
- }
|
|
- return defRepository.updateLineOffline(schId, flowGuid.trim(), 1);
|
|
- }
|
|
- int enableLine(Long schId, String flowGuid) {
|
|
- if (flowGuid == null || flowGuid.trim().isEmpty()
|
|
- || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
|
- .isPresent()) {
|
|
- throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
|
- "流程线不存在:" + flowGuid);
|
|
- }
|
|
- return defRepository.updateLineOffline(schId, flowGuid.trim(), 0);
|
|
- }
|
|
- }
|
|
-
|
|
@BeforeEach
|
|
@SuppressWarnings("unchecked")
|
|
void setUp() {
|
|
defRepository = mock(FlwDefRepository.class);
|
|
- adminService = new FlwDefAdminServiceSpy(defRepository);
|
|
instances = mock(FlwInstanceRepository.class);
|
|
+ adminService = newFlowDefAdminService();
|
|
instanceService = mock(FlowInstanceService.class);
|
|
oapiService = new OapiFlowService(defRepository, instances,
|
|
mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
|
|
@@ -121,9 +100,61 @@ class FlDefLineOfflineTest {
|
|
assertThatThrownBy(() -> adminService.offlineLine(SCH, "g-nope"))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("流程线不存在");
|
|
+ assertThatThrownBy(() -> adminService.enableLine(SCH, "g-nope"))
|
|
+ .isInstanceOf(BizException.class)
|
|
+ .hasMessageContaining("流程线不存在");
|
|
+ verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
|
+ }
|
|
+
|
|
+ @ParameterizedTest
|
|
+ @NullAndEmptySource
|
|
+ @ValueSource(strings = {" ", "\t\r\n"})
|
|
+ void blankGuidRejectedOnBothPaths(String guid) {
|
|
+ assertThatThrownBy(() -> adminService.offlineLine(SCH, guid))
|
|
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
|
+ assertThatThrownBy(() -> adminService.enableLine(SCH, guid))
|
|
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
|
+ verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
|
+ verify(defRepository, never()).findFirstBySchIdAndFlowGuidOrderByVersionDesc(any(), any());
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ void otherSchoolCannotChangeExistingLine() {
|
|
+ assertThatThrownBy(() -> adminService.offlineLine(2L, GUID))
|
|
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
|
+ assertThatThrownBy(() -> adminService.enableLine(2L, GUID))
|
|
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
|
+ verify(defRepository, times(2)).findFirstBySchIdAndFlowGuidOrderByVersionDesc(2L, GUID);
|
|
verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
|
}
|
|
|
|
+ @Test
|
|
+ void guidTrimmedForBothLookupAndUpdate() {
|
|
+ when(defRepository.updateLineOffline(SCH, GUID, 1)).thenReturn(3);
|
|
+ when(defRepository.updateLineOffline(SCH, GUID, 0)).thenReturn(3);
|
|
+ assertThat(adminService.offlineLine(SCH, " " + GUID + "\t")).isEqualTo(3);
|
|
+ assertThat(adminService.enableLine(SCH, "\t" + GUID + " ")).isEqualTo(3);
|
|
+ verify(defRepository).updateLineOffline(SCH, GUID, 1);
|
|
+ verify(defRepository).updateLineOffline(SCH, GUID, 0);
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ void lineToggleDoesNotChangePublishedDefinitionOrInflightInstances() {
|
|
+ FlwDef published = def(9, 3, 0);
|
|
+ String frozenJson = published.getContentJson();
|
|
+ when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, GUID))
|
|
+ .thenReturn(Optional.of(published));
|
|
+
|
|
+ adminService.offlineLine(SCH, GUID);
|
|
+ adminService.enableLine(SCH, GUID);
|
|
+
|
|
+ assertThat(published.getVersion()).isEqualTo(3);
|
|
+ assertThat(published.getStatus()).isEqualTo(FlwEnums.DefStatus.PUBLISHED.getCode());
|
|
+ assertThat(published.getContentJson()).isEqualTo(frozenJson);
|
|
+ verify(defRepository, never()).save(any());
|
|
+ verifyNoInteractions(instances, instanceService);
|
|
+ }
|
|
+
|
|
// ==================== 新发起/预览的线级守卫 ====================
|
|
|
|
@Test
|
|
@@ -229,7 +260,7 @@ class FlDefLineOfflineTest {
|
|
mock(com.yunhai.flowcenter.repository.FlwRouterRepository.class),
|
|
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
|
|
appRepo(true),
|
|
- mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
|
+ instances,
|
|
mock(FlowDeptAdminService.class),
|
|
mock(FlowEngine.class),
|
|
new ObjectMapper(),
|
|
diff --git a/src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java b/src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java
|
|
@@ -0,0 +1,122 @@
|
|
+package com.yunhai.flowcenter.service;
|
|
+
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
+import com.yunhai.flowcenter.engine.*;
|
|
+import com.yunhai.flowcenter.entity.*;
|
|
+import com.yunhai.flowcenter.repository.*;
|
|
+import org.junit.jupiter.api.Test;
|
|
+import org.springframework.context.ApplicationEventPublisher;
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
+import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
+
|
|
+import javax.persistence.EntityManager;
|
|
+import java.util.*;
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
+
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
+import static org.mockito.ArgumentMatchers.*;
|
|
+import static org.mockito.Mockito.*;
|
|
+
|
|
+/** Real service/engine/activation code with repository mocks. Does not prove DB or transaction behavior. */
|
|
+class FlNewInstancePathTest {
|
|
+ @Test
|
|
+ void freshInstanceCreatesProvenTaskAndApprovesWhileLineIsOffline() {
|
|
+ ObjectMapper mapper = new ObjectMapper();
|
|
+ FlwDefRepository defs = mock(FlwDefRepository.class);
|
|
+ FlwInstanceRepository instances = mock(FlwInstanceRepository.class);
|
|
+ FlwTaskRepository tasks = mock(FlwTaskRepository.class);
|
|
+ FlwBranchRepository branches = mock(FlwBranchRepository.class);
|
|
+ FlwAdvanceIntentRepository intents = mock(FlwAdvanceIntentRepository.class);
|
|
+ AssigneeResolver assignees = mock(AssigneeResolver.class);
|
|
+ ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
|
|
+ List<FlowEvent> events = new ArrayList<>();
|
|
+ doAnswer(call -> { events.add(call.getArgument(0)); return null; })
|
|
+ .when(publisher).publishEvent(any(FlowEvent.class));
|
|
+ FlowEngine engine = new FlowEngine(mapper, tasks, branches, mock(FlwCcRepository.class),
|
|
+ assignees, mock(ConditionRouteService.class), publisher);
|
|
+ FlowInstanceService apply = new FlowInstanceService(instances, defs, tasks, branches, engine, publisher);
|
|
+ FlowTaskService handle = new FlowTaskService(mapper, tasks, instances, branches, defs,
|
|
+ assignees, engine, publisher);
|
|
+ FlowAdvanceService advance = new FlowAdvanceService(instances, defs, tasks, engine, intents);
|
|
+ ReflectionTestUtils.setField(advance, "entityManager", mock(EntityManager.class));
|
|
+ FlowNodeAdvanceListener listener = new FlowNodeAdvanceListener(advance, intents);
|
|
+
|
|
+ FlwDef def = new FlwDef();
|
|
+ def.setId(7L); def.setSchId(1L); def.setFlowGuid("fresh-line");
|
|
+ def.setCode("fresh"); def.setVersion(1); def.setStatus(1); def.setLineOffline(0);
|
|
+ def.setContentJson("{\"start\":\"start\",\"nodes\":["
|
|
+ + "{\"id\":\"start\",\"type\":\"START\"},"
|
|
+ + "{\"id\":\"review\",\"type\":\"APPROVAL\",\"name\":\"Review\",\"countersign\":\"ALL\"},"
|
|
+ + "{\"id\":\"end\",\"type\":\"END\"}],"
|
|
+ + "\"edges\":[{\"from\":\"start\",\"to\":\"review\"},{\"from\":\"review\",\"to\":\"end\"}]}");
|
|
+ when(defs.findById(7L)).thenReturn(Optional.of(def));
|
|
+ when(assignees.resolve(eq(1L), any())).thenReturn(Collections.singletonList(new AssigneeRef(201, "Reviewer")));
|
|
+ AtomicReference<FlwInstance> created = new AtomicReference<>();
|
|
+ when(instances.saveAndFlush(any())).thenAnswer(call -> {
|
|
+ FlwInstance instance = call.getArgument(0);
|
|
+ if (instance.getId() == null) instance.setId(900L);
|
|
+ created.set(instance); return instance;
|
|
+ });
|
|
+ when(instances.findById(900L)).thenAnswer(call -> Optional.ofNullable(created.get()));
|
|
+ AtomicReference<FlwTask> task = new AtomicReference<>();
|
|
+ when(tasks.save(any())).thenAnswer(call -> {
|
|
+ FlwTask row = call.getArgument(0); row.setId(33L); task.set(row); return row;
|
|
+ });
|
|
+ when(tasks.findById(33L)).thenAnswer(call -> Optional.ofNullable(task.get()));
|
|
+ when(tasks.findByInstanceIdAndStatus(900L, 0)).thenAnswer(call ->
|
|
+ task.get().getStatus() == 0 ? Collections.singletonList(task.get()) : Collections.emptyList());
|
|
+ when(tasks.claimAndFinish(eq(33L), eq(1), any(), any(), any())).thenAnswer(call -> {
|
|
+ task.get().setStatus(1); task.get().setEdgeName(call.getArgument(4)); return 1;
|
|
+ });
|
|
+ AtomicReference<FlwAdvanceIntent> activation = new AtomicReference<>();
|
|
+ when(intents.save(any())).thenAnswer(call -> {
|
|
+ FlwAdvanceIntent row = call.getArgument(0); row.setId(80L); activation.set(row); return row;
|
|
+ });
|
|
+ when(intents.findByIdForUpdate(80L)).thenAnswer(call -> Optional.of(activation.get()));
|
|
+ when(intents.findByInstanceIdAndNodeIdOrderByIdDesc(900L, "review"))
|
|
+ .thenAnswer(call -> Collections.singletonList(activation.get()));
|
|
+ when(intents.findFirstByInstanceIdAndNodeIdAndActivationTaskIdIsNotNullOrderByIdDesc(900L, "review"))
|
|
+ .thenAnswer(call -> Optional.of(activation.get()));
|
|
+
|
|
+ FlowInstanceService.ApplyCommand command = new FlowInstanceService.ApplyCommand(
|
|
+ 1L, def, "test", "fresh-20261027", "Fresh", "/test", "{}", 101, "Applicant",
|
|
+ null, "100001", "notice", null);
|
|
+ FlowInstanceService.GuardedOutcome outcome = apply.applyGuarded(command,
|
|
+ new FlowInstanceService.ApplyRetryPolicy("fresh-token", 1, false));
|
|
+ assertThat(outcome.duplicated).isFalse();
|
|
+ assertThat(created.get().getCurrentNodes()).isEqualTo("review");
|
|
+ assertThat(task.get().getStatus()).isZero();
|
|
+ assertThat(task.get().getAssigneeId()).isEqualTo(201);
|
|
+ FlowEvent taskCreated = events.stream().filter(e -> e.getType() == FlowEvent.Type.TASK_CREATED)
|
|
+ .findFirst().orElseThrow(AssertionError::new);
|
|
+ // Explicit listener harness: test synchronizations only, no real transaction/AFTER_COMMIT claim.
|
|
+ TransactionSynchronizationManager.initSynchronization();
|
|
+ try {
|
|
+ listener.persistIntent(taskCreated);
|
|
+ TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.beforeCommit(false));
|
|
+ } finally {
|
|
+ TransactionSynchronizationManager.clearSynchronization();
|
|
+ }
|
|
+ assertThat(activation.get().containsTask(task.get().getId())).isTrue();
|
|
+ assertThat(activation.get().getActivationDefId()).isEqualTo(7L);
|
|
+ assertThat(activation.get().getActivationApplyRound()).isEqualTo(1);
|
|
+
|
|
+ // Existing tasks resolve their pinned definition even when the line is stopped for new applications.
|
|
+ def.setLineOffline(1);
|
|
+ assertThat(handle.handle(1L, 33L, true, "Approved", 201).getStatus()).isEqualTo(1);
|
|
+ FlowEvent handled = events.stream().filter(e -> e.getType() == FlowEvent.Type.TASK_HANDLED)
|
|
+ .findFirst().orElseThrow(AssertionError::new);
|
|
+ listener.persistIntent(handled);
|
|
+ listener.onTaskHandled(handled);
|
|
+ assertThat(activation.get().getStatus()).isEqualTo("DONE");
|
|
+ assertThat(activation.get().getLastError()).isNull();
|
|
+ assertThat(created.get().getStatus()).isEqualTo(20);
|
|
+ assertThat(created.get().getCurrentNodes()).isNullOrEmpty();
|
|
+ assertThat(created.get().getDefId()).isEqualTo(7L);
|
|
+ assertThat(created.get().getDefVersion()).isEqualTo(1);
|
|
+ assertThat(events).extracting(FlowEvent::getType)
|
|
+ .contains(FlowEvent.Type.SUBMITTED, FlowEvent.Type.TASK_CREATED,
|
|
+ FlowEvent.Type.TASK_HANDLED, FlowEvent.Type.COMPLETED);
|
|
+ verify(defs, never()).findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(any(), any(), any());
|
|
+ }
|
|
+}
|