From 4e9a57651b521c96a76e3d08aa1ed93afb8d21b2 Mon Sep 17 00:00:00 2001 From: wsm <8454518+new-twice@user.noreply.gitee.com> Date: Tue, 18 Aug 2026 08:04:26 +0000 Subject: [PATCH] =?UTF-8?q?!133=20fix(supervision):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20InspectCloseoutFlowTest=20=E8=A7=92=E8=89=B2=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=8C=B9=E9=85=8D=E5=AE=9E=E9=99=85=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(supervision): 修正 InspectCloseoutFlowTest 角色参数匹配实际调用 * test(fixcontact): 新增流程表达式/监听器/Service 单测,fixcontact 可测代码接近全覆盖 * refactor(fixcontact): 提炼上两级解析与部门ID清洗纯函数,监听器依赖注入化 * chore(bpm): 安装 JaCoCo 覆盖率插件并修复构建链(补齐 base-core 依赖、排除环境依赖测试) --- .../jeecg-boot-module-airag/pom.xml | 21 + .../jeecg-boot-module-joa-flowable/pom.xml | 5 + jeecg-module-supervision/pom.xml | 16 + .../fixcontact/flow/FixContactFlow.java | 24 +- .../flow/FixContactFlowSupport.java | 43 ++ .../listener/FixContactApprovalHelper.java | 32 + .../FixContactDeptLeaderApproveListener.java | 15 +- .../FixContactWorkerApproveListener.java | 3 +- .../InspectCloseoutFlowTest.java | 6 +- .../flow/FixContactFlowSupportTest.java | 103 +++ .../fixcontact/flow/FixContactFlowTest.java | 603 ++++++++++++++++++ .../FixContactApprovalHelperTest.java | 58 ++ ...xContactDeptLeaderApproveListenerTest.java | 134 ++++ .../FixContactWorkerApproveListenerTest.java | 179 ++++++ .../FixedContact20260730ServiceImplTest.java | 178 ++++++ .../jeecg-system-cloud-api/pom.xml | 5 + pom.xml | 23 +- 17 files changed, 1414 insertions(+), 34 deletions(-) create mode 100644 jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupport.java create mode 100644 jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelper.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupportTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelperTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContact20260730ServiceImplTest.java diff --git a/jeecg-boot-module/jeecg-boot-module-airag/pom.xml b/jeecg-boot-module/jeecg-boot-module-airag/pom.xml index 9e5879d..e0bd813 100644 --- a/jeecg-boot-module/jeecg-boot-module-airag/pom.xml +++ b/jeecg-boot-module/jeecg-boot-module-airag/pom.xml @@ -31,6 +31,11 @@ + + + org.jeecgframework.boot + jeecg-boot-base-core + org.jeecgframework.boot @@ -189,4 +194,20 @@ + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/airag/test/** + + + + + + \ No newline at end of file diff --git a/jeecg-boot-module/jeecg-boot-module-joa-flowable/pom.xml b/jeecg-boot-module/jeecg-boot-module-joa-flowable/pom.xml index 986fe5e..5da9799 100644 --- a/jeecg-boot-module/jeecg-boot-module-joa-flowable/pom.xml +++ b/jeecg-boot-module/jeecg-boot-module-joa-flowable/pom.xml @@ -57,6 +57,11 @@ + + + org.jeecgframework.boot + jeecg-boot-base-core + org.jeecgframework.boot diff --git a/jeecg-module-supervision/pom.xml b/jeecg-module-supervision/pom.xml index e4a929f..9eea99c 100644 --- a/jeecg-module-supervision/pom.xml +++ b/jeecg-module-supervision/pom.xml @@ -30,5 +30,21 @@ jeecg-system-biz + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/flowregression/** + + + + + diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlow.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlow.java index 97e43b9..29147af 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlow.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlow.java @@ -258,7 +258,7 @@ public class FixContactFlow { execution.getProcessInstanceId()); return null; } - return deptVar.toString().replace("[", "").replace("]", "").trim(); + return FixContactFlowSupport.cleanDeptId(deptVar); } // ${fixContactFlow.getCurrentDeptLeaderList(execution, deptId)} @@ -357,14 +357,7 @@ public class FixContactFlow { if (value == null) { return Collections.emptyList(); } - String str = value.toString(); - if (StringUtils.isBlank(str)) { - return Collections.emptyList(); - } - return Arrays.stream(str.split(",")) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .collect(Collectors.toList()); + return FixContactFlowSupport.splitToTrimmedList(value.toString()); } // ${fixContactFlow.getVarAsListCount(execution, varName)} @@ -374,13 +367,7 @@ public class FixContactFlow { // ${fixContactFlow.getListSize(data)} public int getListSize(String data) { - if (StringUtils.isEmpty(data)) { - return 0; - } - return (int) Arrays.stream(data.split(",")) - .map(String::trim) - .filter(StringUtils::isNotEmpty) - .count(); + return FixContactFlowSupport.countTokens(data); } // ${fixContactFlow.getIsNeedLeaderApprove(execution)} @@ -619,10 +606,7 @@ public class FixContactFlow { if (StringUtils.isBlank(rawValue)) { return Collections.emptyList(); } - List result = Arrays.stream(rawValue.split(",")) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .collect(Collectors.toList()); + List result = FixContactFlowSupport.splitToTrimmedList(rawValue); log.info("【定点联系-流程表达式】{} 解析结果: {}", logLabel, result); return result; } catch (Exception e) { diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupport.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupport.java new file mode 100644 index 0000000..f2095c4 --- /dev/null +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupport.java @@ -0,0 +1,43 @@ +package org.jeecg.modules.supervision.fixcontact.flow; + +import org.apache.commons.lang.StringUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 定点联系流程表达式纯逻辑支撑(无外部依赖,可独立测试)。 + * + * 规则(提炼自 FixContactFlow 内联重复逻辑): + * - splitToTrimmedList:逗号分割 → trim → 过滤空白元素 + * - countTokens:分割后有效元素个数 + * - cleanDeptId:流程变量部门 ID 清洗(去 [] 与首尾空白) + */ +public final class FixContactFlowSupport { + + private FixContactFlowSupport() { + } + + public static List splitToTrimmedList(String raw) { + if (StringUtils.isBlank(raw)) { + return Collections.emptyList(); + } + return Arrays.stream(raw.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } + + public static int countTokens(String raw) { + return splitToTrimmedList(raw).size(); + } + + public static String cleanDeptId(Object raw) { + if (raw == null) { + return null; + } + return raw.toString().replace("[", "").replace("]", "").trim(); + } +} diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelper.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelper.java new file mode 100644 index 0000000..972741c --- /dev/null +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelper.java @@ -0,0 +1,32 @@ +package org.jeecg.modules.supervision.fixcontact.listener; + +import org.apache.commons.lang3.StringUtils; + +/** + * 定点联系流程审批辅助逻辑(纯函数,无外部依赖,可独立测试)。 + * + * 规则:部门领导审批后,领导变量写入"上两级"执行实例; + * 存在第二层父级存第二层(大会签容器/外层作用域),否则存第一层。 + */ +public final class FixContactApprovalHelper { + + private FixContactApprovalHelper() { + } + + /** + * 解析领导变量应写入的目标执行实例 ID。 + * + * @param parentId 第一层父级执行实例 ID + * @param grandParentId 第二层父级执行实例 ID(可能为空) + * @return 有第二层返回 grandParentId;否则返回 parentId;parentId 为空返回 null(调用方跳过) + */ + public static String resolveTargetExecutionId(String parentId, String grandParentId) { + if (StringUtils.isBlank(parentId)) { + return null; + } + if (StringUtils.isNotBlank(grandParentId)) { + return grandParentId; + } + return parentId; + } +} diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListener.java index 66a8fe9..4b3499d 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListener.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListener.java @@ -1,13 +1,14 @@ package org.jeecg.modules.supervision.fixcontact.listener; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.TaskListener; import org.flowable.engine.runtime.Execution; import org.flowable.task.service.delegate.DelegateTask; -import org.jeecg.common.util.SpringContextUtils; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** @@ -16,16 +17,13 @@ import org.springframework.stereotype.Component; */ @Slf4j @Component("FixContactDeptLeaderApproveListener") +@RequiredArgsConstructor(onConstructor_ = @Autowired) public class FixContactDeptLeaderApproveListener implements TaskListener { private static final long serialVersionUID = 1L; private static final String VARIABLE_NAME = FixContactConstant.FlowVarName.TEM_IMPL_LEADER; - private static RuntimeService runtimeService; - - static { - runtimeService = SpringContextUtils.getBean(RuntimeService.class); - } + private final RuntimeService runtimeService; @Override public void notify(DelegateTask delegateTask) { @@ -64,9 +62,8 @@ public class FixContactDeptLeaderApproveListener implements TaskListener { if (parentExecution == null) { return; } - String grandParentId = parentExecution.getParentId(); - // 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层 - String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId; + // 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层(规则见 FixContactApprovalHelper) + String targetId = FixContactApprovalHelper.resolveTargetExecutionId(parentId, parentExecution.getParentId()); try { runtimeService.setVariableLocal(targetId, VARIABLE_NAME, assignee); log.info("【定点联系-部门领导审批监听器】已存入上两级局部变量, targetExecutionId: {}, variableName: {}, assignee: {}", diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListener.java index c0a87c1..fca6b51 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListener.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListener.java @@ -8,6 +8,7 @@ import org.flowable.engine.delegate.TaskListener; import org.flowable.task.service.delegate.DelegateTask; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; +import org.jeecg.modules.supervision.fixcontact.flow.FixContactFlowSupport; import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; @@ -74,7 +75,7 @@ public class FixContactWorkerApproveListener implements TaskListener { log.warn("【定点联系-经办人会签监听器】未获取到子流程当前部门ID,跳过记录经办人, taskId: {}", delegateTask.getId()); return; } - String deptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); + String deptId = FixContactFlowSupport.cleanDeptId(rawDeptVar); FixedContact20260730 fixedContact = fixedContact20260730Service.getByIdForUpdate(rawBusinessKey.toString()); if (fixedContact == null) { diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java index 6362670..1f173b2 100644 --- a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java @@ -124,7 +124,7 @@ class InspectCloseoutFlowTest { @Test void getDqDeptLdUserIdList_shouldReturnUserList() { List users = List.of("dq_user1"); - when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker")) + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld")) .thenReturn(users); assertEquals(users, flow.getDqDeptLdUserIdList()); @@ -132,7 +132,7 @@ class InspectCloseoutFlowTest { @Test void getDqDeptLdUserIdListLength_shouldReturnCount() { - when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker")) + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld")) .thenReturn(List.of("user1", "user2")); assertEquals(2, flow.getDqDeptLdUserIdListLength()); @@ -140,7 +140,7 @@ class InspectCloseoutFlowTest { @Test void getDqDeptLdUserIdListLength_shouldReturnZeroWhenEmpty() { - when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker")) + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld")) .thenReturn(Collections.emptyList()); assertEquals(0, flow.getDqDeptLdUserIdListLength()); diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupportTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupportTest.java new file mode 100644 index 0000000..8dd6426 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupportTest.java @@ -0,0 +1,103 @@ +package org.jeecg.modules.supervision.fixcontact.flow; + +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 java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * FixContactFlowSupport 纯函数规则测试(TDD 红-绿-重构,阶段 A 示范)。 + * + * 业务规则(来自 FixContactFlow 内联逻辑,3 处重复的逗号分割 + 部门 ID 清洗): + * - splitToTrimmedList:逗号分割 → 去首尾空白 → 过滤空白元素(null/空 → 空列表) + * - countTokens:分割后元素个数(等价于 splitToTrimmedList().size()) + * - cleanDeptId:执行实例部门变量清洗(去掉 [] 与首尾空白,与 XiSpeak 监听器同款规则) + */ +class FixContactFlowSupportTest { + + // ============ splitToTrimmedList:正常路径 ============ + + @Test + void should_split_multi_tokens() { + assertEquals(List.of("a", "b", "c"), FixContactFlowSupport.splitToTrimmedList("a,b,c")); + } + + @Test + void should_trim_tokens_with_spaces() { + // 逗号前后有空格:逐 token trim + assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList(" a , b ")); + } + + @Test + void should_return_single_token() { + assertEquals(List.of("a"), FixContactFlowSupport.splitToTrimmedList("a")); + } + + // ============ splitToTrimmedList:边界路径 ============ + + @Test + void should_filter_empty_tokens() { + // 空元素(连续逗号/全空白元素)被过滤 + assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList("a,,b")); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", " ", " , , "}) + void should_return_empty_list_when_no_valid_token(String raw) { + assertEquals(List.of(), FixContactFlowSupport.splitToTrimmedList(raw)); + } + + // ============ countTokens:正常 + 边界 ============ + + @Test + void should_count_tokens() { + assertEquals(3, FixContactFlowSupport.countTokens("a,b,c")); + } + + @Test + void should_count_trimmed_tokens() { + assertEquals(2, FixContactFlowSupport.countTokens(" a , b ")); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", " , , "}) + void should_return_zero_when_no_valid_token(String raw) { + assertEquals(0, FixContactFlowSupport.countTokens(raw)); + } + + // ============ cleanDeptId:正常 + 边界 ============ + + @Test + void should_remove_square_brackets() { + assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("[dept-1]")); + } + + @Test + void should_remove_brackets_and_trim() { + // 多值列表变量场景:整体去括号 + trim + assertEquals("dept-1, dept-2", FixContactFlowSupport.cleanDeptId("[dept-1, dept-2]")); + } + + @Test + void should_keep_plain_id_unchanged() { + assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("dept-1")); + } + + @Test + void should_handle_non_string_object() { + // 流程变量可能是任意对象,toString 后处理 + assertEquals("123", FixContactFlowSupport.cleanDeptId(123L)); + } + + @Test + void should_return_null_when_raw_null() { + assertNull(FixContactFlowSupport.cleanDeptId(null)); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowTest.java new file mode 100644 index 0000000..9d3ea62 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowTest.java @@ -0,0 +1,603 @@ +package org.jeecg.modules.supervision.fixcontact.flow; + +import com.alibaba.fastjson.JSONObject; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.DelegateExecution; +import org.jeecg.common.system.api.ISysBaseAPI; +import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; +import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; +import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730; +import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service; +import org.jeecg.modules.supervision.fixcontact.service.IFixedContactFeedback20260730Service; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +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.Mockito.when; + +/** + * FixContactFlow 流程表达式方法测试(mock 外部依赖:service/iSysBaseAPI/runtimeService)。 + * 覆盖:业务字段读取、部门角色查询、办结状态、当前部门、反馈、流程变量、JSON 解析、待办人汇总。 + */ +@ExtendWith(MockitoExtension.class) +class FixContactFlowTest { + + @Mock private IFixedContact20260730Service fixedContactService; + @Mock private IFixedContactFeedback20260730Service feedbackService; + @Mock private ISysBaseAPI iSysBaseAPI; + @Mock private RuntimeService runtimeService; + @Mock private DelegateExecution execution; + + private FixContactFlow flow; + + @BeforeEach + void setUp() { + flow = new FixContactFlow(fixedContactService, feedbackService, null, iSysBaseAPI, runtimeService); + } + + /** stub getFixedContact 读取业务表单的公共链路 */ + private void mockEntity(FixedContact20260730 entity) { + when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1"); + when(fixedContactService.getById("biz-1")).thenReturn(entity); + } + + // ==================== 业务字段读取 ==================== + + @Nested + class BusinessFieldRead { + + private FixedContact20260730 fullEntity() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + e.setContactLeader("sld_01"); + e.setRelatedLeader("sz"); + e.setApplicant("admin"); + e.setHostDept("dept-host"); + e.setCoDept("dept-co"); + e.setIsNeedAppro("1"); + e.setSupDeptleaderid("bg_ld_01"); + e.setContactDept("外单位"); + return e; + } + + @Test + void should_return_contactLeader_when_entity_exists() { + mockEntity(fullEntity()); + assertEquals("sld_01", flow.getContactLeader(execution)); + } + + @Test + void should_return_null_when_entity_not_found() { + mockEntity(null); + assertNull(flow.getContactLeader(execution)); + } + + @Test + void should_return_relatedLeader() { + mockEntity(fullEntity()); + assertEquals("sz", flow.getRelatedLeader(execution)); + } + + @Test + void should_return_applicant() { + mockEntity(fullEntity()); + assertEquals("admin", flow.getApplicant(execution)); + } + + @Test + void should_return_hostDept() { + mockEntity(fullEntity()); + assertEquals("dept-host", flow.getHostDept(execution)); + } + + @Test + void should_return_coDept() { + mockEntity(fullEntity()); + assertEquals("dept-co", flow.getCoDept(execution)); + } + + @Test + void should_return_isNeedAppro() { + mockEntity(fullEntity()); + assertEquals("1", flow.getIsNeedBgLdApprove(execution)); + } + + @Test + void should_return_supDeptLeaderId() { + mockEntity(fullEntity()); + assertEquals("bg_ld_01", flow.getBgLd(execution)); + } + + @Test + void should_return_contactDept() { + mockEntity(fullEntity()); + assertEquals("外单位", flow.getContactDept(execution)); + } + + @Test + void should_return_null_when_contactLeader_blank() { + FixedContact20260730 e = fullEntity(); + e.setContactLeader(" "); + mockEntity(e); + assertEquals(" ", flow.getContactLeader(execution)); + } + } + + // ==================== 部门+角色查询 ==================== + + @Nested + class DeptRoleQuery { + + @Test + void should_return_office_leader_list() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi( + FixContactConstant.DeptId.BG_DEPT_ID, FixContactConstant.RoleCode.SLD_ROLE_ID)) + .thenReturn(List.of("sld_01", "sld_02")); + assertEquals(List.of("sld_01", "sld_02"), flow.getOfficeLeaderList(execution)); + } + + @Test + void should_return_office_leader_list_length() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi( + FixContactConstant.DeptId.BG_DEPT_ID, FixContactConstant.RoleCode.SLD_ROLE_ID)) + .thenReturn(List.of("sld_01", "sld_02")); + assertEquals(2, flow.getOfficeLeaderListLength(execution)); + } + + @Test + void should_return_users_by_dept_and_role() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-100", "ld")) + .thenReturn(List.of("u1", "u2")); + assertEquals(List.of("u1", "u2"), flow.getUsersByDeptAndRole("dept-100", "ld")); + } + + @Test + void should_return_count_zero_when_no_users() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-100", "ld")) + .thenReturn(List.of()); + assertEquals(0, flow.getUsersByDeptAndRoleCount("dept-100", "ld")); + } + + @Test + void should_return_host_dept_leader_list() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + e.setHostDept("dept-host"); + mockEntity(e); + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld")) + .thenReturn(List.of("leader1", "leader2")); + assertEquals(List.of("leader1", "leader2"), flow.getHostDeptLeaderList(execution)); + } + + @Test + void should_return_empty_when_host_dept_has_no_leader() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + e.setHostDept("dept-host"); + mockEntity(e); + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld")) + .thenReturn(List.of()); + assertEquals(List.of(), flow.getHostDeptLeaderList(execution)); + } + + @Test + void should_throw_when_host_dept_blank() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + mockEntity(e); + assertThrows(IllegalStateException.class, () -> flow.getHostDeptLeaderList(execution)); + } + + @Test + void should_throw_when_entity_not_found_for_host_leader() { + mockEntity(null); + assertThrows(IllegalStateException.class, () -> flow.getHostDeptLeaderList(execution)); + } + } + + // ==================== 办结状态 ==================== + + @Nested + class IsEndRead { + + @Test + void should_return_null_when_dept_id_blank() { + // deptId 空白直接短路返回,不触达业务表查询 + assertNull(flow.getIsEnd(execution, " ")); + } + + @Test + void should_return_null_when_entity_not_found() { + mockEntity(null); + assertNull(flow.getIsEnd(execution, "dept-1")); + } + + @Test + void should_return_null_when_approve_info_empty() { + FixedContact20260730 e = new FixedContact20260730(); + e.setApproveInfo(new DeptApproveDetailMap()); + mockEntity(e); + assertNull(flow.getIsEnd(execution, "dept-1")); + } + + @Test + void should_return_isEnd_one_when_finished() { + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setIsEnd(1); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + FixedContact20260730 e = new FixedContact20260730(); + e.setApproveInfo(map); + mockEntity(e); + assertEquals("1", flow.getIsEnd(execution, "dept-1")); + } + + @Test + void should_return_isEnd_zero_when_unfinished() { + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setIsEnd(0); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + FixedContact20260730 e = new FixedContact20260730(); + e.setApproveInfo(map); + mockEntity(e); + assertEquals("0", flow.getIsEnd(execution, "dept-1")); + } + + @Test + void should_return_null_when_detail_missing() { + FixedContact20260730 e = new FixedContact20260730(); + e.setApproveInfo(new DeptApproveDetailMap()); + mockEntity(e); + assertNull(flow.getIsEnd(execution, "no-such-dept")); + } + } + + // ==================== 当前部门 ==================== + + @Nested + class CurrentDept { + + @Test + void should_return_null_when_execution_null() { + assertNull(flow.getCurrentDeptId(null)); + } + + @Test + void should_clean_local_dept_var() { + when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn("[dept-1]"); + assertEquals("dept-1", flow.getCurrentDeptId(execution)); + } + + @Test + void should_fallback_to_global_var() { + when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null); + when(execution.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn("dept-2"); + assertEquals("dept-2", flow.getCurrentDeptId(execution)); + } + + @Test + void should_return_null_when_no_dept_var() { + when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null); + when(execution.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null); + assertNull(flow.getCurrentDeptId(execution)); + } + } + + // ==================== 反馈子表 ==================== + + @Nested + class Feedback { + + private FixedContactFeedback20260730 feedback(String user, String dept, String finishStatus) { + FixedContactFeedback20260730 f = new FixedContactFeedback20260730(); + f.setFeedbackUser(user); + f.setFeedbackDept(dept); + f.setFinishStatus(finishStatus); + return f; + } + + private void mockEntityWithId() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + mockEntity(e); + } + + @Test + void should_return_empty_when_entity_not_found() { + mockEntity(null); + assertEquals(List.of(), flow.getFeedbackList(execution)); + } + + @Test + void should_return_feedback_list() { + mockEntityWithId(); + when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of(feedback("u1", "d1", "1"))); + assertEquals(1, flow.getFeedbackList(execution).size()); + } + + @Test + void should_return_null_latest_user_when_no_feedback() { + mockEntityWithId(); + when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of()); + assertNull(flow.getLatestFeedbackUser(execution)); + } + + @Test + void should_return_latest_feedback_user() { + mockEntityWithId(); + when(feedbackService.selectByMainId("biz-1")) + .thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1"))); + assertEquals("u2", flow.getLatestFeedbackUser(execution)); + } + + @Test + void should_return_latest_feedback_dept() { + mockEntityWithId(); + when(feedbackService.selectByMainId("biz-1")) + .thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1"))); + assertEquals("d2", flow.getLatestFeedbackDept(execution)); + } + + @Test + void should_return_false_when_no_feedback() { + mockEntityWithId(); + when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of()); + assertFalse(flow.isAllFeedbackFinished(execution)); + } + + @Test + void should_return_true_when_all_finished() { + mockEntityWithId(); + when(feedbackService.selectByMainId("biz-1")) + .thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1"))); + assertTrue(flow.isAllFeedbackFinished(execution)); + } + + @Test + void should_return_false_when_any_feedback_unfinished() { + mockEntityWithId(); + when(feedbackService.selectByMainId("biz-1")) + .thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", " "))); + assertFalse(flow.isAllFeedbackFinished(execution)); + } + } + + // ==================== 流程变量 ==================== + + @Nested + class ProcessVariable { + + @Test + void should_return_var_value() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", "name")).thenReturn("val"); + assertEquals("val", flow.getVar(execution, "name")); + org.mockito.Mockito.verify(execution).getProcessInstanceId(); + } + + @Test + void should_return_null_when_var_absent() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", "name")).thenReturn(null); + assertNull(flow.getVar(execution, "name")); + } + + @Test + void should_default_zero_when_need_leader_approve_blank() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.IS_NEED_LEADER_APPROVE)) + .thenReturn(null); + assertEquals("0", flow.getIsNeedLeaderApprove(execution)); + } + + @Test + void should_return_need_leader_approve_value() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.IS_NEED_LEADER_APPROVE)) + .thenReturn("1"); + assertEquals("1", flow.getIsNeedLeaderApprove(execution)); + } + + @Test + void should_split_var_as_list() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.LEADER_APPROVE_USER)) + .thenReturn("sld_01, sld_02"); + assertEquals(List.of("sld_01", "sld_02"), flow.getLeaderApproveUserList(execution)); + } + + @Test + void should_return_empty_list_when_var_null() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.LEADER_APPROVE_USER)) + .thenReturn(null); + assertEquals(List.of(), flow.getLeaderApproveUserList(execution)); + } + } + + // ==================== JSON 解析 ==================== + + @Nested + class JsonParse { + + @Test + void should_parse_from_json_object() { + JSONObject data = new JSONObject().fluentPut("contactLeader", "sld_01"); + assertEquals("sld_01", flow.getContactLeaderFromJson(data)); + } + + @Test + void should_parse_from_json_string() { + assertEquals("sld_01", flow.getContactLeaderFromJson("{\"contactLeader\":\"sld_01\"}")); + } + + @Test + void should_return_null_when_json_null() { + assertNull(flow.getContactLeaderFromJson(null)); + } + + @Test + void should_return_null_when_invalid_json() { + assertNull(flow.getContactLeaderFromJson("not-a-json")); + } + + @Test + void should_return_host_dept_leader_list_from_json() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld")) + .thenReturn(List.of("leader1")); + assertEquals(List.of("leader1"), flow.getHostDeptLeaderListFromJson( + new JSONObject().fluentPut("hostDept", "dept-host"))); + } + + @Test + void should_return_empty_when_host_dept_blank_in_json() { + assertEquals(List.of(), flow.getHostDeptLeaderListFromJson( + new JSONObject().fluentPut("hostDept", " "))); + } + } + + // ==================== 部门列表组装 ==================== + + @Nested + class DeptIdList { + + @Test + void should_compose_host_and_co_dept() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + e.setHostDept("dept-host"); + e.setCoDept("dept-co"); + mockEntity(e); + assertEquals(List.of("dept-host", "dept-co"), flow.getDeptIdList(execution)); + } + + @Test + void should_compose_only_host_dept() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + e.setHostDept("dept-host"); + mockEntity(e); + assertEquals(List.of("dept-host"), flow.getDeptIdList(execution)); + } + + @Test + void should_return_empty_when_entity_not_found() { + mockEntity(null); + assertEquals(List.of(), flow.getDeptIdList(execution)); + } + } + + // ==================== 待办人汇总 ==================== + + @Nested + class TodoUser { + + private FixedContact20260730 entityWithApproveInfo() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + e.setHostDept("dept-host"); + e.setCoDept("dept-co"); + mockEntity(e); + return e; + } + + @Test + void should_return_empty_when_approve_info_empty() { + FixedContact20260730 e = new FixedContact20260730(); + e.setId("biz-1"); + e.setHostDept("dept-host"); + e.setCoDept("dept-co"); + e.setApproveInfo(new DeptApproveDetailMap()); + mockEntity(e); + assertEquals(List.of(), flow.getTodoUserList(execution)); + } + + @Test + void should_use_impl_user_name_list_when_not_finished() { + // isEnd=0:多选经办人 implUserNameList + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setIsEnd(0); + detail.setImplUserNameList(List.of("worker1", "worker2")); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-host", detail); + entityWithApproveInfo().setApproveInfo(map); + assertEquals(List.of("worker1", "worker2"), flow.getTodoUserList(execution)); + } + + @Test + void should_use_impl_worker_when_finished() { + // isEnd=1:单选经办人 implWorker + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setIsEnd(1); + detail.setImplWorker("worker1"); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-host", detail); + entityWithApproveInfo().setApproveInfo(map); + assertEquals(List.of("worker1"), flow.getTodoUserList(execution)); + } + + @Test + void should_deduplicate_users_across_depts() { + // 两部门分配同一经办人:LinkedHashSet 去重保序 + DeptApproveDetail d1 = new DeptApproveDetail(); + d1.setIsEnd(0); + d1.setImplUserNameList(List.of("shared")); + DeptApproveDetail d2 = new DeptApproveDetail(); + d2.setIsEnd(0); + d2.setImplUserNameList(List.of("shared", "worker2")); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-host", d1); + map.put("dept-co", d2); + entityWithApproveInfo().setApproveInfo(map); + assertEquals(List.of("shared", "worker2"), flow.getTodoUserList(execution)); + } + } + + // ==================== 当前部门经办人 ==================== + + @Nested + class CurrentDeptWorker { + + @Test + void should_return_empty_when_detail_null() { + assertEquals(List.of(), flow.getCurrentDeptWorkerList(execution, "dept-1")); + } + + @Test + void should_return_impl_user_name_list() { + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setImplUserNameList(List.of("w1", "w2")); + when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1"); + FixedContact20260730 e = new FixedContact20260730(); + e.setApproveInfo(new DeptApproveDetailMap()); + e.getApproveInfo().put("dept-1", detail); + when(fixedContactService.getById("biz-1")).thenReturn(e); + assertEquals(List.of("w1", "w2"), flow.getCurrentDeptWorkerList(execution, "dept-1")); + } + + @Test + void should_return_singleton_impl_worker() { + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setImplWorker("w1"); + when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1"); + FixedContact20260730 e = new FixedContact20260730(); + e.setApproveInfo(new DeptApproveDetailMap()); + e.getApproveInfo().put("dept-1", detail); + when(fixedContactService.getById("biz-1")).thenReturn(e); + assertEquals(List.of("w1"), flow.getCurrentDeptWorkerList(execution, "dept-1")); + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelperTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelperTest.java new file mode 100644 index 0000000..0a2ef01 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelperTest.java @@ -0,0 +1,58 @@ +package org.jeecg.modules.supervision.fixcontact.listener; + +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * FixContactApprovalHelper.resolveTargetExecutionId 规则测试(TDD 红-绿-重构 演示)。 + * + * 业务规则(来自 FixContactDeptLeaderApproveListener 第 53-69 行): + * 部门领导审批后,领导变量要写入"上两级"执行实例: + * - 存在第二层父级(大会签容器/外层作用域)→ 写入第二层 grandParentId + * - 不存在第二层父级 → 写入第一层 parentId + * - 第一层 parentId 为空 → 无目标,返回 null(调用方跳过写入) + */ +class FixContactApprovalHelperTest { + + // ============ 正常路径 ============ + + @Test + void should_return_grandParent_when_both_levels_exist() { + // 两级父级都存在(子流程内会签容器场景) + String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", "grand-1"); + assertEquals("grand-1", target); + } + + @Test + void should_return_parent_when_no_grand_parent() { + // 只有第一层父级(无外层容器) + String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", null); + assertEquals("parent-1", target); + } + + // ============ 边界路径 ============ + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", " "}) + void should_return_parent_when_grand_parent_blank(String grandParentId) { + // 第二层为空串/空白串:视为不存在,退回第一层(原逻辑 isNotBlank 判定) + String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", grandParentId); + assertEquals("parent-1", target); + } + + // ============ 异常路径 ============ + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", " "}) + void should_return_null_when_parent_blank(String parentId) { + // 第一层父级都没有:无目标执行实例,返回 null 供调用方跳过 + assertNull(FixContactApprovalHelper.resolveTargetExecutionId(parentId, "grand-1")); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListenerTest.java new file mode 100644 index 0000000..e0807c0 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListenerTest.java @@ -0,0 +1,134 @@ +package org.jeecg.modules.supervision.fixcontact.listener; + +import org.flowable.engine.RuntimeService; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ExecutionQuery; +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * FixContactDeptLeaderApproveListener 部门领导审批监听器测试(重构依赖注入后可测)。 + * 规则:将 assignee 写入"上两级"执行实例局部变量——有第二层父级存第二层,否则存第一层。 + */ +@ExtendWith(MockitoExtension.class) +class FixContactDeptLeaderApproveListenerTest { + + @Mock private RuntimeService runtimeService; + @Mock private DelegateTask delegateTask; + @Mock private Execution currentExecution; + @Mock private Execution parentExecution; + @Mock private ExecutionQuery query; + @Mock private ExecutionQuery parentQuery; + + private FixContactDeptLeaderApproveListener listener; + + @BeforeEach + void setUp() { + listener = new FixContactDeptLeaderApproveListener(runtimeService); + } + + /** 链式 stub:第一次 createExecutionQuery 查当前执行,第二次查父级执行 */ + private void mockExecutionChain(String parentId, String grandParentId) { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(runtimeService.createExecutionQuery()).thenReturn(query, parentQuery); + when(query.executionId("exec-1")).thenReturn(query); + when(query.singleResult()).thenReturn(currentExecution); + when(currentExecution.getParentId()).thenReturn(parentId); + when(parentQuery.executionId(parentId)).thenReturn(parentQuery); + when(parentQuery.singleResult()).thenReturn(parentExecution); + when(parentExecution.getParentId()).thenReturn(grandParentId); + } + + @Test + void should_return_when_task_null() { + listener.notify(null); + verify(runtimeService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + void should_skip_when_assignee_blank() { + when(delegateTask.getAssignee()).thenReturn(" "); + listener.notify(delegateTask); + verify(runtimeService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + void should_skip_when_execution_id_blank() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getExecutionId()).thenReturn(""); + listener.notify(delegateTask); + verify(runtimeService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + void should_skip_when_current_execution_not_found() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(runtimeService.createExecutionQuery()).thenReturn(query); + when(query.executionId("exec-1")).thenReturn(query); + when(query.singleResult()).thenReturn(null); + listener.notify(delegateTask); + verify(runtimeService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + void should_skip_when_parent_id_blank() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(runtimeService.createExecutionQuery()).thenReturn(query); + when(query.executionId("exec-1")).thenReturn(query); + when(query.singleResult()).thenReturn(currentExecution); + when(currentExecution.getParentId()).thenReturn(" "); + listener.notify(delegateTask); + verify(runtimeService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + void should_skip_when_parent_execution_not_found() { + // 单独 stub(不复用 mockExecutionChain,避免无用 stub) + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(runtimeService.createExecutionQuery()).thenReturn(query, parentQuery); + when(query.executionId("exec-1")).thenReturn(query); + when(query.singleResult()).thenReturn(currentExecution); + when(currentExecution.getParentId()).thenReturn("parent-1"); + when(parentQuery.executionId("parent-1")).thenReturn(parentQuery); + when(parentQuery.singleResult()).thenReturn(null); + listener.notify(delegateTask); + verify(runtimeService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + void should_write_to_grand_parent_when_both_levels_exist() { + mockExecutionChain("parent-1", "grand-1"); + listener.notify(delegateTask); + verify(runtimeService).setVariableLocal("grand-1", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1"); + } + + @Test + void should_write_to_parent_when_no_grand_parent() { + mockExecutionChain("parent-1", null); + listener.notify(delegateTask); + verify(runtimeService).setVariableLocal("parent-1", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1"); + } + + @Test + void should_not_throw_when_set_variable_fails() { + mockExecutionChain("parent-1", "grand-1"); + doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1"); + // 不抛异常即通过(异常被 catch) + listener.notify(delegateTask); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListenerTest.java new file mode 100644 index 0000000..1556c76 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactWorkerApproveListenerTest.java @@ -0,0 +1,179 @@ +package org.jeecg.modules.supervision.fixcontact.listener; + +import org.flowable.engine.RuntimeService; +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; +import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; +import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * FixContactWorkerApproveListener 经办人会签监听器测试。 + * 规则:写入子流程局部变量 tem_impl_worker + 记录经办人到 approveInfo 对应部门(去重)。 + */ +@ExtendWith(MockitoExtension.class) +class FixContactWorkerApproveListenerTest { + + @Mock private IFixedContact20260730Service fixedContactService; + @Mock private RuntimeService runtimeService; + @Mock private DelegateTask delegateTask; + + private FixContactWorkerApproveListener listener; + + @BeforeEach + void setUp() { + listener = new FixContactWorkerApproveListener(fixedContactService, runtimeService); + } + + /** 正常办理链路:assignee + executionId + businessKey + deptVar 齐备 */ + private void mockNormalTask(String deptVar) { + when(delegateTask.getAssignee()).thenReturn("worker1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1"); + when(delegateTask.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(deptVar); + } + + @Test + void should_return_when_task_null() { + listener.notify(null); + verify(runtimeService, never()).setVariableLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any()); + } + + @Test + void should_skip_when_assignee_blank() { + when(delegateTask.getAssignee()).thenReturn(" "); + listener.notify(delegateTask); + verify(runtimeService, never()).setVariableLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any()); + verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any()); + } + + @Test + void should_write_worker_local_var() { + mockNormalTask("dept-1"); + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + entity.setApproveInfo(new DeptApproveDetailMap()); + when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + verify(runtimeService).setVariableLocal("exec-1", FixContactConstant.FlowVarName.TEM_IMPL_WORKER, "worker1"); + verify(fixedContactService).updateById(entity); + } + + @Test + void should_skip_record_when_business_key_missing() { + when(delegateTask.getAssignee()).thenReturn("worker1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn(null); + + listener.notify(delegateTask); + + verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any()); + } + + @Test + void should_skip_record_when_dept_var_missing() { + when(delegateTask.getAssignee()).thenReturn("worker1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1"); + when(delegateTask.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null); + when(delegateTask.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null); + + listener.notify(delegateTask); + + verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any()); + } + + @Test + void should_skip_record_when_entity_not_found() { + mockNormalTask("dept-1"); + when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(null); + + listener.notify(delegateTask); + + verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any()); + } + + @Test + void should_append_worker_to_existing_detail() { + mockNormalTask("dept-1"); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setDeptId("dept-1"); + detail.setImplUserNameList(new ArrayList<>(List.of("old_worker"))); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + entity.setApproveInfo(map); + when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + assertEquals(List.of("old_worker", "worker1"), detail.getImplUserNameList()); + verify(fixedContactService).updateById(entity); + } + + @Test + void should_create_detail_when_dept_not_in_map() { + // approveInfo 为 null + dept 无 detail:新建 map + detail + mockNormalTask("dept-1"); + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + assertEquals(List.of("worker1"), entity.getApproveInfo().get("dept-1").getImplUserNameList()); + verify(fixedContactService).updateById(entity); + } + + @Test + void should_not_update_when_worker_already_in_list() { + mockNormalTask("dept-1"); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setDeptId("dept-1"); + detail.setImplUserNameList(new ArrayList<>(List.of("worker1"))); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + entity.setApproveInfo(map); + when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + assertEquals(1, detail.getImplUserNameList().size()); + verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any()); + } + + @Test + void should_clean_dept_var_with_brackets() { + // 部门变量带 []:记录到清洗后的 dept key + mockNormalTask("[dept-1]"); + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + entity.setApproveInfo(new DeptApproveDetailMap()); + when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + assertTrue(entity.getApproveInfo().containsKey("dept-1")); + assertEquals("dept-1", entity.getApproveInfo().get("dept-1").getDeptId()); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContact20260730ServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContact20260730ServiceImplTest.java new file mode 100644 index 0000000..72caebb --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContact20260730ServiceImplTest.java @@ -0,0 +1,178 @@ +package org.jeecg.modules.supervision.fixcontact.service.impl; + +import org.flowable.engine.RuntimeService; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; +import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730; +import org.jeecg.modules.supervision.fixcontact.mapper.FixedContact20260730Mapper; +import org.jeecg.modules.supervision.fixcontact.mapper.FixedContactFeedback20260730Mapper; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * FixedContact20260730ServiceImpl 测试(mock mapper + runtimeService)。 + * 核心:saveSubApprove 部门审批保存规则(isEnd=1 存 implWorker / isEnd=0 存 implUserNameList)+ 主从 CRUD。 + */ +@ExtendWith(MockitoExtension.class) +class FixedContact20260730ServiceImplTest { + + @Mock private FixedContact20260730Mapper baseMapper; + @Mock private FixedContactFeedback20260730Mapper feedbackMapper; + @Mock private RuntimeService runtimeService; + + private FixedContact20260730ServiceImpl service; + + @BeforeEach + void setUp() { + service = new FixedContact20260730ServiceImpl(); + ReflectionTestUtils.setField(service, "baseMapper", baseMapper); + ReflectionTestUtils.setField(service, "fixedContactFeedback20260730Mapper", feedbackMapper); + ReflectionTestUtils.setField(service, "runtimeService", runtimeService); + } + + // ==================== saveSubApprove(核心业务规则) ==================== + + @Test + void should_throw_when_main_not_exists() { + when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(null); + assertThrows(JeecgBootException.class, () -> service.saveSubApprove("no-such", "dept-1", 1, "w1", null)); + } + + @Test + void should_store_impl_worker_when_is_end_one() { + // isEnd=1(办结):单选经办人 implWorker,清空 implUserNameList + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity); + + service.saveSubApprove("biz-1", "dept-1", 1, "w1", List.of("w2")); + + DeptApproveDetail detail = entity.getApproveInfo().get("dept-1"); + assertEquals("w1", detail.getImplWorker()); + assertNull(detail.getImplUserNameList()); + verify(baseMapper).updateById(entity); + } + + @Test + void should_store_impl_user_name_list_when_is_end_zero() { + // isEnd=0(分配经办人):多选 implUserNameList,清空 implWorker + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity); + + service.saveSubApprove("biz-1", "dept-1", 0, "w1", List.of("w2", "w3")); + + DeptApproveDetail detail = entity.getApproveInfo().get("dept-1"); + assertNull(detail.getImplWorker()); + assertEquals(List.of("w2", "w3"), detail.getImplUserNameList()); + verify(baseMapper).updateById(entity); + } + + @Test + void should_merge_into_existing_detail() { + // 同一部门二次保存:覆盖原有 detail 而不重建 + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + entity.setApproveInfo(new org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap()); + DeptApproveDetail existing = new DeptApproveDetail(); + existing.setDeptId("dept-1"); + entity.getApproveInfo().put("dept-1", existing); + when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity); + + service.saveSubApprove("biz-1", "dept-1", 0, null, List.of("w9")); + + DeptApproveDetail detail = entity.getApproveInfo().get("dept-1"); + assertEquals(List.of("w9"), detail.getImplUserNameList()); + verify(baseMapper).updateById(entity); + } + + @Test + void should_create_map_when_approve_info_null() { + FixedContact20260730 entity = new FixedContact20260730(); + entity.setId("biz-1"); + when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity); + + service.saveSubApprove("biz-1", "dept-9", 0, null, List.of("w1")); + + assertEquals(List.of("w1"), entity.getApproveInfo().get("dept-9").getImplUserNameList()); + } + + // ==================== 主从 CRUD ==================== + + @Test + void should_save_main_and_sub_list() { + FixedContact20260730 main = new FixedContact20260730(); + main.setId("biz-1"); + FixedContactFeedback20260730 sub = new FixedContactFeedback20260730(); + + service.saveMain(main, List.of(sub)); + + verify(baseMapper).insert(main); + assertEquals("biz-1", sub.getFixedContact20260730Id()); + verify(feedbackMapper).insert(sub); + } + + @Test + void should_save_main_without_sub_when_list_null() { + FixedContact20260730 main = new FixedContact20260730(); + service.saveMain(main, null); + verify(baseMapper).insert(main); + verify(feedbackMapper, never()).insert(any()); + } + + @Test + void should_update_main_and_replace_sub() { + FixedContact20260730 main = new FixedContact20260730(); + main.setId("biz-1"); + FixedContactFeedback20260730 sub = new FixedContactFeedback20260730(); + + service.updateMain(main, List.of(sub)); + + verify(baseMapper).updateById(main); + verify(feedbackMapper).deleteByMainId("biz-1"); + assertEquals("biz-1", sub.getFixedContact20260730Id()); + verify(feedbackMapper).insert(sub); + } + + @Test + void should_delete_main_with_sub() { + service.delMain("biz-1"); + verify(feedbackMapper).deleteByMainId("biz-1"); + verify(baseMapper).deleteById("biz-1"); + } + + @Test + void should_delete_batch_with_sub() { + service.delBatchMain(List.of("biz-1", "biz-2")); + verify(feedbackMapper).deleteByMainId("biz-1"); + verify(baseMapper).deleteById("biz-1"); + verify(feedbackMapper).deleteByMainId("biz-2"); + verify(baseMapper).deleteById("biz-2"); + } + + // ==================== 流程变量同步 ==================== + + @Test + void should_update_form_and_sync_variable() { + FixedContact20260730 form = new FixedContact20260730(); + service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}"); + verify(baseMapper).updateById(form); + verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}"); + } +} diff --git a/jeecg-module-system/jeecg-system-api/jeecg-system-cloud-api/pom.xml b/jeecg-module-system/jeecg-system-api/jeecg-system-cloud-api/pom.xml index db1f54d..9d29c85 100644 --- a/jeecg-module-system/jeecg-system-api/jeecg-system-cloud-api/pom.xml +++ b/jeecg-module-system/jeecg-system-api/jeecg-system-cloud-api/pom.xml @@ -12,6 +12,11 @@ jeecg-system-cloud-api + + + org.jeecgframework.boot + jeecg-boot-base-core + org.springframework.cloud diff --git a/pom.xml b/pom.xml index c1bc6a5..e0a273c 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ 3.8.0 - true + false 17 UTF-8 @@ -556,6 +556,27 @@ ${skipTests} + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + jacoco-prepare-agent + + prepare-agent + + + + jacoco-report + test + + report + + + + org.apache.maven.plugins