From 4d006e16d1e0afac5ba5348a03e8ecc4408ff646 Mon Sep 17 00:00:00 2001 From: wsm <8454518+new-twice@user.noreply.gitee.com> Date: Wed, 19 Aug 2026 08:55:37 +0000 Subject: [PATCH] =?UTF-8?q?!135=20test(improvementproposal):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=AF=84=E5=88=86/=E5=A5=96=E7=BA=A7/=E6=B5=81?= =?UTF-8?q?=E6=B0=B4=E5=8F=B7=E7=BA=AF=E5=87=BD=E6=95=B0=E4=B8=8E=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(improvementproposal): 新增评分/奖级/流水号纯函数与流程单测 * refactor(improvementproposal): 提炼评分/奖级/流水号纯函数 ImprovementProposalSuppo… * test(tasklist): 新增看板 Service 单测并消除第 11 处重复(normalizePid 复用 support) * test(tasklist): 新增任务详情 Service/纯函数单测(权限与父子计数规则) * refactor(tasklist): 提炼任务详情纯函数 TaskListDetailSupport,消除内联重复 * test(supervision): 补齐中小头 Service/枚举/监听器单测 * refactor(xispeak): AfterSDW 监听器复用 FlowVariableSupport,消除第 10 处重复 * test(bgpartymatter): 新增党委会 Service 单测 * test(supervision): 新增反馈流程/反馈Service/问题清单三层校验单测 * refactor(xispeak): 反馈流程经办人分割复用 FlowVariableSupport,消除第 9 处重复 * test(xispeak): 补齐 P1 Service 与监听器单测并消除第 8 处部门ID清洗重复 * test(supervision): 删除已上提的旧 helper 测试文件 * test(supervision): 新增 xispeak 流程/监听器测试并将 helper 测试上提到 common.flow * refactor(supervision): 流程通用逻辑上提到 common.flow 并消除 fixcontact/xispeak 重复 * test(supervision): 补齐 dqinspecttask Service/监听器单测(validateProblemId 规则锁定) --- .../flow/FlowExecutionSupport.java} | 13 +- .../flow/FlowVariableSupport.java} | 10 +- .../fixcontact/flow/FixContactFlow.java | 9 +- .../FixContactDeptLeaderApproveListener.java | 5 +- .../FixContactWorkerApproveListener.java | 4 +- .../impl/ImprovementProposalServiceImpl.java | 55 +-- .../impl/ImprovementProposalSupport.java | 86 +++++ .../service/impl/TaskListDetailSupport.java | 65 ++++ .../impl/TaskListDetialServiceImpl.java | 75 +--- .../service/impl/TaskListServiceImpl.java | 7 +- .../xispeak/flow/XiSpeakFeedbackFlow.java | 6 +- .../supervision/xispeak/flow/XiSpeakFlow.java | 27 +- .../AfterBgLeaderApproveListener.java | 6 +- .../AfterImplDeptLeaderApproveListener.java | 12 +- .../AfterImplWorkerApproveListener.java | 3 +- .../AfterImplWorkerTemStoreListener.java | 12 +- .../listener/AfterSDWApproveHqListener.java | 6 +- .../BgPartymatterFeedbackServiceImplTest.java | 44 +++ .../impl/BgPartymatterServiceImplTest.java | 62 ++++ .../BgTakepulseFeedbackServiceImplTest.java | 41 +++ .../impl/BgTakepulseServiceImplTest.java | 103 ++++++ .../supervision/common/BpmStatusTest.java | 32 ++ .../flow/FlowExecutionSupportTest.java} | 18 +- .../flow/FlowVariableSupportTest.java} | 36 +- .../impl/ProcessPortalServiceImplTest.java | 71 ++++ .../impl/DqInspectProblemServiceImplTest.java | 210 +++++++++++ ...InspectTaskStageIncrementListenerTest.java | 103 ++++++ .../DqInspectProgressServiceImplTest.java | 44 +++ .../impl/DqInspectTaskServiceImplTest.java | 185 ++++++++++ ...ontactFeedback20260730ServiceImplTest.java | 42 +++ .../ImprovementDeptScoreServiceImplTest.java | 41 +++ ...provementInstituteVoteServiceImplTest.java | 41 +++ .../ImprovementProposalServiceImplTest.java | 233 ++++++++++++ .../impl/ImprovementProposalSupportTest.java | 97 +++++ .../impl/TaskListDetailSupportTest.java | 88 +++++ .../impl/TaskListDetialServiceImplTest.java | 193 ++++++++++ .../impl/TaskListFavoriteServiceImplTest.java | 127 +++++++ .../TaskListPermissionServiceImplTest.java | 41 +++ .../service/impl/TaskListServiceImplTest.java | 236 ++++++++++++ .../xispeak/flow/XiSpeakFeedbackFlowTest.java | 259 +++++++++++++ .../xispeak/flow/XiSpeakFlowTest.java | 300 +++++++++++++++ .../AfterBgLeaderApproveListenerTest.java | 128 +++++++ ...AfterBgWorkerFinalApproveListenerTest.java | 61 ++++ ...DeptLeaderApproveFeedbackListenerTest.java | 126 +++++++ ...fterImplDeptLeaderApproveListenerTest.java | 116 ++++++ ...erImplDeptLeaderStoreJsonListenerTest.java | 148 ++++++++ .../AfterImplWorkerApproveListenerTest.java | 134 +++++++ .../AfterImplWorkerTemStoreListenerTest.java | 116 ++++++ .../BgXiSpeakFeedbackServiceImplTest.java | 62 ++++ .../impl/BgXiSpeakServiceImplTest.java | 345 ++++++++++++++++++ 50 files changed, 4073 insertions(+), 211 deletions(-) rename jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/{fixcontact/listener/FixContactApprovalHelper.java => common/flow/FlowExecutionSupport.java} (62%) rename jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/{fixcontact/flow/FixContactFlowSupport.java => common/flow/FlowVariableSupport.java} (77%) create mode 100644 jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalSupport.java create mode 100644 jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupport.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterFeedbackServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseFeedbackServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/common/BpmStatusTest.java rename jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/{fixcontact/listener/FixContactApprovalHelperTest.java => common/flow/FlowExecutionSupportTest.java} (69%) rename jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/{fixcontact/flow/FixContactFlowSupportTest.java => common/flow/FlowVariableSupportTest.java} (60%) create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dashboard/service/impl/ProcessPortalServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspectproblem/service/impl/DqInspectProblemServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/listener/DqInspectTaskStageIncrementListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectProgressServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectTaskServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContactFeedback20260730ServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementDeptScoreServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementInstituteVoteServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalSupportTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupportTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListFavoriteServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListPermissionServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlowTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlowTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgWorkerFinalApproveListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveFeedbackListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderStoreJsonListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListenerTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakFeedbackServiceImplTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakServiceImplTest.java 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/common/flow/FlowExecutionSupport.java similarity index 62% rename from jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelper.java rename to jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/common/flow/FlowExecutionSupport.java index 972741c..3967225 100644 --- 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/common/flow/FlowExecutionSupport.java @@ -1,20 +1,21 @@ -package org.jeecg.modules.supervision.fixcontact.listener; +package org.jeecg.modules.supervision.common.flow; import org.apache.commons.lang3.StringUtils; /** - * 定点联系流程审批辅助逻辑(纯函数,无外部依赖,可独立测试)。 + * Flowable 流程通用支撑:执行实例层级解析(无外部依赖,可独立测试)。 * - * 规则:部门领导审批后,领导变量写入"上两级"执行实例; + * 规则(提炼自 fixcontact/xispeak 多处重复内联逻辑): + * 部门领导/经办人审批后,将审批人写入"上两级"执行实例; * 存在第二层父级存第二层(大会签容器/外层作用域),否则存第一层。 */ -public final class FixContactApprovalHelper { +public final class FlowExecutionSupport { - private FixContactApprovalHelper() { + private FlowExecutionSupport() { } /** - * 解析领导变量应写入的目标执行实例 ID。 + * 解析审批人应写入的目标执行实例 ID。 * * @param parentId 第一层父级执行实例 ID * @param grandParentId 第二层父级执行实例 ID(可能为空) 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/common/flow/FlowVariableSupport.java similarity index 77% rename from jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupport.java rename to jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/common/flow/FlowVariableSupport.java index f2095c4..147a837 100644 --- 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/common/flow/FlowVariableSupport.java @@ -1,4 +1,4 @@ -package org.jeecg.modules.supervision.fixcontact.flow; +package org.jeecg.modules.supervision.common.flow; import org.apache.commons.lang.StringUtils; @@ -8,16 +8,16 @@ import java.util.List; import java.util.stream.Collectors; /** - * 定点联系流程表达式纯逻辑支撑(无外部依赖,可独立测试)。 + * Flowable 流程通用支撑:流程变量字符串处理(无外部依赖,可独立测试)。 * - * 规则(提炼自 FixContactFlow 内联重复逻辑): + * 规则(提炼自 fixcontact/xispeak 多处重复内联逻辑): * - splitToTrimmedList:逗号分割 → trim → 过滤空白元素 * - countTokens:分割后有效元素个数 * - cleanDeptId:流程变量部门 ID 清洗(去 [] 与首尾空白) */ -public final class FixContactFlowSupport { +public final class FlowVariableSupport { - private FixContactFlowSupport() { + private FlowVariableSupport() { } public static List splitToTrimmedList(String raw) { 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 29147af..21ea212 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 @@ -8,6 +8,7 @@ import org.apache.commons.lang.StringUtils; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.DelegateExecution; import org.jeecg.common.system.api.ISysBaseAPI; +import org.jeecg.modules.supervision.common.flow.FlowVariableSupport; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730; @@ -258,7 +259,7 @@ public class FixContactFlow { execution.getProcessInstanceId()); return null; } - return FixContactFlowSupport.cleanDeptId(deptVar); + return FlowVariableSupport.cleanDeptId(deptVar); } // ${fixContactFlow.getCurrentDeptLeaderList(execution, deptId)} @@ -357,7 +358,7 @@ public class FixContactFlow { if (value == null) { return Collections.emptyList(); } - return FixContactFlowSupport.splitToTrimmedList(value.toString()); + return FlowVariableSupport.splitToTrimmedList(value.toString()); } // ${fixContactFlow.getVarAsListCount(execution, varName)} @@ -367,7 +368,7 @@ public class FixContactFlow { // ${fixContactFlow.getListSize(data)} public int getListSize(String data) { - return FixContactFlowSupport.countTokens(data); + return FlowVariableSupport.countTokens(data); } // ${fixContactFlow.getIsNeedLeaderApprove(execution)} @@ -606,7 +607,7 @@ public class FixContactFlow { if (StringUtils.isBlank(rawValue)) { return Collections.emptyList(); } - List result = FixContactFlowSupport.splitToTrimmedList(rawValue); + List result = FlowVariableSupport.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/listener/FixContactDeptLeaderApproveListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactDeptLeaderApproveListener.java index 4b3499d..491c64c 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 @@ -7,6 +7,7 @@ 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.modules.supervision.common.flow.FlowExecutionSupport; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -62,8 +63,8 @@ public class FixContactDeptLeaderApproveListener implements TaskListener { if (parentExecution == null) { return; } - // 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层(规则见 FixContactApprovalHelper) - String targetId = FixContactApprovalHelper.resolveTargetExecutionId(parentId, parentExecution.getParentId()); + // 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层(规则见 FlowExecutionSupport) + String targetId = FlowExecutionSupport.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 fca6b51..6866038 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,7 +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.common.flow.FlowVariableSupport; import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; @@ -75,7 +75,7 @@ public class FixContactWorkerApproveListener implements TaskListener { log.warn("【定点联系-经办人会签监听器】未获取到子流程当前部门ID,跳过记录经办人, taskId: {}", delegateTask.getId()); return; } - String deptId = FixContactFlowSupport.cleanDeptId(rawDeptVar); + String deptId = FlowVariableSupport.cleanDeptId(rawDeptVar); FixedContact20260730 fixedContact = fixedContact20260730Service.getByIdForUpdate(rawBusinessKey.toString()); if (fixedContact == null) { diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImpl.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImpl.java index 66dc74f..5f7fad0 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImpl.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImpl.java @@ -161,7 +161,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl= 0) { - return AWARD_GOLD; - } - if (averageScore.compareTo(BigDecimal.valueOf(60)) >= 0) { - return AWARD_SILVER; - } - if (averageScore.compareTo(BigDecimal.valueOf(40)) >= 0) { - return AWARD_BRONZE; - } - return AWARD_PARTICIPATION; - } private ImprovementProposal validateRunningTask(ImprovementProposalBpmActionDTO action, LoginUser loginUser, String expectedTaskDefKey) { if (action == null || oConvertUtils.isEmpty(action.getMainId()) || oConvertUtils.isEmpty(action.getProcessInstanceId()) @@ -858,33 +830,24 @@ public class ImprovementProposalServiceImpl extends ServiceImpl= 0) { + return AWARD_GOLD; + } + if (averageScore.compareTo(BigDecimal.valueOf(60)) >= 0) { + return AWARD_SILVER; + } + if (averageScore.compareTo(BigDecimal.valueOf(40)) >= 0) { + return AWARD_BRONZE; + } + return AWARD_PARTICIPATION; + } + + public static boolean isValidAward(String award) { + return AWARD_GOLD.equals(award) || AWARD_SILVER.equals(award) + || AWARD_BRONZE.equals(award) || AWARD_PARTICIPATION.equals(award); + } + + public static int awardRank(String award) { + return Integer.parseInt(award); + } + + public static boolean needsDeptScore(String award) { + return AWARD_GOLD.equals(award) || AWARD_SILVER.equals(award); + } +} diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupport.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupport.java new file mode 100644 index 0000000..a9a0509 --- /dev/null +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupport.java @@ -0,0 +1,65 @@ +package org.jeecg.modules.supervision.tasklist.service.impl; + +import org.jeecg.common.util.oConvertUtils; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * 任务清单详情纯逻辑支撑(无外部依赖,可独立测试)。 + * + * 规则(提炼自 TaskListDetialServiceImpl 内联逻辑): + * - normalizePid:空 pid 归一为 null + * - collectUserIds:逗号分隔用户ID收集到集合(trim + 去空) + * - buildNamesFromMap:ID 列表 → 姓名列表(按映射,保序拼接) + * - mergeIds:两组用户ID合并去重(LinkedHashSet 保序) + */ +public final class TaskListDetailSupport { + + private TaskListDetailSupport() { + } + + public static String normalizePid(String pid) { + return oConvertUtils.isNotEmpty(pid) ? pid : null; + } + + public static void collectUserIds(String idsStr, Set idSet) { + if (oConvertUtils.isEmpty(idsStr)) { + return; + } + for (String id : idsStr.split(",")) { + String trimmed = id.trim(); + if (oConvertUtils.isNotEmpty(trimmed)) { + idSet.add(trimmed); + } + } + } + + public static String buildNamesFromMap(String idsStr, Map idToRealname) { + if (oConvertUtils.isEmpty(idsStr) || idToRealname.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (String id : idsStr.split(",")) { + String trimmed = id.trim(); + if (oConvertUtils.isNotEmpty(trimmed)) { + String name = idToRealname.get(trimmed); + if (name != null) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(name); + } + } + } + return sb.length() > 0 ? sb.toString() : null; + } + + public static String mergeIds(String existingIds, String newIds) { + Set idSet = new LinkedHashSet<>(); + collectUserIds(existingIds, idSet); + collectUserIds(newIds, idSet); + return idSet.isEmpty() ? null : String.join(",", idSet); + } +} diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImpl.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImpl.java index 52f1b44..efd35e4 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImpl.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImpl.java @@ -75,7 +75,7 @@ public class TaskListDetialServiceImpl extends ServiceImpl list) { if (list == null || list.isEmpty()) { @@ -568,9 +565,9 @@ public class TaskListDetialServiceImpl extends ServiceImpl idSet) { - if (oConvertUtils.isEmpty(idsStr)) { - return; - } - for (String id : idsStr.split(",")) { - String trimmed = id.trim(); - if (oConvertUtils.isNotEmpty(trimmed)) { - idSet.add(trimmed); - } - } - } - private String buildNamesFromMap(String idsStr, Map idToRealname) { - if (oConvertUtils.isEmpty(idsStr) || idToRealname.isEmpty()) { - return null; - } - StringBuilder sb = new StringBuilder(); - for (String id : idsStr.split(",")) { - String trimmed = id.trim(); - if (oConvertUtils.isNotEmpty(trimmed)) { - String name = idToRealname.get(trimmed); - if (name != null) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(name); - } - } - } - return sb.length() > 0 ? sb.toString() : null; - } private String translateUserIdsToNames(String ids) { if (oConvertUtils.isEmpty(ids)) { @@ -664,26 +631,6 @@ public class TaskListDetialServiceImpl extends ServiceImpl idSet = new LinkedHashSet<>(); - if (oConvertUtils.isNotEmpty(existingIds)) { - for (String id : existingIds.split(",")) { - String trimmed = id.trim(); - if (oConvertUtils.isNotEmpty(trimmed)) { - idSet.add(trimmed); - } - } - } - if (oConvertUtils.isNotEmpty(newIds)) { - for (String id : newIds.split(",")) { - String trimmed = id.trim(); - if (oConvertUtils.isNotEmpty(trimmed)) { - idSet.add(trimmed); - } - } - } - return idSet.isEmpty() ? null : String.join(",", idSet); - } private String filterUsersBySecLevel(String userIds, Integer listSecLevel) { if (oConvertUtils.isEmpty(userIds)) return userIds; diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImpl.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImpl.java index ea43daf..16c1807 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImpl.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImpl.java @@ -233,7 +233,7 @@ public class TaskListServiceImpl extends ServiceImpl i throw new RuntimeException("分组不支持移动操作"); } - String oldPid = normalizePid(favorite.getPid()); + String oldPid = TaskListDetailSupport.normalizePid(favorite.getPid()); String newPid = null; if (req.getTargetGroupId() != null) { @@ -242,7 +242,7 @@ public class TaskListServiceImpl extends ServiceImpl i if (targetGroup == null || !"0".equals(targetGroup.getType()) || !userId.equals(targetGroup.getUserId())) { throw new RuntimeException("目标分组不存在或无权限"); } - newPid = normalizePid(req.getTargetGroupId()); + newPid = TaskListDetailSupport.normalizePid(req.getTargetGroupId()); } else { newPid = ""; } @@ -700,9 +700,6 @@ public class TaskListServiceImpl extends ServiceImpl i return taskListMapper.selectCount(query).intValue(); } - private String normalizePid(String pid) { - return oConvertUtils.isNotEmpty(pid) ? pid : null; - } private List enrichListSummaries(List lists, String currentUserId) { if (lists == null || lists.isEmpty()) { diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlow.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlow.java index 8232c38..a944f4e 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlow.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlow.java @@ -10,6 +10,7 @@ import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.DelegateExecution; import org.jeecg.modules.supervision.common.OrgConfig; import org.jeecg.modules.supervision.xispeak.config.XiSpeakFeedbackConfig; +import org.jeecg.modules.supervision.common.flow.FlowVariableSupport; import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; @@ -198,10 +199,7 @@ public class XiSpeakFeedbackFlow { } // 将 "张三, 李四 " 转换为 ["张三", "李四"] - return Arrays.stream(implDeptWorker.split(",")) - .map(String::trim) // 去掉前后空格 - .filter(StringUtils::isNotBlank) // 过滤掉空字符串(防止出现 "张三,,李四") - .collect(Collectors.toList()); + return FlowVariableSupport.splitToTrimmedList(implDeptWorker); } /** diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlow.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlow.java index 7d21dbc..7230db8 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlow.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlow.java @@ -15,6 +15,7 @@ import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +import org.jeecg.modules.supervision.common.flow.FlowVariableSupport; import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -125,16 +126,7 @@ public class XiSpeakFlow { String rawValue = data.getString(codeName); // 4. 转换逗号分隔的字符串为 List - List resultList; - if (org.apache.commons.lang.StringUtils.isBlank(rawValue)) { - resultList = Collections.emptyList(); - } else { - // 使用逗号分割,并过滤掉每个元素前后的空格 - resultList = Arrays.stream(rawValue.split(",")) - .map(String::trim) - .filter(org.apache.commons.lang.StringUtils::isNotBlank) // 过滤掉因连续逗号产生的空元素 - .collect(Collectors.toList()); - } + List resultList = FlowVariableSupport.splitToTrimmedList(rawValue); log.info("{} 业务解析结果: {}", logLabel, resultList); return resultList; @@ -285,7 +277,7 @@ public class XiSpeakFlow { if (org.apache.commons.lang.StringUtils.isBlank(businessKey) || rawDeptVar == null) { return null; } - String deptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); + String deptId = FlowVariableSupport.cleanDeptId(rawDeptVar); BgXiSpeak xiSpeak = bgXiSpeakService.getById(businessKey); if (xiSpeak == null || xiSpeak.getApproveInfo() == null) { return null; @@ -340,13 +332,7 @@ public class XiSpeakFlow { } public int getListSize(String jsonData){ - if (org.apache.commons.lang3.StringUtils.isEmpty(jsonData)) { - return 0; - } - return Math.toIntExact(Arrays.stream(jsonData.split(",")) - .map(String::trim) - .filter(org.apache.commons.lang3.StringUtils::isNotEmpty) - .count()); + return FlowVariableSupport.countTokens(jsonData); } // List getImplDeptWorker(String JG_LOCAL_PROCESS_ID,String deptId){ @@ -421,10 +407,7 @@ public class XiSpeakFlow { value = execution.getVariable(variableName); } if (value != null) { - return Arrays.stream(String.valueOf(value).split(SymbolConstant.COMMA)) - .map(String::trim) - .filter(org.apache.commons.lang.StringUtils::isNotBlank) - .collect(Collectors.toList()); + return FlowVariableSupport.splitToTrimmedList(String.valueOf(value)); } } catch (Exception e) { log.warn("getDeptWorkerList from process variable failed", e); diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListener.java index a0971aa..790cbe9 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListener.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListener.java @@ -19,6 +19,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -75,9 +76,8 @@ public class AfterBgLeaderApproveListener implements TaskListener { // 4. 获取第二层父级 ID (大会签容器/外层作用域) String grandParentId = parentExecution.getParentId(); - // 5. 确定最终存储目标 - // 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层 - String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId; + // 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层) + String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId); try { // 6. 执行存储 diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListener.java index 5636dc1..1499427 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListener.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListener.java @@ -20,6 +20,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -35,11 +36,7 @@ public class AfterImplDeptLeaderApproveListener implements TaskListener { private static final long serialVersionUID = 1L; private static final String VARIABLE_NAME = "tem_impl_leader"; - private static RuntimeService runtimeService; - - static { - runtimeService = SpringContextUtils.getBean(RuntimeService.class); - } + private final RuntimeService runtimeService; @Override public void notify(DelegateTask delegateTask) { @@ -75,9 +72,8 @@ public class AfterImplDeptLeaderApproveListener implements TaskListener { // 4. 获取第二层父级 ID (大会签容器/外层作用域) String grandParentId = parentExecution.getParentId(); - // 5. 确定最终存储目标 - // 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层 - String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId; + // 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层) + String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId); try { // 6. 执行存储 diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListener.java index 07a6ee7..1ef5dd5 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListener.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListener.java @@ -20,6 +20,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.common.flow.FlowVariableSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -74,7 +75,7 @@ public class AfterImplWorkerApproveListener implements TaskListener { return; } - String cleanDeptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); + String cleanDeptId = FlowVariableSupport.cleanDeptId(rawDeptVar); DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo(); if (approveMap == null) { diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListener.java index ef91672..618f627 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListener.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListener.java @@ -20,6 +20,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -35,11 +36,7 @@ public class AfterImplWorkerTemStoreListener implements TaskListener { private static final long serialVersionUID = 1L; private static final String VARIABLE_NAME = "tem_impl_worker"; - private static RuntimeService runtimeService; - - static { - runtimeService = SpringContextUtils.getBean(RuntimeService.class); - } + private final RuntimeService runtimeService; @Override public void notify(DelegateTask delegateTask) { @@ -75,9 +72,8 @@ public class AfterImplWorkerTemStoreListener implements TaskListener { // 4. 获取第二层父级 ID (大会签容器/外层作用域) String grandParentId = parentExecution.getParentId(); - // 5. 确定最终存储目标 - // 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层 - String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId; + // 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层) + String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId); try { // 6. 执行存储 diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterSDWApproveHqListener.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterSDWApproveHqListener.java index 4d9d0ca..1870664 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterSDWApproveHqListener.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/supervision/xispeak/listener/AfterSDWApproveHqListener.java @@ -9,6 +9,7 @@ import org.flowable.engine.delegate.ExecutionListener; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.common.flow.FlowVariableSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -65,10 +66,7 @@ public class AfterSDWApproveHqListener implements ExecutionListener { // 3. 只有当字段存在且内容不为空时,才进行解析 if (oConvertUtils.isNotEmpty(jsonStr)) { - List deptIds = Arrays.stream(jsonStr.split(",")) - .map(String::trim) // 去掉可能存在的空格 - .filter(s -> !s.isEmpty()) // 过滤掉空字符串 - .collect(Collectors.toList()); + List deptIds = FlowVariableSupport.splitToTrimmedList(jsonStr); if (deptIds != null && !deptIds.isEmpty()) { // 4. 将提取出的 List 存入子流程变量(供多实例 Collection 使用) diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterFeedbackServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterFeedbackServiceImplTest.java new file mode 100644 index 0000000..7871fb9 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterFeedbackServiceImplTest.java @@ -0,0 +1,44 @@ +package org.jeecg.modules.supervision.bgpartymatter.service.impl; + +import org.jeecg.modules.supervision.bgpartymatter.entity.BgPartymatterFeedback; +import org.jeecg.modules.supervision.bgpartymatter.mapper.BgPartymatterFeedbackMapper; +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.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * BgPartymatterFeedbackServiceImpl 测试(简单转发)。 + */ +@ExtendWith(MockitoExtension.class) +class BgPartymatterFeedbackServiceImplTest { + + @Mock private BgPartymatterFeedbackMapper bgPartymatterFeedbackMapper; + + private BgPartymatterFeedbackServiceImpl service; + + @BeforeEach + void setUp() { + service = new BgPartymatterFeedbackServiceImpl(); + ReflectionTestUtils.setField(service, "bgPartymatterFeedbackMapper", bgPartymatterFeedbackMapper); + } + + @Test + void should_select_by_main_id() { + BgPartymatterFeedback feedback = new BgPartymatterFeedback(); + when(bgPartymatterFeedbackMapper.selectByMainId("m-1")).thenReturn(List.of(feedback)); + + List result = service.selectByMainId("m-1"); + + assertEquals(1, result.size()); + verify(bgPartymatterFeedbackMapper).selectByMainId("m-1"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterServiceImplTest.java new file mode 100644 index 0000000..a39b328 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bgpartymatter/service/impl/BgPartymatterServiceImplTest.java @@ -0,0 +1,62 @@ +package org.jeecg.modules.supervision.bgpartymatter.service.impl; + +import org.flowable.engine.RuntimeService; +import org.jeecg.modules.supervision.bgpartymatter.entity.BgPartymatter; +import org.jeecg.modules.supervision.bgpartymatter.mapper.BgPartymatterFeedbackMapper; +import org.jeecg.modules.supervision.bgpartymatter.mapper.BgPartymatterMapper; +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.mockito.Mockito.verify; + +/** + * BgPartymatterServiceImpl 测试(主从删除 + 流程变量同步)。 + */ +@ExtendWith(MockitoExtension.class) +class BgPartymatterServiceImplTest { + + @Mock private BgPartymatterMapper bgPartymatterMapper; + @Mock private BgPartymatterFeedbackMapper bgPartymatterFeedbackMapper; + @Mock private RuntimeService runtimeService; + + private BgPartymatterServiceImpl service; + + @BeforeEach + void setUp() { + service = new BgPartymatterServiceImpl(); + ReflectionTestUtils.setField(service, "bgPartymatterMapper", bgPartymatterMapper); + ReflectionTestUtils.setField(service, "baseMapper", bgPartymatterMapper); + ReflectionTestUtils.setField(service, "bgPartymatterFeedbackMapper", bgPartymatterFeedbackMapper); + ReflectionTestUtils.setField(service, "runtimeService", runtimeService); + } + + @Test + void should_delete_main_with_feedback() { + service.delMain("m-1"); + verify(bgPartymatterFeedbackMapper).deleteByMainId("m-1"); + verify(bgPartymatterMapper).deleteById("m-1"); + } + + @Test + void should_delete_batch_with_feedback() { + service.delBatchMain(List.of("m-1", "m-2")); + verify(bgPartymatterFeedbackMapper).deleteByMainId("m-1"); + verify(bgPartymatterMapper).deleteById("m-1"); + verify(bgPartymatterFeedbackMapper).deleteByMainId("m-2"); + verify(bgPartymatterMapper).deleteById("m-2"); + } + + @Test + void should_update_form_and_sync_variable() { + BgPartymatter form = new BgPartymatter(); + service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}"); + verify(bgPartymatterMapper).updateById(form); + verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseFeedbackServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseFeedbackServiceImplTest.java new file mode 100644 index 0000000..afb6f8c --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseFeedbackServiceImplTest.java @@ -0,0 +1,41 @@ +package org.jeecg.modules.supervision.bqtakepulse.service.impl; + +import org.jeecg.modules.supervision.bqtakepulse.entity.BgTakepulseFeedback; +import org.jeecg.modules.supervision.bqtakepulse.mapper.BgTakepulseFeedbackMapper; +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.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * BgTakepulseFeedbackServiceImpl 测试(简单转发)。 + */ +@ExtendWith(MockitoExtension.class) +class BgTakepulseFeedbackServiceImplTest { + + @Mock private BgTakepulseFeedbackMapper bgTakepulseFeedbackMapper; + + private BgTakepulseFeedbackServiceImpl service; + + @BeforeEach + void setUp() { + service = new BgTakepulseFeedbackServiceImpl(); + ReflectionTestUtils.setField(service, "bgTakepulseFeedbackMapper", bgTakepulseFeedbackMapper); + } + + @Test + void should_select_by_main_id() { + BgTakepulseFeedback feedback = new BgTakepulseFeedback(); + when(bgTakepulseFeedbackMapper.selectByMainId("m-1")).thenReturn(List.of(feedback)); + assertEquals(1, service.selectByMainId("m-1").size()); + verify(bgTakepulseFeedbackMapper).selectByMainId("m-1"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseServiceImplTest.java new file mode 100644 index 0000000..ac719a0 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/bqtakepulse/service/impl/BgTakepulseServiceImplTest.java @@ -0,0 +1,103 @@ +package org.jeecg.modules.supervision.bqtakepulse.service.impl; + +import org.flowable.engine.RuntimeService; +import org.jeecg.modules.supervision.bqtakepulse.entity.BgTakepulse; +import org.jeecg.modules.supervision.bqtakepulse.entity.BgTakepulseFeedback; +import org.jeecg.modules.supervision.bqtakepulse.mapper.BgTakepulseFeedbackMapper; +import org.jeecg.modules.supervision.bqtakepulse.mapper.BgTakepulseMapper; +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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * BgTakepulseServiceImpl 测试。 + * 核心:saveMain 默认 del_flag 补齐(防 @TableLogic 过滤)+ 主从 CRUD。 + */ +@ExtendWith(MockitoExtension.class) +class BgTakepulseServiceImplTest { + + @Mock private BgTakepulseMapper bgTakepulseMapper; + @Mock private BgTakepulseFeedbackMapper bgTakepulseFeedbackMapper; + @Mock private RuntimeService runtimeService; + + private BgTakepulseServiceImpl service; + + @BeforeEach + void setUp() { + service = new BgTakepulseServiceImpl(); + ReflectionTestUtils.setField(service, "bgTakepulseMapper", bgTakepulseMapper); + ReflectionTestUtils.setField(service, "baseMapper", bgTakepulseMapper); + ReflectionTestUtils.setField(service, "bgTakepulseFeedbackMapper", bgTakepulseFeedbackMapper); + ReflectionTestUtils.setField(service, "runtimeService", runtimeService); + } + + @Test + void should_fill_default_del_flag_on_save() { + BgTakepulse main = new BgTakepulse(); + main.setId("biz-1"); + BgTakepulseFeedback sub = new BgTakepulseFeedback(); + + service.saveMain(main, List.of(sub)); + + assertEquals("0", main.getDelFlag()); + assertEquals("0", sub.getDelFlag()); + assertEquals("biz-1", sub.getMainId()); + verify(bgTakepulseMapper).insert(main); + verify(bgTakepulseFeedbackMapper).insert(sub); + } + + @Test + void should_keep_existing_del_flag() { + BgTakepulse main = new BgTakepulse(); + main.setDelFlag("1"); + service.saveMain(main, null); + assertEquals("1", main.getDelFlag()); + verify(bgTakepulseMapper).insert(main); + } + + @Test + void should_update_main_and_replace_sub() { + BgTakepulse main = new BgTakepulse(); + main.setId("biz-1"); + BgTakepulseFeedback sub = new BgTakepulseFeedback(); + + service.updateMain(main, List.of(sub)); + + verify(bgTakepulseMapper).updateById(main); + verify(bgTakepulseFeedbackMapper).deleteByMainId("biz-1"); + assertEquals("biz-1", sub.getMainId()); + verify(bgTakepulseFeedbackMapper).insert(sub); + } + + @Test + void should_delete_main_with_sub() { + service.delMain("biz-1"); + verify(bgTakepulseFeedbackMapper).deleteByMainId("biz-1"); + verify(bgTakepulseMapper).deleteById("biz-1"); + } + + @Test + void should_delete_batch() { + service.delBatchMain(List.of("biz-1", "biz-2")); + verify(bgTakepulseFeedbackMapper).deleteByMainId("biz-2"); + verify(bgTakepulseMapper).deleteById("biz-2"); + } + + @Test + void should_update_form_and_sync_variable() { + BgTakepulse form = new BgTakepulse(); + service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}"); + verify(bgTakepulseMapper).updateById(form); + verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/common/BpmStatusTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/common/BpmStatusTest.java new file mode 100644 index 0000000..886ed8b --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/common/BpmStatusTest.java @@ -0,0 +1,32 @@ +package org.jeecg.modules.supervision.common; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * BpmStatus 枚举解析测试。 + */ +class BpmStatusTest { + + @Test + void should_parse_known_codes() { + assertEquals(BpmStatus.NOT_START, BpmStatus.fromCode("1")); + assertEquals(BpmStatus.SUPERVISING, BpmStatus.fromCode("2")); + assertEquals(BpmStatus.FINISH, BpmStatus.fromCode("3")); + } + + @ParameterizedTest + @ValueSource(strings = {"0", "4", "abc", ""}) + void should_return_null_for_unknown_code(String code) { + assertNull(BpmStatus.fromCode(code)); + } + + @Test + void should_return_null_for_null_code() { + assertNull(BpmStatus.fromCode(null)); + } +} 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/common/flow/FlowExecutionSupportTest.java similarity index 69% rename from jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/listener/FixContactApprovalHelperTest.java rename to jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/common/flow/FlowExecutionSupportTest.java index 0a2ef01..b76cc71 100644 --- 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/common/flow/FlowExecutionSupportTest.java @@ -1,4 +1,4 @@ -package org.jeecg.modules.supervision.fixcontact.listener; +package org.jeecg.modules.supervision.common.flow; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -9,29 +9,29 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; /** - * FixContactApprovalHelper.resolveTargetExecutionId 规则测试(TDD 红-绿-重构 演示)。 + * FlowExecutionSupport.resolveTargetExecutionId 规则测试(TDD 红-绿-重构 演示)。 * - * 业务规则(来自 FixContactDeptLeaderApproveListener 第 53-69 行): - * 部门领导审批后,领导变量要写入"上两级"执行实例: + * 业务规则(来自 fixcontact/xispeak 监听器内联重复逻辑): + * 部门领导/经办人审批后,审批人变量要写入"上两级"执行实例: * - 存在第二层父级(大会签容器/外层作用域)→ 写入第二层 grandParentId * - 不存在第二层父级 → 写入第一层 parentId * - 第一层 parentId 为空 → 无目标,返回 null(调用方跳过写入) */ -class FixContactApprovalHelperTest { +class FlowExecutionSupportTest { // ============ 正常路径 ============ @Test void should_return_grandParent_when_both_levels_exist() { // 两级父级都存在(子流程内会签容器场景) - String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", "grand-1"); + String target = FlowExecutionSupport.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); + String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", null); assertEquals("parent-1", target); } @@ -42,7 +42,7 @@ class FixContactApprovalHelperTest { @ValueSource(strings = {" ", " "}) void should_return_parent_when_grand_parent_blank(String grandParentId) { // 第二层为空串/空白串:视为不存在,退回第一层(原逻辑 isNotBlank 判定) - String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", grandParentId); + String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", grandParentId); assertEquals("parent-1", target); } @@ -53,6 +53,6 @@ class FixContactApprovalHelperTest { @ValueSource(strings = {" ", " "}) void should_return_null_when_parent_blank(String parentId) { // 第一层父级都没有:无目标执行实例,返回 null 供调用方跳过 - assertNull(FixContactApprovalHelper.resolveTargetExecutionId(parentId, "grand-1")); + assertNull(FlowExecutionSupport.resolveTargetExecutionId(parentId, "grand-1")); } } 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/common/flow/FlowVariableSupportTest.java similarity index 60% rename from jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/flow/FixContactFlowSupportTest.java rename to jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/common/flow/FlowVariableSupportTest.java index 8dd6426..e414e51 100644 --- 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/common/flow/FlowVariableSupportTest.java @@ -1,4 +1,4 @@ -package org.jeecg.modules.supervision.fixcontact.flow; +package org.jeecg.modules.supervision.common.flow; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -11,31 +11,31 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; /** - * FixContactFlowSupport 纯函数规则测试(TDD 红-绿-重构,阶段 A 示范)。 + * FlowVariableSupport 纯函数规则测试(TDD 红-绿-重构,阶段 A 示范)。 * - * 业务规则(来自 FixContactFlow 内联逻辑,3 处重复的逗号分割 + 部门 ID 清洗): + * 业务规则(来自 fixcontact/xispeak 多处重复的逗号分割 + 部门 ID 清洗): * - splitToTrimmedList:逗号分割 → 去首尾空白 → 过滤空白元素(null/空 → 空列表) * - countTokens:分割后元素个数(等价于 splitToTrimmedList().size()) - * - cleanDeptId:执行实例部门变量清洗(去掉 [] 与首尾空白,与 XiSpeak 监听器同款规则) + * - cleanDeptId:执行实例部门变量清洗(去掉 [] 与首尾空白) */ -class FixContactFlowSupportTest { +class FlowVariableSupportTest { // ============ splitToTrimmedList:正常路径 ============ @Test void should_split_multi_tokens() { - assertEquals(List.of("a", "b", "c"), FixContactFlowSupport.splitToTrimmedList("a,b,c")); + assertEquals(List.of("a", "b", "c"), FlowVariableSupport.splitToTrimmedList("a,b,c")); } @Test void should_trim_tokens_with_spaces() { // 逗号前后有空格:逐 token trim - assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList(" a , b ")); + assertEquals(List.of("a", "b"), FlowVariableSupport.splitToTrimmedList(" a , b ")); } @Test void should_return_single_token() { - assertEquals(List.of("a"), FixContactFlowSupport.splitToTrimmedList("a")); + assertEquals(List.of("a"), FlowVariableSupport.splitToTrimmedList("a")); } // ============ splitToTrimmedList:边界路径 ============ @@ -43,61 +43,61 @@ class FixContactFlowSupportTest { @Test void should_filter_empty_tokens() { // 空元素(连续逗号/全空白元素)被过滤 - assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList("a,,b")); + assertEquals(List.of("a", "b"), FlowVariableSupport.splitToTrimmedList("a,,b")); } @ParameterizedTest @NullAndEmptySource @ValueSource(strings = {" ", " ", " , , "}) void should_return_empty_list_when_no_valid_token(String raw) { - assertEquals(List.of(), FixContactFlowSupport.splitToTrimmedList(raw)); + assertEquals(List.of(), FlowVariableSupport.splitToTrimmedList(raw)); } // ============ countTokens:正常 + 边界 ============ @Test void should_count_tokens() { - assertEquals(3, FixContactFlowSupport.countTokens("a,b,c")); + assertEquals(3, FlowVariableSupport.countTokens("a,b,c")); } @Test void should_count_trimmed_tokens() { - assertEquals(2, FixContactFlowSupport.countTokens(" a , b ")); + assertEquals(2, FlowVariableSupport.countTokens(" a , b ")); } @ParameterizedTest @NullAndEmptySource @ValueSource(strings = {" ", " , , "}) void should_return_zero_when_no_valid_token(String raw) { - assertEquals(0, FixContactFlowSupport.countTokens(raw)); + assertEquals(0, FlowVariableSupport.countTokens(raw)); } // ============ cleanDeptId:正常 + 边界 ============ @Test void should_remove_square_brackets() { - assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("[dept-1]")); + assertEquals("dept-1", FlowVariableSupport.cleanDeptId("[dept-1]")); } @Test void should_remove_brackets_and_trim() { // 多值列表变量场景:整体去括号 + trim - assertEquals("dept-1, dept-2", FixContactFlowSupport.cleanDeptId("[dept-1, dept-2]")); + assertEquals("dept-1, dept-2", FlowVariableSupport.cleanDeptId("[dept-1, dept-2]")); } @Test void should_keep_plain_id_unchanged() { - assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("dept-1")); + assertEquals("dept-1", FlowVariableSupport.cleanDeptId("dept-1")); } @Test void should_handle_non_string_object() { // 流程变量可能是任意对象,toString 后处理 - assertEquals("123", FixContactFlowSupport.cleanDeptId(123L)); + assertEquals("123", FlowVariableSupport.cleanDeptId(123L)); } @Test void should_return_null_when_raw_null() { - assertNull(FixContactFlowSupport.cleanDeptId(null)); + assertNull(FlowVariableSupport.cleanDeptId(null)); } } diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dashboard/service/impl/ProcessPortalServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dashboard/service/impl/ProcessPortalServiceImplTest.java new file mode 100644 index 0000000..b2497a1 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dashboard/service/impl/ProcessPortalServiceImplTest.java @@ -0,0 +1,71 @@ +package org.jeecg.modules.supervision.dashboard.service.impl; + +import org.jeecg.modules.supervision.dashboard.dto.ProcessPortalStatsDTO; +import org.jeecg.modules.supervision.dashboard.mapper.ProcessPortalMapper; +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * ProcessPortalServiceImpl 测试。 + * 核心:formatDuration 时长格式化规则(0/分钟/小时/天),经 getStats 间接覆盖。 + */ +@ExtendWith(MockitoExtension.class) +class ProcessPortalServiceImplTest { + + @Mock private ProcessPortalMapper processPortalMapper; + + private ProcessPortalServiceImpl service; + + @BeforeEach + void setUp() { + service = new ProcessPortalServiceImpl(); + ReflectionTestUtils.setField(service, "processPortalMapper", processPortalMapper); + } + + private ProcessPortalStatsDTO statsWithAvg(Long avgMillis) { + when(processPortalMapper.countTodo(eq("u1"))).thenReturn(3L); + when(processPortalMapper.countDoneThisMonth(eq("u1"), org.mockito.ArgumentMatchers.anyString())).thenReturn(10L); + when(processPortalMapper.avgDurationMillis(eq("u1"))).thenReturn(avgMillis); + when(processPortalMapper.countOverdue(eq("u1"))).thenReturn(1L); + return service.getStats("u1"); + } + + @Test + void should_return_zero_hours_when_null() { + assertEquals("0小时", statsWithAvg(null).getAvgDuration()); + } + + @Test + void should_format_minutes_when_less_than_hour() { + // 30 分钟 + assertEquals("30分钟", statsWithAvg(30L * 60 * 1000).getAvgDuration()); + } + + @Test + void should_format_hours_when_less_than_day() { + // 2.5 小时 + assertEquals("2.5小时", statsWithAvg((long) (2.5 * 3600 * 1000)).getAvgDuration()); + } + + @Test + void should_format_days_when_ge_day() { + // 48 小时 → 2 天 + assertEquals("2.0天", statsWithAvg(48L * 3600 * 1000).getAvgDuration()); + } + + @Test + void should_fill_all_stats() { + ProcessPortalStatsDTO dto = statsWithAvg(null); + assertEquals(3, dto.getTodoCount()); + assertEquals(10, dto.getDoneThisMonth()); + assertEquals(1, dto.getOverdueCount()); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspectproblem/service/impl/DqInspectProblemServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspectproblem/service/impl/DqInspectProblemServiceImplTest.java new file mode 100644 index 0000000..857a6b2 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspectproblem/service/impl/DqInspectProblemServiceImplTest.java @@ -0,0 +1,210 @@ +package org.jeecg.modules.supervision.dqinspectproblem.service.impl; + +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.modules.supervision.dqinspectproblem.entity.DqInspectProblem; +import org.jeecg.modules.supervision.dqinspectproblem.mapper.DqInspectProblemMapper; +import org.jeecg.modules.supervision.dqinspectproblem.service.IDqInspectProblemService; +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 org.springframework.test.util.ReflectionTestUtils; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * DqInspectProblemServiceImpl 测试。 + * 核心:validateAndNormalizeNode 三层问题清单校验(分类→面上→具体),经 add/update 触发。 + */ +@ExtendWith(MockitoExtension.class) +class DqInspectProblemServiceImplTest { + + @Mock private DqInspectProblemMapper baseMapper; + + private DqInspectProblemServiceImpl service; + + @BeforeEach + void setUp() { + service = new DqInspectProblemServiceImpl(); + ReflectionTestUtils.setField(service, "baseMapper", baseMapper); + } + + private DqInspectProblem problem(String nodeType, String pid) { + DqInspectProblem p = new DqInspectProblem(); + p.setNodeType(nodeType); + p.setPid(pid); + return p; + } + + private DqInspectProblem category(String id) { + DqInspectProblem c = new DqInspectProblem(); + c.setId(id); + c.setNodeType(IDqInspectProblemService.NODE_TYPE_CATEGORY); + c.setPid(IDqInspectProblemService.ROOT_PID_VALUE); + return c; + } + + private DqInspectProblem surface(String id) { + DqInspectProblem s = new DqInspectProblem(); + s.setId(id); + s.setNodeType(IDqInspectProblemService.NODE_TYPE_SURFACE); + s.setPid("c-1"); + return s; + } + + // ==================== validateAndNormalizeNode:基础校验 ==================== + + @Nested + class BasicValidation { + + @Test + void should_throw_when_form_null() { + assertThrows(JeecgBootException.class, () -> service.addDqInspectProblem(null)); + } + + @Test + void should_throw_when_node_type_invalid() { + assertThrows(JeecgBootException.class, + () -> service.addDqInspectProblem(problem("invalid", "c-1"))); + } + } + + // ==================== 分类节点 ==================== + + @Nested + class CategoryNode { + + @Test + void should_force_root_pid_and_clear_specific_fields() { + DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_CATEGORY, "c-2"); + p.setInspectAdvice("advice"); + p.setQuestionResLeader("leader"); + p.setQuestionResDept("dept"); + + service.addDqInspectProblem(p); + + assertEquals(IDqInspectProblemService.ROOT_PID_VALUE, p.getPid()); + assertNull(p.getInspectAdvice()); + assertNull(p.getQuestionResLeader()); + assertNull(p.getQuestionResDept()); + verify(baseMapper).insert(p); + } + } + + // ==================== 面上/具体节点:父级校验 ==================== + + @Nested + class ParentValidation { + + @Test + void should_throw_when_pid_empty() { + assertThrows(JeecgBootException.class, + () -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, ""))); + } + + @Test + void should_throw_when_pid_is_root() { + assertThrows(JeecgBootException.class, + () -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, + IDqInspectProblemService.ROOT_PID_VALUE))); + } + + @Test + void should_throw_when_parent_not_exists() { + when(baseMapper.selectById("c-9")).thenReturn(null); + assertThrows(JeecgBootException.class, + () -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, "c-9"))); + } + + @Test + void should_throw_when_parent_type_mismatch() { + // 面上问题的父必须是分类;父是面上 → 抛 + DqInspectProblem wrongParent = surface("s-1"); + when(baseMapper.selectById("s-1")).thenReturn(wrongParent); + assertThrows(JeecgBootException.class, + () -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, "s-1"))); + } + + @Test + void should_insert_surface_under_category() { + when(baseMapper.selectById("c-1")).thenReturn(category("c-1")); + service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, "c-1")); + verify(baseMapper).insert(any()); + } + + @Test + void should_insert_specific_under_surface() { + when(baseMapper.selectById("s-1")).thenReturn(surface("s-1")); + service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1")); + verify(baseMapper).insert(any()); + } + } + + // ==================== updateDqInspectProblem:自身/下级校验 ==================== + + @Nested + class UpdateValidation { + + @Test + void should_throw_when_entity_not_found() { + DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1"); + p.setId("x-1"); + when(baseMapper.selectById("x-1")).thenReturn(null); + assertThrows(JeecgBootException.class, () -> service.updateDqInspectProblem(p)); + } + + @Test + void should_throw_when_pid_is_self() { + DqInspectProblem existing = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1"); + existing.setId("x-1"); + when(baseMapper.selectById("x-1")).thenReturn(existing); + + DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "x-1"); + p.setId("x-1"); + assertThrows(JeecgBootException.class, () -> service.updateDqInspectProblem(p)); + } + + @Test + void should_throw_when_pid_is_descendant() { + // x-1 的下级是 d-2:把 x-1 的父设为 d-2 非法 + DqInspectProblem existing = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1"); + existing.setId("x-1"); + when(baseMapper.selectById("x-1")).thenReturn(existing); + DqInspectProblem child = new DqInspectProblem(); + child.setId("d-2"); + child.setPid("x-1"); + when(baseMapper.selectById("d-2")).thenReturn(child); + when(baseMapper.selectById("x-1")).thenReturn(existing); // isDescendantOf 起点 + + DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "d-2"); + p.setId("x-1"); + assertThrows(JeecgBootException.class, () -> service.updateDqInspectProblem(p)); + } + + @Test + void should_update_normally_when_pid_unchanged() { + DqInspectProblem existing = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1"); + existing.setId("x-1"); + existing.setHasChild(IDqInspectProblemService.NOCHILD); + when(baseMapper.selectById("x-1")).thenReturn(existing); + when(baseMapper.selectById("s-1")).thenReturn(surface("s-1")); + + DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1"); + p.setId("x-1"); + service.updateDqInspectProblem(p); + + // hasChild 由树结构维护,忽略前端提交值 + assertEquals(IDqInspectProblemService.NOCHILD, p.getHasChild()); + verify(baseMapper, never()).updateTreeNodeStatus(any(), any()); + verify(baseMapper).updateById(p); + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/listener/DqInspectTaskStageIncrementListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/listener/DqInspectTaskStageIncrementListenerTest.java new file mode 100644 index 0000000..896dc31 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/listener/DqInspectTaskStageIncrementListenerTest.java @@ -0,0 +1,103 @@ +package org.jeecg.modules.supervision.dqinspecttask.listener; + +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.DelegateExecution; +import org.jeecg.modules.supervision.dqinspecttask.config.InspectImproveConfig; +import org.jeecg.modules.supervision.dqinspecttask.config.InspectImproveConfig.InspectImproveProperties; +import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectTask; +import org.jeecg.modules.supervision.dqinspecttask.service.IDqInspectTaskService; +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.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * DqInspectTaskStageIncrementListener 阶段递增监听器测试。 + * 规则:流程结束时 completedStage 原子递增 1;businessKey 缺失跳过;幂等标记 stage_increment_done 防重复递增。 + */ +@ExtendWith(MockitoExtension.class) +class DqInspectTaskStageIncrementListenerTest { + + private static final String INCREMENT_FLAG = "stage_increment_done"; + private static final String BUSINESS_KEY_VAR = "businessKey"; + + @Mock private InspectImproveConfig inspectImproveConfig; + @Mock private IDqInspectTaskService dqInspectTaskService; + @Mock private RuntimeService runtimeService; + @Mock private DelegateExecution execution; + @Mock private LambdaUpdateChainWrapper chainWrapper; + + private DqInspectTaskStageIncrementListener listener; + + @BeforeEach + void setUp() { + InspectImproveProperties props = new InspectImproveProperties(); + props.setBusinessKey(BUSINESS_KEY_VAR); + when(inspectImproveConfig.getInspectImprove()).thenReturn(props); + listener = new DqInspectTaskStageIncrementListener(inspectImproveConfig, dqInspectTaskService, runtimeService); + } + + @Test + void should_skip_when_business_key_missing() { + when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn(null); + + listener.notify(execution); + + verify(runtimeService, never()).setVariable(any(), any(), any()); + verify(dqInspectTaskService, never()).lambdaUpdate(); + } + + @Test + void should_skip_when_already_incremented() { + // 幂等:同一流程实例结束仅递增一次 + when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn("biz-1"); + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", INCREMENT_FLAG)).thenReturn(Boolean.TRUE); + + listener.notify(execution); + + verify(dqInspectTaskService, never()).lambdaUpdate(); + } + + @Test + void should_increment_stage_and_set_flag() { + when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn("biz-1"); + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", INCREMENT_FLAG)).thenReturn(null); + when(dqInspectTaskService.lambdaUpdate()).thenReturn(chainWrapper); + when(chainWrapper.setSql("completed_stage = completed_stage + 1")).thenReturn(chainWrapper); + // 方法引用 DqInspectTask::getId 在测试类与生产类编译为不同 Lambda 实例,用 any() 匹配函数式参数 + when(chainWrapper.eq(any(), eq("biz-1"))).thenReturn(chainWrapper); + when(chainWrapper.update()).thenReturn(true); + + listener.notify(execution); + + verify(runtimeService).setVariable("proc-1", INCREMENT_FLAG, Boolean.TRUE); + verify(chainWrapper).setSql("completed_stage = completed_stage + 1"); + verify(chainWrapper).eq(any(), eq("biz-1")); + verify(chainWrapper).update(); + } + + @Test + void should_not_throw_when_update_affects_zero_rows() { + // 记录可能已删除:update 返回 false 仅 warn,不抛异常 + when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn("biz-1"); + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(runtimeService.getVariable("proc-1", INCREMENT_FLAG)).thenReturn(null); + when(dqInspectTaskService.lambdaUpdate()).thenReturn(chainWrapper); + when(chainWrapper.setSql("completed_stage = completed_stage + 1")).thenReturn(chainWrapper); + when(chainWrapper.eq(any(), eq("biz-1"))).thenReturn(chainWrapper); + when(chainWrapper.update()).thenReturn(false); + + // 不抛异常即通过 + listener.notify(execution); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectProgressServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectProgressServiceImplTest.java new file mode 100644 index 0000000..ad378a7 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectProgressServiceImplTest.java @@ -0,0 +1,44 @@ +package org.jeecg.modules.supervision.dqinspecttask.service.impl; + +import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectProgress; +import org.jeecg.modules.supervision.dqinspecttask.mapper.DqInspectProgressMapper; +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.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * DqInspectProgressServiceImpl 测试(简单转发)。 + */ +@ExtendWith(MockitoExtension.class) +class DqInspectProgressServiceImplTest { + + @Mock private DqInspectProgressMapper dqInspectProgressMapper; + + private DqInspectProgressServiceImpl service; + + @BeforeEach + void setUp() { + service = new DqInspectProgressServiceImpl(); + ReflectionTestUtils.setField(service, "dqInspectProgressMapper", dqInspectProgressMapper); + } + + @Test + void should_select_by_main_id() { + DqInspectProgress progress = new DqInspectProgress(); + when(dqInspectProgressMapper.selectByMainId("main-1")).thenReturn(List.of(progress)); + + List result = service.selectByMainId("main-1"); + + assertEquals(1, result.size()); + verify(dqInspectProgressMapper).selectByMainId("main-1"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectTaskServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectTaskServiceImplTest.java new file mode 100644 index 0000000..9008e8f --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/service/impl/DqInspectTaskServiceImplTest.java @@ -0,0 +1,185 @@ +package org.jeecg.modules.supervision.dqinspecttask.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.modules.supervision.dqinspectproblem.entity.DqInspectProblem; +import org.jeecg.modules.supervision.dqinspectproblem.service.IDqInspectProblemService; +import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectProgress; +import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectTask; +import org.jeecg.modules.supervision.dqinspecttask.mapper.DqInspectProgressMapper; +import org.jeecg.modules.supervision.dqinspecttask.mapper.DqInspectTaskMapper; +import org.jeecg.modules.supervision.dqinspecttask.vo.DqInspectTaskLedgerVo; +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.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * DqInspectTaskServiceImpl 测试。 + * 核心:validateProblemId 业务规则(整改任务必须挂具体问题节点下)+ saveMain 默认值 + 主从 CRUD。 + */ +@ExtendWith(MockitoExtension.class) +class DqInspectTaskServiceImplTest { + + @Mock private DqInspectTaskMapper dqInspectTaskMapper; + @Mock private DqInspectProgressMapper dqInspectProgressMapper; + @Mock private IDqInspectProblemService dqInspectProblemService; + + private DqInspectTaskServiceImpl service; + + @BeforeEach + void setUp() { + service = new DqInspectTaskServiceImpl(); + ReflectionTestUtils.setField(service, "dqInspectTaskMapper", dqInspectTaskMapper); + ReflectionTestUtils.setField(service, "dqInspectProgressMapper", dqInspectProgressMapper); + ReflectionTestUtils.setField(service, "dqInspectProblemService", dqInspectProblemService); + } + + private DqInspectProblem specificProblem() { + DqInspectProblem p = new DqInspectProblem(); + p.setId("prob-1"); + p.setNodeType(IDqInspectProblemService.NODE_TYPE_SPECIFIC); + return p; + } + + // ==================== validateProblemId 业务规则 ==================== + + @Test + void should_throw_when_problem_id_blank() { + DqInspectTask task = new DqInspectTask(); + assertThrows(JeecgBootException.class, () -> service.saveMain(task, null)); + } + + @Test + void should_throw_when_problem_not_exists() { + DqInspectTask task = new DqInspectTask(); + task.setProblemId("no-such"); + when(dqInspectProblemService.getById("no-such")).thenReturn(null); + assertThrows(JeecgBootException.class, () -> service.saveMain(task, null)); + } + + @Test + void should_throw_when_problem_not_specific_node() { + DqInspectTask task = new DqInspectTask(); + task.setProblemId("prob-1"); + DqInspectProblem category = new DqInspectProblem(); + category.setNodeType("category"); + when(dqInspectProblemService.getById("prob-1")).thenReturn(category); + assertThrows(JeecgBootException.class, () -> service.saveMain(task, null)); + } + + // ==================== saveMain ==================== + + @Test + void should_set_default_bpm_status_and_stage() { + DqInspectTask task = new DqInspectTask(); + task.setProblemId("prob-1"); + when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem()); + + service.saveMain(task, null); + + assertEquals("1", task.getBpmStatus()); + assertEquals(0, task.getCompletedStage()); + verify(dqInspectTaskMapper).insert(task); + } + + @Test + void should_save_sub_progress_with_main_id() { + DqInspectTask task = new DqInspectTask(); + task.setId("task-1"); + task.setProblemId("prob-1"); + when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem()); + DqInspectProgress progress = new DqInspectProgress(); + + service.saveMain(task, List.of(progress)); + + assertEquals("task-1", progress.getMainId()); + verify(dqInspectProgressMapper).insert(progress); + } + + @Test + void should_not_save_sub_when_list_null() { + DqInspectTask task = new DqInspectTask(); + task.setProblemId("prob-1"); + when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem()); + + service.saveMain(task, null); + + verify(dqInspectProgressMapper, never()).insert(any()); + } + + // ==================== updateMain ==================== + + @Test + void should_not_touch_sub_when_list_null() { + // 关键行为:子表 null 表示"不修改子表",不得删除子表数据 + DqInspectTask task = new DqInspectTask(); + task.setId("task-1"); + task.setProblemId("prob-1"); + when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem()); + + service.updateMain(task, null); + + verify(dqInspectTaskMapper).updateById(task); + verify(dqInspectProgressMapper, never()).deleteByMainId(any()); + } + + @Test + void should_replace_sub_when_list_provided() { + DqInspectTask task = new DqInspectTask(); + task.setId("task-1"); + task.setProblemId("prob-1"); + when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem()); + DqInspectProgress progress = new DqInspectProgress(); + + service.updateMain(task, List.of(progress)); + + verify(dqInspectProgressMapper).deleteByMainId("task-1"); + assertEquals("task-1", progress.getMainId()); + verify(dqInspectProgressMapper).insert(progress); + } + + // ==================== 删除 ==================== + + @Test + void should_delete_main_with_sub() { + service.delMain("task-1"); + verify(dqInspectProgressMapper).deleteByMainId("task-1"); + verify(dqInspectTaskMapper).deleteById("task-1"); + } + + @Test + void should_delete_batch_with_sub() { + service.delBatchMain(List.of("task-1", "task-2")); + verify(dqInspectProgressMapper).deleteByMainId("task-1"); + verify(dqInspectTaskMapper).deleteById("task-1"); + verify(dqInspectProgressMapper).deleteByMainId("task-2"); + verify(dqInspectTaskMapper).deleteById("task-2"); + } + + // ==================== 台账查询 ==================== + + @Test + void should_query_ledger_page() { + Page page = new Page<>(1, 10); + DqInspectTask query = new DqInspectTask(); + when(dqInspectTaskMapper.queryLedgerPage(page, query)).thenReturn(page); + + IPage result = service.queryLedgerPage(page, query); + + assertEquals(page, result); + verify(dqInspectTaskMapper).queryLedgerPage(page, query); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContactFeedback20260730ServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContactFeedback20260730ServiceImplTest.java new file mode 100644 index 0000000..ba17d1d --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/fixcontact/service/impl/FixedContactFeedback20260730ServiceImplTest.java @@ -0,0 +1,42 @@ +package org.jeecg.modules.supervision.fixcontact.service.impl; + +import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730; +import org.jeecg.modules.supervision.fixcontact.mapper.FixedContactFeedback20260730Mapper; +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.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * FixedContactFeedback20260730ServiceImpl 测试(简单转发)。 + */ +@ExtendWith(MockitoExtension.class) +class FixedContactFeedback20260730ServiceImplTest { + + @Mock private FixedContactFeedback20260730Mapper fixedContactFeedback20260730Mapper; + + private FixedContactFeedback20260730ServiceImpl service; + + @BeforeEach + void setUp() { + service = new FixedContactFeedback20260730ServiceImpl(); + // 该类直接使用 ServiceImpl 的 baseMapper(无自有 mapper 字段) + ReflectionTestUtils.setField(service, "baseMapper", fixedContactFeedback20260730Mapper); + } + + @Test + void should_select_by_main_id() { + FixedContactFeedback20260730 feedback = new FixedContactFeedback20260730(); + when(fixedContactFeedback20260730Mapper.selectByMainId("m-1")).thenReturn(List.of(feedback)); + assertEquals(1, service.selectByMainId("m-1").size()); + verify(fixedContactFeedback20260730Mapper).selectByMainId("m-1"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementDeptScoreServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementDeptScoreServiceImplTest.java new file mode 100644 index 0000000..7c3e41b --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementDeptScoreServiceImplTest.java @@ -0,0 +1,41 @@ +package org.jeecg.modules.supervision.improvementproposal.service.impl; + +import org.jeecg.modules.supervision.improvementproposal.entity.ImprovementDeptScore; +import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementDeptScoreMapper; +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.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * ImprovementDeptScoreServiceImpl 测试(简单转发)。 + */ +@ExtendWith(MockitoExtension.class) +class ImprovementDeptScoreServiceImplTest { + + @Mock private ImprovementDeptScoreMapper improvementDeptScoreMapper; + + private ImprovementDeptScoreServiceImpl service; + + @BeforeEach + void setUp() { + service = new ImprovementDeptScoreServiceImpl(); + ReflectionTestUtils.setField(service, "improvementDeptScoreMapper", improvementDeptScoreMapper); + } + + @Test + void should_select_by_main_id() { + ImprovementDeptScore score = new ImprovementDeptScore(); + when(improvementDeptScoreMapper.selectByMainId("m-1")).thenReturn(List.of(score)); + assertEquals(1, service.selectByMainId("m-1").size()); + verify(improvementDeptScoreMapper).selectByMainId("m-1"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementInstituteVoteServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementInstituteVoteServiceImplTest.java new file mode 100644 index 0000000..1e74c43 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementInstituteVoteServiceImplTest.java @@ -0,0 +1,41 @@ +package org.jeecg.modules.supervision.improvementproposal.service.impl; + +import org.jeecg.modules.supervision.improvementproposal.entity.ImprovementInstituteVote; +import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementInstituteVoteMapper; +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.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * ImprovementInstituteVoteServiceImpl 测试(简单转发)。 + */ +@ExtendWith(MockitoExtension.class) +class ImprovementInstituteVoteServiceImplTest { + + @Mock private ImprovementInstituteVoteMapper improvementInstituteVoteMapper; + + private ImprovementInstituteVoteServiceImpl service; + + @BeforeEach + void setUp() { + service = new ImprovementInstituteVoteServiceImpl(); + ReflectionTestUtils.setField(service, "improvementInstituteVoteMapper", improvementInstituteVoteMapper); + } + + @Test + void should_select_by_main_id() { + ImprovementInstituteVote vote = new ImprovementInstituteVote(); + when(improvementInstituteVoteMapper.selectByMainId("m-1")).thenReturn(List.of(vote)); + assertEquals(1, service.selectByMainId("m-1").size()); + verify(improvementInstituteVoteMapper).selectByMainId("m-1"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImplTest.java new file mode 100644 index 0000000..3cb52d0 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalServiceImplTest.java @@ -0,0 +1,233 @@ +package org.jeecg.modules.supervision.improvementproposal.service.impl; + +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.modules.extbpm.process.service.impl.BpmBaseExtApiImpl; +import org.jeecg.modules.extbpm.process.service.IExtActBpmFileService; +import org.jeecg.modules.extbpm.process.service.IExtActBpmLogService; +import org.jeecg.modules.extbpm.process.service.IExtActFlowDataService; +import org.jeecg.modules.extbpm.process.service.IExtActTaskCcService; +import org.jeecg.modules.system.service.ISysUserDepartService; +import org.jeecg.modules.system.service.ISysRoleService; +import org.jeecg.modules.supervision.improvementproposal.dto.ImprovementProposalBpmActionDTO; +import org.jeecg.modules.supervision.improvementproposal.dto.ImprovementDeptScoreBpmDTO; +import org.jeecg.modules.supervision.improvementproposal.entity.ImprovementProposal; +import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementDeptScoreMapper; +import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementInstituteVoteMapper; +import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementProposalMapper; +import org.jeecg.modules.taskapprovalopinion.service.ITaskApprovalOpinionService; +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 org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * ImprovementProposalServiceImpl 测试(mock mapper/流程服务)。 + * 核心:流水号生成、初评奖级与 needDeptScore 联动、部门评分计算与落库。 + */ +@ExtendWith(MockitoExtension.class) +class ImprovementProposalServiceImplTest { + + @Mock private ImprovementProposalMapper improvementProposalMapper; + @Mock private ImprovementDeptScoreMapper improvementDeptScoreMapper; + @Mock private ImprovementInstituteVoteMapper improvementInstituteVoteMapper; + @Mock private BpmBaseExtApiImpl bpmBaseExtApi; + @Mock private RuntimeService runtimeService; + @Mock private HistoryService historyService; + @Mock private TaskService taskService; + @Mock private ISysRoleService sysRoleService; + @Mock private ISysUserDepartService sysUserDepartService; + @Mock private IExtActTaskCcService extActTaskCcService; + @Mock private ITaskApprovalOpinionService taskApprovalOpinionService; + @Mock private IExtActFlowDataService extActFlowDataService; + @Mock private IExtActBpmLogService extActBpmLogService; + @Mock private IExtActBpmFileService extActBpmFileService; + + private ImprovementProposalServiceImpl service; + + @BeforeEach + void setUp() { + service = new ImprovementProposalServiceImpl(); + ReflectionTestUtils.setField(service, "improvementProposalMapper", improvementProposalMapper); + ReflectionTestUtils.setField(service, "improvementDeptScoreMapper", improvementDeptScoreMapper); + ReflectionTestUtils.setField(service, "improvementInstituteVoteMapper", improvementInstituteVoteMapper); + ReflectionTestUtils.setField(service, "bpmBaseExtApi", bpmBaseExtApi); + ReflectionTestUtils.setField(service, "runtimeService", runtimeService); + ReflectionTestUtils.setField(service, "historyService", historyService); + ReflectionTestUtils.setField(service, "taskService", taskService); + ReflectionTestUtils.setField(service, "sysRoleService", sysRoleService); + ReflectionTestUtils.setField(service, "sysUserDepartService", sysUserDepartService); + ReflectionTestUtils.setField(service, "extActTaskCcService", extActTaskCcService); + ReflectionTestUtils.setField(service, "taskApprovalOpinionService", taskApprovalOpinionService); + ReflectionTestUtils.setField(service, "extActFlowDataService", extActFlowDataService); + ReflectionTestUtils.setField(service, "extActBpmLogService", extActBpmLogService); + ReflectionTestUtils.setField(service, "extActBpmFileService", extActBpmFileService); + } + + private LoginUser user(String id) { + LoginUser u = new LoginUser(); + u.setId(id); + u.setUsername("u" + id); + u.setRealname("用户" + id); + return u; + } + + /** mock 用户有效部门:orgType=2 + orgCategory=2 + delFlag=0 才进入可提报部门 */ + private void mockUserDepartments(String userId, String deptId, String deptName) { + org.jeecg.modules.system.entity.SysDepart depart = new org.jeecg.modules.system.entity.SysDepart(); + depart.setId(deptId); + depart.setDepartName(deptName); + depart.setOrgCode("org-" + deptId); + depart.setOrgType("2"); + depart.setOrgCategory("2"); + depart.setDelFlag("0"); + when(sysUserDepartService.getOrderedDepartListById(userId)).thenReturn(java.util.List.of(depart)); + } + + // ==================== createNewProposal(流水号) ==================== + + @Nested + class CreateNewProposal { + + @Test + void should_generate_proposal_no_with_sequence() { + ImprovementProposal proposal = new ImprovementProposal(); + proposal.setProposalDeptId("dept-1"); + mockUserDepartments("1", "dept-1", "研发部"); + when(improvementProposalMapper.incrementProposalYearSequence(2026)).thenReturn(1); + when(improvementProposalMapper.selectProposalYearSequenceForUpdate(2026)).thenReturn(12); + + service.createNewProposal(proposal, user("1")); + + assertEquals("研发部-2026年-0012", proposal.getProposalNo()); + verify(improvementProposalMapper).insert(any()); + } + + @Test + void should_throw_when_sequence_missing() { + ImprovementProposal proposal = new ImprovementProposal(); + proposal.setProposalDeptId("dept-1"); + mockUserDepartments("1", "dept-1", "研发部"); + when(improvementProposalMapper.incrementProposalYearSequence(any())).thenReturn(1); + when(improvementProposalMapper.selectProposalYearSequenceForUpdate(any())).thenReturn(null); + assertThrows(IllegalStateException.class, () -> service.createNewProposal(proposal, user("1"))); + } + } + + // ==================== saveInitialReview(奖级 → needDeptScore) ==================== + + @Nested + class SaveInitialReview { + + private ImprovementProposal proposal(String id, String procId) { + ImprovementProposal p = new ImprovementProposal(); + p.setId(id); + p.setProcessInstanceId(procId); + return p; + } + + @Test + void should_set_need_dept_score_when_gold() { + ImprovementProposalBpmActionDTO action = new ImprovementProposalBpmActionDTO(); + action.setMainId("p-1"); + action.setProcessInstanceId("proc-1"); + action.setTaskId("t-1"); + action.setInitialAward("0"); // 金 → 需部门评分 + when(improvementProposalMapper.selectById("p-1")).thenReturn(proposal("p-1", "proc-1")); + org.flowable.task.api.Task task = org.mockito.Mockito.mock(org.flowable.task.api.Task.class); + org.flowable.task.api.TaskQuery taskQuery = org.mockito.Mockito.mock(org.flowable.task.api.TaskQuery.class); + when(taskService.createTaskQuery()).thenReturn(taskQuery); + when(taskQuery.taskId("t-1")).thenReturn(taskQuery); + when(taskQuery.singleResult()).thenReturn(task); + when(task.getProcessInstanceId()).thenReturn("proc-1"); + when(task.getTaskDefinitionKey()).thenReturn("Task_improve001"); + when(task.getAssignee()).thenReturn("u1"); + + service.saveInitialReview(action, user("1")); + + verify(improvementProposalMapper).updateById(any()); + verify(runtimeService).setVariable("proc-1", "need_dept_score", "1"); + } + + @Test + void should_not_need_dept_score_when_bronze() { + ImprovementProposalBpmActionDTO action = new ImprovementProposalBpmActionDTO(); + action.setMainId("p-1"); + action.setProcessInstanceId("proc-1"); + action.setTaskId("t-1"); + action.setInitialAward("2"); // 铜 → 不需部门评分 + when(improvementProposalMapper.selectById("p-1")).thenReturn(proposal("p-1", "proc-1")); + org.flowable.task.api.Task task = org.mockito.Mockito.mock(org.flowable.task.api.Task.class); + org.flowable.task.api.TaskQuery taskQuery = org.mockito.Mockito.mock(org.flowable.task.api.TaskQuery.class); + when(taskService.createTaskQuery()).thenReturn(taskQuery); + when(taskQuery.taskId("t-1")).thenReturn(taskQuery); + when(taskQuery.singleResult()).thenReturn(task); + when(task.getProcessInstanceId()).thenReturn("proc-1"); + when(task.getTaskDefinitionKey()).thenReturn("Task_improve001"); + when(task.getAssignee()).thenReturn("u1"); + + service.saveInitialReview(action, user("1")); + + verify(runtimeService).setVariable("proc-1", "need_dept_score", "0"); + } + } + + // ==================== saveDeptScore(评分计算 + 落库) ==================== + + @Nested + class SaveDeptScore { + + @Test + void should_calculate_and_insert_score() { + ImprovementDeptScoreBpmDTO scoreAction = new ImprovementDeptScoreBpmDTO(); + scoreAction.setMainId("p-1"); + scoreAction.setProcessInstanceId("proc-1"); + scoreAction.setTaskId("t-1"); + scoreAction.setSafetyScore(BigDecimal.valueOf(80)); + scoreAction.setQualityScore(BigDecimal.valueOf(90)); + scoreAction.setEfficiencyScore(BigDecimal.valueOf(70)); + scoreAction.setCostScore(BigDecimal.valueOf(60)); + scoreAction.setPromotionScore(BigDecimal.valueOf(80)); + scoreAction.setOriginalityScore(BigDecimal.valueOf(90)); + scoreAction.setEffortScore(BigDecimal.valueOf(60)); + + ImprovementProposal proposal = new ImprovementProposal(); + proposal.setId("p-1"); + proposal.setProcessInstanceId("proc-1"); + when(improvementProposalMapper.selectById("p-1")).thenReturn(proposal); + when(improvementProposalMapper.selectByIdForUpdate("p-1")).thenReturn(proposal); + when(improvementDeptScoreMapper.selectByMainId("p-1")).thenReturn(java.util.List.of()); + org.flowable.task.api.Task task = org.mockito.Mockito.mock(org.flowable.task.api.Task.class); + org.flowable.task.api.TaskQuery taskQuery = org.mockito.Mockito.mock(org.flowable.task.api.TaskQuery.class); + when(taskService.createTaskQuery()).thenReturn(taskQuery); + when(taskQuery.taskId("t-1")).thenReturn(taskQuery); + when(taskQuery.singleResult()).thenReturn(task); + when(task.getProcessInstanceId()).thenReturn("proc-1"); + when(task.getTaskDefinitionKey()).thenReturn("Task_improve002"); + when(task.getAssignee()).thenReturn("u1"); + + service.saveDeptScore(scoreAction, user("1")); + + org.mockito.ArgumentCaptor captor = + org.mockito.ArgumentCaptor.forClass(org.jeecg.modules.supervision.improvementproposal.entity.ImprovementDeptScore.class); + verify(improvementDeptScoreMapper).insert(captor.capture()); + // 效果分 (80+90+70+60)/4=75;总分 75*0.5+80*0.2+90*0.2+60*0.1=77.5 + assertEquals("75.00", captor.getValue().getEffectScore().toPlainString()); + assertEquals("77.50", captor.getValue().getTotalScore().toPlainString()); + assertEquals("p-1", captor.getValue().getMainId()); + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalSupportTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalSupportTest.java new file mode 100644 index 0000000..b12c9a7 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/improvementproposal/service/impl/ImprovementProposalSupportTest.java @@ -0,0 +1,97 @@ +package org.jeecg.modules.supervision.improvementproposal.service.impl; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * ImprovementProposalSupport 纯函数测试(TDD 提炼自 ImprovementProposalServiceImpl)。 + */ +class ImprovementProposalSupportTest { + + // ============ formatProposalNo ============ + + @Test + void should_format_sequence_with_4_digits() { + // 原格式:部门-年份年-4位序号(含连字符) + assertEquals("研发部-2026年-0001", + ImprovementProposalSupport.formatProposalNo("研发部", 2026, 1)); + assertEquals("研发部-2026年-0012", + ImprovementProposalSupport.formatProposalNo(" 研发部 ", 2026, 12)); + } + + // ============ calculateEffectScore ============ + + @Test + void should_calculate_effect_average() { + // (80+90+70+60)/4 = 75 + BigDecimal score = ImprovementProposalSupport.calculateEffectScore( + BigDecimal.valueOf(80), BigDecimal.valueOf(90), + BigDecimal.valueOf(70), BigDecimal.valueOf(60)); + assertEquals(0, score.compareTo(BigDecimal.valueOf(75))); + } + + @Test + void should_round_half_up_to_2_digits() { + // (85+85+85+86)/4 = 85.25 + BigDecimal score = ImprovementProposalSupport.calculateEffectScore( + BigDecimal.valueOf(85), BigDecimal.valueOf(85), + BigDecimal.valueOf(85), BigDecimal.valueOf(86)); + assertEquals("85.25", score.toPlainString()); + } + + // ============ calculateTotalScore ============ + + @Test + void should_calculate_weighted_total() { + // 效果75*0.5 + 推广80*0.2 + 独创90*0.2 + 努力60*0.1 = 37.5+16+18+6 = 77.5 + BigDecimal total = ImprovementProposalSupport.calculateTotalScore( + BigDecimal.valueOf(75), BigDecimal.valueOf(80), + BigDecimal.valueOf(90), BigDecimal.valueOf(60)); + assertEquals("77.50", total.toPlainString()); + } + + // ============ resolveSuggestedAward ============ + + @Test + void should_map_award_by_average_score() { + assertEquals("0", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(85))); // 金 + assertEquals("1", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(70))); // 银 + assertEquals("2", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(50))); // 铜 + assertEquals("3", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(30))); // 参与 + } + + @Test + void should_handle_boundaries() { + assertEquals("0", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(80))); // 80→金 + assertEquals("1", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(60))); // 60→银 + assertEquals("2", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(40))); // 40→铜 + } + + // ============ award 校验/排名/部门评分判断 ============ + + @Test + void should_validate_award_values() { + assertTrue(ImprovementProposalSupport.isValidAward("0")); + assertTrue(ImprovementProposalSupport.isValidAward("3")); + assertFalse(ImprovementProposalSupport.isValidAward("4")); + assertFalse(ImprovementProposalSupport.isValidAward(null)); + } + + @Test + void should_rank_award() { + assertEquals(2, ImprovementProposalSupport.awardRank("2")); + } + + @Test + void should_need_dept_score_only_for_gold_silver() { + assertTrue(ImprovementProposalSupport.needsDeptScore("0")); + assertTrue(ImprovementProposalSupport.needsDeptScore("1")); + assertFalse(ImprovementProposalSupport.needsDeptScore("2")); + assertFalse(ImprovementProposalSupport.needsDeptScore("3")); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupportTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupportTest.java new file mode 100644 index 0000000..8a6de18 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetailSupportTest.java @@ -0,0 +1,88 @@ +package org.jeecg.modules.supervision.tasklist.service.impl; + +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * TaskListDetailSupport 纯函数测试(TDD 提炼自 TaskListDetialServiceImpl)。 + */ +class TaskListDetailSupportTest { + + // ============ normalizePid ============ + + @Test + void should_keep_non_empty_pid() { + assertEquals("p-1", TaskListDetailSupport.normalizePid("p-1")); + } + + @Test + void should_normalize_empty_pid_to_null() { + assertNull(TaskListDetailSupport.normalizePid("")); + assertNull(TaskListDetailSupport.normalizePid(null)); + } + + // ============ collectUserIds ============ + + @Test + void should_collect_trimmed_ids() { + Set set = new LinkedHashSet<>(); + TaskListDetailSupport.collectUserIds(" u1 , u2 ,u1 ", set); + assertEquals(Set.of("u1", "u2"), set); + } + + @Test + void should_skip_empty_ids_str() { + Set set = new LinkedHashSet<>(); + TaskListDetailSupport.collectUserIds("", set); + TaskListDetailSupport.collectUserIds(null, set); + assertEquals(0, set.size()); + } + + // ============ buildNamesFromMap ============ + + @Test + void should_build_names_in_order() { + String names = TaskListDetailSupport.buildNamesFromMap("u1,u2", + Map.of("u1", "张三", "u2", "李四")); + assertEquals("张三,李四", names); + } + + @Test + void should_skip_ids_without_mapping() { + String names = TaskListDetailSupport.buildNamesFromMap("u1,u3", + Map.of("u1", "张三")); + assertEquals("张三", names); + } + + @Test + void should_return_null_when_no_match_or_empty() { + assertNull(TaskListDetailSupport.buildNamesFromMap("u9", Map.of("u1", "张三"))); + assertNull(TaskListDetailSupport.buildNamesFromMap("", Map.of("u1", "张三"))); + assertNull(TaskListDetailSupport.buildNamesFromMap("u1", Map.of())); + } + + // ============ mergeIds ============ + + @Test + void should_merge_deduplicate_keep_order() { + assertEquals("u1,u2,u3", TaskListDetailSupport.mergeIds("u1,u2", "u2,u3")); + } + + @Test + void should_merge_single_side() { + assertEquals("u1,u2", TaskListDetailSupport.mergeIds("u1,u2", null)); + assertEquals("u3", TaskListDetailSupport.mergeIds(null, "u3")); + } + + @Test + void should_return_null_when_both_empty() { + assertNull(TaskListDetailSupport.mergeIds("", "")); + assertNull(TaskListDetailSupport.mergeIds(null, null)); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImplTest.java new file mode 100644 index 0000000..5a17f8b --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListDetialServiceImplTest.java @@ -0,0 +1,193 @@ +package org.jeecg.modules.supervision.tasklist.service.impl; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.jeecg.common.system.api.ISysBaseAPI; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.modules.supervision.tasklist.entity.TaskListDetial; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListDetialMapper; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListMapper; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListPermissionMapper; +import org.junit.jupiter.api.AfterEach; +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.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * TaskListDetialServiceImpl 测试(Shiro ThreadContext.bind 提供登录用户 + mock mapper/sysBaseAPI)。 + * 核心:deleteTask/toggleStatus/moveTask 的权限与父子计数规则 + 查询方法。 + */ +@ExtendWith(MockitoExtension.class) +class TaskListDetialServiceImplTest { + + @Mock private TaskListDetialMapper taskListDetialMapper; + @Mock private TaskListMapper taskListMapper; + @Mock private TaskListPermissionMapper taskListPermissionMapper; + @Mock private ISysBaseAPI sysBaseAPI; + @Mock private Subject subject; + + private TaskListDetialServiceImpl service; + + private final LoginUser loginUser = new LoginUser(); + + @BeforeEach + void setUp() { + loginUser.setId("user-1"); + loginUser.setUsername("admin"); + loginUser.setRealname("管理员"); + service = new TaskListDetialServiceImpl(); + ReflectionTestUtils.setField(service, "taskListDetialMapper", taskListDetialMapper); + ReflectionTestUtils.setField(service, "taskListMapper", taskListMapper); + ReflectionTestUtils.setField(service, "taskListPermissionMapper", taskListPermissionMapper); + ReflectionTestUtils.setField(service, "sysBaseAPI", sysBaseAPI); + // Shiro 官方测试机制:绑定 Subject 到当前线程,SecurityUtils.getSubject() 即可取到 + ThreadContext.bind(subject); + // lenient:查询方法不经过 SecurityUtils,仅写方法用到 + lenient().when(subject.getPrincipal()).thenReturn(loginUser); + } + + @AfterEach + void tearDown() { + // Shiro 1.13 无 unbind(),用 remove() 清除线程上下文 + ThreadContext.remove(); + } + + private TaskListDetial task(String id, String mainId, String pid, String type, Integer isDefault) { + TaskListDetial t = new TaskListDetial(); + t.setId(id); + t.setMainId(mainId); + t.setPid(pid); + t.setType(type); + t.setIsDefault(isDefault); + return t; + } + + // ==================== deleteTask ==================== + + @Test + void should_throw_when_task_not_exists() { + when(taskListDetialMapper.selectById("t1")).thenReturn(null); + assertThrows(RuntimeException.class, () -> service.deleteTask("t1")); + } + + @Test + void should_throw_when_default_group() { + TaskListDetial group = task("g1", "m1", null, "0", 1); + when(taskListDetialMapper.selectById("g1")).thenReturn(group); + when(taskListPermissionMapper.selectOne(any())).thenReturn(null); + assertThrows(RuntimeException.class, () -> service.deleteTask("g1")); + } + + @Test + void should_delete_task_and_update_parent_counts() { + TaskListDetial task = task("t1", "m1", "p1", "1", null); + task.setTaskStatus(1); + when(taskListDetialMapper.selectById("t1")).thenReturn(task); + when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("1")); + when(taskListDetialMapper.selectChildrenByPid("t1")).thenReturn(List.of()); + TaskListDetial parent = task("p1", "m1", null, "1", null); + when(taskListDetialMapper.selectById("p1")).thenReturn(parent); + when(taskListDetialMapper.countChildrenByPid("p1")).thenReturn(0); + + service.deleteTask("t1"); + + verify(taskListDetialMapper).deleteById("t1"); + verify(taskListDetialMapper).decrementSubTaskCount("p1"); + verify(taskListDetialMapper).decrementCompletedSubTaskCount("p1"); + verify(taskListDetialMapper).updateHasChild("p1", "0"); + } + + // ==================== toggleStatus ==================== + + @Test + void should_toggle_status_and_increment_completed() { + TaskListDetial task = task("t1", "m1", "p1", "1", null); + task.setTaskStatus(0); + when(taskListDetialMapper.selectById("t1")).thenReturn(task); + when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("2")); + TaskListDetial parent = task("p1", "m1", null, "1", null); + when(taskListDetialMapper.selectById("p1")).thenReturn(parent); + + service.toggleStatus("t1"); + + verify(taskListDetialMapper).toggleTaskStatus(eq("t1"), eq("admin"), eq(1), any()); + verify(taskListDetialMapper).incrementCompletedSubTaskCount("p1"); + } + + @Test + void should_reject_reader_operating_others_task() { + TaskListDetial task = task("t1", "m1", null, "1", null); + task.setAssigneeId("other-user"); + when(taskListDetialMapper.selectById("t1")).thenReturn(task); + when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("3")); + assertThrows(RuntimeException.class, () -> service.toggleStatus("t1")); + } + + // ==================== moveTask ==================== + + @Test + void should_move_task_and_update_parents_when_pid_changed() { + TaskListDetial task = task("t1", "m1", "old-p", "1", null); + when(taskListDetialMapper.selectById("t1")).thenReturn(task); + when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("2")); + when(taskListDetialMapper.selectList(any())).thenReturn(List.of()); + TaskListDetial oldParent = task("old-p", "m1", null, "1", null); + TaskListDetial newParent = task("new-p", "m1", null, "1", null); + when(taskListDetialMapper.selectById("old-p")).thenReturn(oldParent); + when(taskListDetialMapper.selectById("new-p")).thenReturn(newParent); + when(taskListDetialMapper.countChildrenByPid("old-p")).thenReturn(0); + + org.jeecg.modules.supervision.tasklist.vo.MoveTaskReq req = new org.jeecg.modules.supervision.tasklist.vo.MoveTaskReq(); + req.setTaskId("t1"); + req.setTargetPid("new-p"); + req.setTargetSortOrder(1); + service.moveTask(req); + + verify(taskListDetialMapper).decrementSubTaskCount("old-p"); + verify(taskListDetialMapper).incrementSubTaskCount("new-p"); + verify(taskListDetialMapper).updateHasChild("new-p", "1"); + } + + // ==================== 查询方法 ==================== + + @Test + void should_query_all_by_main_id() { + // task 无 createBy/assigneeId 等字段 → fillCreateByName 不触发用户查询 + TaskListDetial task = task("t1", "m1", null, "1", null); + when(taskListDetialMapper.selectAllByMainId("m1")).thenReturn(List.of(task)); + + List result = service.queryAllByMainId("m1"); + + assertEquals(1, result.size()); + verify(taskListDetialMapper).selectAllByMainId("m1"); + } + + @Test + void should_query_top_level() { + when(taskListDetialMapper.selectTopLevelByMainId("m1")).thenReturn(List.of()); + assertEquals(0, service.queryTopLevelByMainId("m1").size()); + } + + // ==================== 工具 ==================== + + private org.jeecg.modules.supervision.tasklist.entity.TaskListPermission perm(String permission) { + org.jeecg.modules.supervision.tasklist.entity.TaskListPermission p = + new org.jeecg.modules.supervision.tasklist.entity.TaskListPermission(); + p.setPermission(permission); + return p; + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListFavoriteServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListFavoriteServiceImplTest.java new file mode 100644 index 0000000..fb4ddfb --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListFavoriteServiceImplTest.java @@ -0,0 +1,127 @@ +package org.jeecg.modules.supervision.tasklist.service.impl; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import org.jeecg.modules.supervision.tasklist.entity.TaskListFavorite; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListFavoriteMapper; +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 org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * TaskListFavoriteServiceImpl 测试(排序规则 reorderItem 为核心)。 + */ +@ExtendWith(MockitoExtension.class) +class TaskListFavoriteServiceImplTest { + + @Mock private TaskListFavoriteMapper taskListFavoriteMapper; + + private TaskListFavoriteServiceImpl service; + + @BeforeEach + void setUp() { + service = new TaskListFavoriteServiceImpl(); + ReflectionTestUtils.setField(service, "taskListFavoriteMapper", taskListFavoriteMapper); + } + + @Nested + class MaxSortOrder { + + @Test + void should_default_zero_when_null() { + when(taskListFavoriteMapper.selectMaxSortOrder("u1", "p1")).thenReturn(null); + assertEquals(0, service.getMaxSortOrder("u1", "p1")); + } + + @Test + void should_return_max() { + when(taskListFavoriteMapper.selectMaxSortOrder("u1", "p1")).thenReturn(5); + assertEquals(5, service.getMaxSortOrder("u1", "p1")); + } + } + + @Nested + class ReorderItem { + + private TaskListFavorite favorite(String id, Integer sort) { + TaskListFavorite f = new TaskListFavorite(); + f.setId(id); + f.setSortOrder(sort); + return f; + } + + @Test + void should_throw_when_moved_not_found() { + when(taskListFavoriteMapper.selectById("m1")).thenReturn(null); + assertThrows(RuntimeException.class, + () -> service.reorderItem("m1", "u1", "p1", null, 1, null)); + } + + @Test + void should_reorder_siblings_within_same_group() { + // 同组移动:m1 拖到位置 1,兄弟重排 + TaskListFavorite moved = favorite("m1", 3); + when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved); + when(taskListFavoriteMapper.selectList(any(Wrapper.class))) + .thenReturn(List.of(favorite("a", 1), favorite("b", 2))); + + service.reorderItem("m1", "u1", "p1", null, 1, null); + + // 兄弟按新位置重排:pos=1 → a 挪到 2,b 挪到 3 + verify(taskListFavoriteMapper).updateSortOrder(eq("a"), eq(2)); + verify(taskListFavoriteMapper).updateSortOrder(eq("b"), eq(3)); + // moved 更新 pid 与位置 + verify(taskListFavoriteMapper).updatePidAndSort("m1", "p1", 1); + } + + @Test + void should_use_new_pid_when_cross_group() { + TaskListFavorite moved = favorite("m1", 1); + when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved); + when(taskListFavoriteMapper.selectList(any(Wrapper.class))).thenReturn(List.of()); + + service.reorderItem("m1", "u1", "p1", "type1", null, "new-group"); + + verify(taskListFavoriteMapper).updatePidAndSort("m1", "new-group", 1); + } + + @Test + void should_set_null_pid_when_new_pid_empty() { + // newPid="" 表示拖到根级别(无分组)→ pid 置 null + TaskListFavorite moved = favorite("m1", 1); + when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved); + when(taskListFavoriteMapper.selectList(any(Wrapper.class))).thenReturn(List.of()); + + service.reorderItem("m1", "u1", "p1", null, null, ""); + + verify(taskListFavoriteMapper).updatePidAndSort("m1", null, 1); + } + + @Test + void should_not_update_sibling_when_sort_unchanged() { + TaskListFavorite moved = favorite("m1", 2); + when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved); + when(taskListFavoriteMapper.selectList(any(Wrapper.class))) + .thenReturn(List.of(favorite("a", 1))); + + // pos=2(默认 targetPosition null → siblings.size()+1=2):a 保持 1,不更新 + service.reorderItem("m1", "u1", "p1", null, null, null); + + verify(taskListFavoriteMapper, never()).updateSortOrder(eq("a"), any()); + verify(taskListFavoriteMapper).updatePidAndSort("m1", "p1", 2); + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListPermissionServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListPermissionServiceImplTest.java new file mode 100644 index 0000000..5a11100 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListPermissionServiceImplTest.java @@ -0,0 +1,41 @@ +package org.jeecg.modules.supervision.tasklist.service.impl; + +import org.jeecg.modules.supervision.tasklist.entity.TaskListPermission; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListPermissionMapper; +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.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * TaskListPermissionServiceImpl 测试(简单转发)。 + */ +@ExtendWith(MockitoExtension.class) +class TaskListPermissionServiceImplTest { + + @Mock private TaskListPermissionMapper taskListPermissionMapper; + + private TaskListPermissionServiceImpl service; + + @BeforeEach + void setUp() { + service = new TaskListPermissionServiceImpl(); + ReflectionTestUtils.setField(service, "taskListPermissionMapper", taskListPermissionMapper); + } + + @Test + void should_select_by_main_id() { + TaskListPermission perm = new TaskListPermission(); + when(taskListPermissionMapper.selectByMainId("m-1")).thenReturn(List.of(perm)); + assertEquals(1, service.selectByMainId("m-1").size()); + verify(taskListPermissionMapper).selectByMainId("m-1"); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImplTest.java new file mode 100644 index 0000000..009f333 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/tasklist/service/impl/TaskListServiceImplTest.java @@ -0,0 +1,236 @@ +package org.jeecg.modules.supervision.tasklist.service.impl; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.jeecg.common.system.api.ISysBaseAPI; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.modules.supervision.tasklist.entity.TaskList; +import org.jeecg.modules.supervision.tasklist.entity.TaskListFavorite; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListDetialMapper; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListFavoriteMapper; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListMapper; +import org.jeecg.modules.supervision.tasklist.mapper.TaskListPermissionMapper; +import org.jeecg.modules.supervision.tasklist.service.ITaskListFavoriteService; +import org.jeecg.modules.supervision.tasklist.vo.AddCollaboratorReq; +import org.jeecg.modules.supervision.tasklist.vo.CreateTaskListGroupReq; +import org.jeecg.modules.supervision.tasklist.vo.CreateTaskListReq; +import org.jeecg.modules.supervision.tasklist.vo.MoveTaskListReq; +import org.junit.jupiter.api.AfterEach; +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 org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * TaskListServiceImpl 测试(Shiro ThreadContext.bind + mock mapper/service/sysBaseAPI)。 + * 核心:创建清单的密级/分组校验、创建分组排序、移动清单权限、添加协作人权限。 + */ +@ExtendWith(MockitoExtension.class) +class TaskListServiceImplTest { + + @Mock private TaskListMapper taskListMapper; + @Mock private TaskListDetialMapper taskListDetialMapper; + @Mock private TaskListPermissionMapper taskListPermissionMapper; + @Mock private TaskListFavoriteMapper taskListFavoriteMapper; + @Mock private ITaskListFavoriteService taskListFavoriteService; + @Mock private ISysBaseAPI sysBaseAPI; + @Mock private Subject subject; + + private TaskListServiceImpl service; + + private final LoginUser loginUser = new LoginUser(); + + @BeforeEach + void setUp() { + loginUser.setId("user-1"); + loginUser.setUsername("admin"); + loginUser.setRealname("管理员"); + loginUser.setUserSecurityLevel(3); + service = new TaskListServiceImpl(); + ReflectionTestUtils.setField(service, "taskListMapper", taskListMapper); + ReflectionTestUtils.setField(service, "taskListDetialMapper", taskListDetialMapper); + ReflectionTestUtils.setField(service, "taskListPermissionMapper", taskListPermissionMapper); + ReflectionTestUtils.setField(service, "taskListFavoriteMapper", taskListFavoriteMapper); + ReflectionTestUtils.setField(service, "taskListFavoriteService", taskListFavoriteService); + ReflectionTestUtils.setField(service, "sysBaseAPI", sysBaseAPI); + ThreadContext.bind(subject); + lenient().when(subject.getPrincipal()).thenReturn(loginUser); + } + + @AfterEach + void tearDown() { + ThreadContext.remove(); + } + + // ==================== createTaskList ==================== + + @Nested + class CreateTaskList { + + @Test + void should_reject_when_secret_level_too_high() { + CreateTaskListReq req = new CreateTaskListReq(); + req.setSecretLevel(5); // userSecLevel=3,5>=3 → 拒绝 + assertThrows(RuntimeException.class, () -> service.createTaskList(req)); + } + + @Test + void should_reject_when_group_invalid() { + CreateTaskListReq req = new CreateTaskListReq(); + req.setSecretLevel(1); + req.setPid("g-1"); + when(taskListFavoriteMapper.selectById("g-1")).thenReturn(null); + assertThrows(RuntimeException.class, () -> service.createTaskList(req)); + } + + @Test + void should_create_list_with_default_group() { + CreateTaskListReq req = new CreateTaskListReq(); + req.setTasklistName("我的清单"); + req.setSecretLevel(1); + // mock 模拟 MyBatis Plus insert 主键回填(ASSIGN_ID) + when(taskListMapper.insert(any())).thenAnswer(inv -> { + TaskList tl = inv.getArgument(0); + tl.setId("tl-1"); + return 1; + }); + + String id = service.createTaskList(req); + + assertEquals("tl-1", id); + verify(taskListDetialMapper).insert(any()); // 默认分组 + } + } + + // ==================== createTaskListGroup ==================== + + @Nested + class CreateTaskListGroup { + + @Test + void should_use_max_sort_plus_one() { + CreateTaskListGroupReq req = new CreateTaskListGroupReq(); + req.setTasklistName("新分组"); + when(taskListFavoriteService.getMaxSortOrderByType("user-1", null, "0")).thenReturn(5); + + String id = service.createTaskListGroup(req); + + assertEquals(id, id); + org.mockito.ArgumentCaptor captor = + org.mockito.ArgumentCaptor.forClass(TaskListFavorite.class); + verify(taskListFavoriteMapper).insert(captor.capture()); + assertEquals(6, captor.getValue().getSortOrder()); + assertEquals("user-1", captor.getValue().getUserId()); + assertEquals("0", captor.getValue().getType()); + } + } + + // ==================== moveTaskList ==================== + + @Nested + class MoveTaskList { + + @Test + void should_reject_when_favorite_not_owner() { + MoveTaskListReq req = new MoveTaskListReq(); + req.setFavoriteId("f-1"); + when(taskListFavoriteMapper.selectById("f-1")).thenReturn(null); + assertThrows(RuntimeException.class, () -> service.moveTaskList(req)); + } + + @Test + void should_reject_when_favorite_is_group() { + TaskListFavorite group = new TaskListFavorite(); + group.setId("f-1"); + group.setUserId("user-1"); + group.setType("0"); + MoveTaskListReq req = new MoveTaskListReq(); + req.setFavoriteId("f-1"); + when(taskListFavoriteMapper.selectById("f-1")).thenReturn(group); + assertThrows(RuntimeException.class, () -> service.moveTaskList(req)); + } + + @Test + void should_move_list_and_reorder() { + TaskListFavorite favorite = new TaskListFavorite(); + favorite.setId("f-1"); + favorite.setUserId("user-1"); + favorite.setType("1"); + favorite.setPid("g-1"); + when(taskListFavoriteMapper.selectById("f-1")).thenReturn(favorite); + + MoveTaskListReq req = new MoveTaskListReq(); + req.setFavoriteId("f-1"); + req.setTargetGroupId("g-2"); + req.setSortOrder(1); + TaskListFavorite targetGroup = new TaskListFavorite(); + targetGroup.setUserId("user-1"); + targetGroup.setType("0"); + when(taskListFavoriteMapper.selectById("g-2")).thenReturn(targetGroup); + when(taskListFavoriteService.countChildren("user-1", "g-1")).thenReturn(0); + + service.moveTaskList(req); + + verify(taskListFavoriteService).reorderItem("f-1", "user-1", "g-1", "1", 1, "g-2"); + // 旧父 hasChild 置 0 + 新父 hasChild 置 1(实体 updateById) + verify(taskListFavoriteMapper, times(2)).updateById(any()); + } + } + + // ==================== addCollaborator ==================== + + @Nested + class AddCollaborator { + + @Test + void should_reject_when_not_owner() { + AddCollaboratorReq req = new AddCollaboratorReq(); + req.setTaskListId("tl-1"); + req.setUserId("u-9"); + req.setPermission("2"); + when(taskListPermissionMapper.selectCount(any())).thenReturn(0L); + assertThrows(RuntimeException.class, () -> service.addCollaborator(req)); + } + + @Test + void should_reject_when_permission_type_invalid() { + AddCollaboratorReq req = new AddCollaboratorReq(); + req.setTaskListId("tl-1"); + req.setPermission("1"); + when(taskListPermissionMapper.selectCount(any())).thenReturn(1L); + assertThrows(RuntimeException.class, () -> service.addCollaborator(req)); + } + + @Test + void should_add_collaborator() { + AddCollaboratorReq req = new AddCollaboratorReq(); + req.setTaskListId("tl-1"); + req.setUserId("u-9"); + req.setPermission("2"); + // selectCount 调 2 次:ownerCheck=1(通过)+ existCheck=0(不存在则新增) + when(taskListPermissionMapper.selectCount(any())).thenReturn(1L, 0L); + TaskList taskList = new TaskList(); + taskList.setSecretLevel(1); + when(taskListMapper.selectById("tl-1")).thenReturn(taskList); + LoginUser target = new LoginUser(); + target.setUserSecurityLevel(3); // 密级须 > 清单密级(1) 才能添加 + when(sysBaseAPI.getUserById("u-9")).thenReturn(target); + + service.addCollaborator(req); + + verify(taskListPermissionMapper).insert(any()); + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlowTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlowTest.java new file mode 100644 index 0000000..0652bce --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFeedbackFlowTest.java @@ -0,0 +1,259 @@ +package org.jeecg.modules.supervision.xispeak.flow; + +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.DelegateExecution; +import org.jeecg.common.system.api.ISysBaseAPI; +import org.jeecg.modules.supervision.common.OrgConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakFeedbackConfig; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +import org.jeecg.modules.tasktask.entity.TaskTask; +import org.jeecg.modules.tasktask.service.ITaskTaskService; +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.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * XiSpeakFeedbackFlow 反馈流程表达式测试(mock 外部依赖)。 + */ +@ExtendWith(MockitoExtension.class) +class XiSpeakFeedbackFlowTest { + + @Mock private XiSpeakFeedbackConfig xiSpeakFeedbackConfig; + @Mock private OrgConfig orgConfig; + @Mock private ISysBaseAPI iSysBaseAPI; + @Mock private RuntimeService runtimeService; + @Mock private IBgXiSpeakService bgXiSpeakService; + @Mock private ITaskTaskService taskTaskService; + @Mock private DelegateExecution execution; + + private XiSpeakFeedbackFlow flow; + + @BeforeEach + void setUp() { + OrgConfig.Dept dept = new OrgConfig.Dept(); + dept.setJj("dept-jj"); + OrgConfig.Role role = new OrgConfig.Role(); + role.setJjWorker("role-jj-worker"); + role.setLd("role-ld"); + lenient().when(orgConfig.getDept()).thenReturn(dept); + lenient().when(orgConfig.getRole()).thenReturn(role); + flow = new XiSpeakFeedbackFlow(xiSpeakFeedbackConfig, orgConfig, iSysBaseAPI, runtimeService, bgXiSpeakService, taskTaskService); + } + + // ==================== getImplDeptList ==================== + + @Nested + class ImplDeptList { + + @Test + void should_return_empty_when_keys_missing() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(execution.getProcessInstanceBusinessKey()).thenReturn(""); + assertEquals(List.of(), flow.getImplDeptList(execution)); + } + + @Test + void should_return_empty_when_entity_not_found() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(execution.getProcessInstanceBusinessKey()).thenReturn("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(null); + assertEquals(List.of(), flow.getImplDeptList(execution)); + } + + @Test + void should_return_empty_when_approve_info_empty() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(execution.getProcessInstanceBusinessKey()).thenReturn("biz-1"); + BgXiSpeak entity = new BgXiSpeak(); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + assertEquals(List.of(), flow.getImplDeptList(execution)); + } + + @Test + void should_return_dept_keys() { + when(execution.getProcessInstanceId()).thenReturn("proc-1"); + when(execution.getProcessInstanceBusinessKey()).thenReturn("biz-1"); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", new DeptApproveDetail()); + map.put("dept-2", new DeptApproveDetail()); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(map); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + assertEquals(List.of("dept-1", "dept-2"), flow.getImplDeptList(execution)); + } + } + + // ==================== getImplDeptApprove ==================== + + @Nested + class ImplDeptApprove { + + private TaskTask taskWithHandler(String handler) { + TaskTask tt = new TaskTask(); + tt.setBusinessId("xi-biz"); + tt.setDeptHandlerName(handler); + return tt; + } + + @Test + void should_return_empty_when_business_key_blank() { + when(execution.getProcessInstanceBusinessKey()).thenReturn(" "); + assertEquals("", flow.getImplDeptApprove(execution)); + } + + @Test + void should_return_empty_when_task_missing() { + when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1"); + when(taskTaskService.getById("tt-1")).thenReturn(null); + assertEquals("", flow.getImplDeptApprove(execution)); + } + + @Test + void should_return_matched_approver_name() { + when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1"); + when(taskTaskService.getById("tt-1")).thenReturn(taskWithHandler("worker1")); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setImplUserNameList(List.of("worker1")); + detail.setApproverName("leader1"); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(map); + when(bgXiSpeakService.getById("xi-biz")).thenReturn(entity); + + assertEquals("leader1", flow.getImplDeptApprove(execution)); + } + + @Test + void should_return_empty_when_no_match() { + when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1"); + when(taskTaskService.getById("tt-1")).thenReturn(taskWithHandler("nobody")); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setImplUserNameList(List.of("worker1")); + detail.setApproverName("leader1"); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(map); + when(bgXiSpeakService.getById("xi-biz")).thenReturn(entity); + + assertEquals("", flow.getImplDeptApprove(execution)); + } + } + + // ==================== 部门审批详情(经 taskTask 链路) ==================== + + @Nested + class DeptDetailByTask { + + private void mockDetailChain(String deptId, DeptApproveDetail detail) { + when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1"); + TaskTask tt = new TaskTask(); + tt.setBusinessId("xi-biz"); + tt.setDeptId(deptId); + when(taskTaskService.getById("tt-1")).thenReturn(tt); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put(deptId, detail); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(map); + when(bgXiSpeakService.getById("xi-biz")).thenReturn(entity); + } + + @Test + void should_return_zero_when_detail_missing() { + when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1"); + when(taskTaskService.getById("tt-1")).thenReturn(new TaskTask()); + assertEquals(0, flow.getImplDeptIsEnd(execution)); + } + + @Test + void should_read_is_end() { + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setIsEnd(1); + mockDetailChain("dept-1", detail); + assertEquals(1, flow.getImplDeptIsEnd(execution)); + } + + @Test + void should_read_impl_worker() { + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setImplWorker("w1"); + mockDetailChain("dept-1", detail); + assertEquals("w1", flow.getImplLeaderAsWorker(execution)); + } + + @Test + void should_return_empty_when_worker_blank() { + mockDetailChain("dept-1", new DeptApproveDetail()); + assertEquals("", flow.getImplLeaderAsWorker(execution)); + } + } + + // ==================== getImplDeptWorker ==================== + + @Nested + class ImplDeptWorker { + + @Test + void should_return_empty_when_business_key_blank() { + when(execution.getProcessInstanceBusinessKey()).thenReturn(" "); + assertEquals(List.of(), flow.getImplDeptWorker(execution)); + } + + @Test + void should_return_empty_when_task_missing() { + when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1"); + when(taskTaskService.getById("tt-1")).thenReturn(null); + assertEquals(List.of(), flow.getImplDeptWorker(execution)); + } + + @Test + void should_split_worker_names() { + when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1"); + TaskTask tt = new TaskTask(); + tt.setBusinessId("xi-biz"); + tt.setDeptHandlerName("张三, 李四 ,,王五"); + when(taskTaskService.getById("tt-1")).thenReturn(tt); + assertEquals(List.of("张三", "李四", "王五"), flow.getImplDeptWorker(execution)); + } + } + + // ==================== JJ 部门查询 ==================== + + @Nested + class JJDeptQuery { + + @Test + void should_return_jj_workers() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker")) + .thenReturn(List.of("jj1", "jj2")); + assertEquals(List.of("jj1", "jj2"), flow.getJJDeptWorkerList()); + } + + @Test + void should_return_jj_leaders() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-ld")) + .thenReturn(List.of("ld1")); + assertEquals(List.of("ld1"), flow.getJJDeptLeaderList()); + } + + @Test + void should_return_empty_when_no_worker() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker")) + .thenReturn(List.of()); + assertEquals(0, flow.getJJDeptWorkerListLength()); + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlowTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlowTest.java new file mode 100644 index 0000000..f3cb314 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/flow/XiSpeakFlowTest.java @@ -0,0 +1,300 @@ +package org.jeecg.modules.supervision.xispeak.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.extbpm.process.common.expression.FlowNodeExpression; +import org.jeecg.modules.supervision.common.OrgConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +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.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * XiSpeakFlow 流程表达式测试(mock config/orgConfig/iSysBaseAPI/flowNodeExpression/service)。 + */ +@ExtendWith(MockitoExtension.class) +class XiSpeakFlowTest { + + @Mock private XiSpeakConfig xiSpeakConfig; + @Mock private OrgConfig orgConfig; + @Mock private ISysBaseAPI iSysBaseAPI; + @Mock private RuntimeService runtimeService; + @Mock private FlowNodeExpression flowNodeExpression; + @Mock private IBgXiSpeakService bgXiSpeakService; + @Mock private DelegateExecution execution; + + private XiSpeakFlow flow; + private XiSpeakProperties props; + + @BeforeEach + void setUp() { + props = new XiSpeakProperties(); + props.setNeedSdwApproveCode("need_sdw_approval"); + props.setFeedbackRightNowCode("feedback_right_now"); + props.setSdwLeaderListKey("sdw_leader_list"); + props.setTemImplWorkerKey("tem_impl_worker"); + props.setTemImplLeaderKey("tem_impl_leader"); + props.setTemBgLeaderKey("tem_bg_leader"); + props.setIsEndKey("is_end"); + props.setBusinessKey("business_id"); + props.setImplDeptCollectionUsedKey("impl_dept"); + props.setDeptWorkerKey("dept_worker"); + props.setImplDeptKey("impl_dept"); + // lenient:部分测试(getListSize/getImplDeptNums 等)不触碰 config/orgConfig + lenient().when(xiSpeakConfig.getXiSpeak()).thenReturn(props); + + OrgConfig.Dept dept = new OrgConfig.Dept(); + dept.setBg("dept-bg"); + OrgConfig.Role role = new OrgConfig.Role(); + role.setLd("role-ld"); + role.setSdw("role-sdw"); + lenient().when(orgConfig.getDept()).thenReturn(dept); + lenient().when(orgConfig.getRole()).thenReturn(role); + + flow = new XiSpeakFlow(xiSpeakConfig, orgConfig, iSysBaseAPI, runtimeService, flowNodeExpression, bgXiSpeakService); + } + + // ==================== 部门角色查询 ==================== + + @Nested + class DeptRoleQuery { + + @Test + void should_return_bg_dept_ld_users() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-bg", "role-ld")) + .thenReturn(List.of("u1", "u2")); + assertEquals(List.of("u1", "u2"), flow.getBgDeptLdUserIdList()); + } + + @Test + void should_return_sdw_leaders() { + when(iSysBaseAPI.getUserByRoleIdLocalApi("role-sdw")).thenReturn(List.of("sdw1")); + assertEquals(List.of("sdw1"), flow.getSDWLeader()); + } + + @Test + void should_return_impl_dept_lds() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-x", "role-ld")) + .thenReturn(List.of("ld1")); + assertEquals(List.of("ld1"), flow.getImplDeptLdsList("dept-x")); + } + } + + // ==================== JSON Integer 解析 ==================== + + @Nested + class JsonIntegerParse { + + @Test + void should_parse_int_from_json_object() { + assertEquals(1, flow.getNeedSdwApproval(new JSONObject().fluentPut("need_sdw_approval", 1))); + } + + @Test + void should_parse_int_from_json_string() { + assertEquals(1, flow.getNeedSdwApproval("{\"need_sdw_approval\":1}")); + } + + @Test + void should_return_zero_when_json_null() { + assertEquals(0, flow.getNeedSdwApproval(null)); + } + + @Test + void should_return_zero_when_key_missing() { + assertEquals(0, flow.getFeedBackRightNow(new JSONObject().fluentPut("other", 1))); + } + + @Test + void should_return_zero_when_invalid_json() { + assertEquals(0, flow.getNeedSdwApproval("not-json")); + } + } + + // ==================== JSON List 解析 ==================== + + @Nested + class JsonListParse { + + @Test + void should_split_list_from_json_object() { + assertEquals(List.of("sdw1", "sdw2"), + flow.getSdwLeaderList(new JSONObject().fluentPut("sdw_leader_list", "sdw1,sdw2"))); + } + + @Test + void should_trim_tokens() { + assertEquals(List.of("sdw1", "sdw2"), + flow.getSdwLeaderList(new JSONObject().fluentPut("sdw_leader_list", " sdw1 , sdw2 "))); + } + + @Test + void should_return_empty_when_json_null() { + assertEquals(List.of(), flow.getSdwLeaderList(null)); + } + } + + // ==================== flowNodeExpression 转发 ==================== + + @Nested + class FlowNodeForward { + + @Test + void should_forward_impl_worker_list() { + when(flowNodeExpression.getSonProcessHqVariableList(execution, "tem_impl_worker")) + .thenReturn(List.of("w1")); + assertEquals(List.of("w1"), flow.getTemImplWorkerList(execution)); + } + + @Test + void should_forward_impl_leader_list() { + when(flowNodeExpression.getSonProcessHqVariableList(execution, "tem_impl_leader")) + .thenReturn(List.of("l1", "l2")); + assertEquals(2, flow.getTemImplLeaderListLength(execution)); + } + + @Test + void should_forward_bg_leader_list() { + when(flowNodeExpression.getSonProcessHqVariableList(execution, "tem_bg_leader")) + .thenReturn(List.of("b1")); + assertEquals(List.of("b1"), flow.getTemBgLeaderList(execution)); + } + } + + // ==================== 部门审批详情读取 ==================== + + @Nested + class DeptApproveDetailRead { + + private void mockEntityWithDetail(Integer isEnd, String implWorker, List workers) { + when(execution.getVariable("business_id")).thenReturn("biz-1"); + when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setIsEnd(isEnd); + detail.setImplWorker(implWorker); + detail.setImplUserNameList(workers); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(map); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + } + + @Test + void should_read_is_end_from_detail() { + mockEntityWithDetail(1, "w1", null); + assertEquals("1", flow.getImplDeptLeaderIsEnd(execution)); + assertEquals(1, flow.getImplDeptIsEnd(execution)); + } + + @Test + void should_read_impl_worker_from_detail() { + mockEntityWithDetail(1, "w1", null); + assertEquals("w1", flow.getImplDeptLeader(execution)); + } + + @Test + void should_read_worker_list_from_detail() { + mockEntityWithDetail(0, null, List.of("w1", "w2")); + assertEquals(List.of("w1", "w2"), flow.getImplDeptWorkerListFromJson(execution)); + assertEquals(List.of("w1", "w2"), flow.getDeptWorkerList(execution)); + } + + @Test + void should_fallback_to_variable_when_no_detail() { + // 无 detail:isEnd 从流程变量兜底(兼容旧流程) + // ⚠️ 现状行为:getSonProcessVariable 返回 List(splitUsernames), + // value.toString() 得到 "[1]" 而非 "1";getImplDeptIsEnd 的 parseInt("[1]") 失败返回 0。 + // 测试锁定现状,若后续修复 isEnd 兜底解析需同步更新。 + when(execution.getVariable("business_id")).thenReturn("biz-1"); + when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(new DeptApproveDetailMap()); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + when(flowNodeExpression.getSonProcessVariable(execution, "is_end")).thenReturn(List.of("1")); + + assertEquals("[1]", flow.getImplDeptLeaderIsEnd(execution)); + assertEquals(0, flow.getImplDeptIsEnd(execution)); + assertEquals("", flow.getImplDeptLeader(execution)); + } + + @Test + void should_return_list_to_string_when_variable_empty() { + // getSonProcessVariable 从不返回 null(空时返回 emptyList),value.toString() = "[]" + when(execution.getVariable("business_id")).thenReturn("biz-1"); + when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(new DeptApproveDetailMap()); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + when(flowNodeExpression.getSonProcessVariable(execution, "is_end")).thenReturn(List.of()); + + assertEquals("[]", flow.getImplDeptLeaderIsEnd(execution)); + assertEquals(0, flow.getImplDeptIsEnd(execution)); + } + + @Test + void should_return_zero_when_is_end_not_number() { + when(execution.getVariable("business_id")).thenReturn("biz-1"); + when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + BgXiSpeak entity = new BgXiSpeak(); + entity.setApproveInfo(new DeptApproveDetailMap()); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + when(flowNodeExpression.getSonProcessVariable(execution, "is_end")).thenReturn(List.of("abc")); + + assertEquals(0, flow.getImplDeptIsEnd(execution)); + } + } + + // ==================== implDeptNums ==================== + + @Nested + class ImplDeptNums { + + @Test + void should_count_json_array_value() { + assertEquals(2, flow.getImplDeptNums("{\"impl_dept\":[\"d1\",\"d2\"]}")); + } + + @Test + void should_count_comma_string_value() { + assertEquals(3, flow.getImplDeptNums("{\"impl_dept\":\"d1,d2,d3\"}")); + } + + @Test + void should_return_zero_when_json_null() { + assertEquals(0, flow.getImplDeptNums(null)); + } + } + + // ==================== getListSize ==================== + + @Nested + class ListSize { + + @Test + void should_count_tokens() { + assertEquals(3, flow.getListSize("a,b,c")); + } + + @Test + void should_return_zero_when_blank() { + assertEquals(0, flow.getListSize(" ")); + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListenerTest.java new file mode 100644 index 0000000..a6da4db --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgLeaderApproveListenerTest.java @@ -0,0 +1,128 @@ +package org.jeecg.modules.supervision.xispeak.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.extbpm.process.common.expression.FlowNodeExpression; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +import org.jeecg.modules.tasktask.service.ITaskTaskService; +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; + +/** + * AfterBgLeaderApproveListener 测试:将 bg 领导写入上两级执行实例局部变量。 + */ +@ExtendWith(MockitoExtension.class) +class AfterBgLeaderApproveListenerTest { + + @Mock private FlowNodeExpression flowNodeExpression; + @Mock private RuntimeService runtimeService; + @Mock private XiSpeakConfig xiSpeakConfig; + @Mock private ITaskTaskService taskTaskService; + @Mock private IBgXiSpeakService bgXiSpeakService; + @Mock private DelegateTask delegateTask; + @Mock private Execution currentExecution; + @Mock private Execution parentExecution; + @Mock private ExecutionQuery query; + @Mock private ExecutionQuery parentQuery; + + private AfterBgLeaderApproveListener listener; + + @BeforeEach + void setUp() { + XiSpeakProperties props = new XiSpeakProperties(); + props.setTemBgLeaderKey("tem_bg_leader"); + when(xiSpeakConfig.getXiSpeak()).thenReturn(props); + listener = new AfterBgLeaderApproveListener(flowNodeExpression, runtimeService, xiSpeakConfig, taskTaskService, bgXiSpeakService); + } + + 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_write_to_grand_parent_when_both_levels_exist() { + mockExecutionChain("parent-1", "grand-1"); + listener.notify(delegateTask); + verify(runtimeService).setVariableLocal("grand-1", "tem_bg_leader", "leader1"); + } + + @Test + void should_write_to_parent_when_no_grand_parent() { + mockExecutionChain("parent-1", null); + listener.notify(delegateTask); + verify(runtimeService).setVariableLocal("parent-1", "tem_bg_leader", "leader1"); + } + + @Test + void should_throw_when_set_variable_fails() { + // 与 fixcontact 不同:xispeak 监听器 catch 后重新抛出 RuntimeException(写入失败即中断流程) + mockExecutionChain("parent-1", "grand-1"); + doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", "tem_bg_leader", "leader1"); + org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, () -> listener.notify(delegateTask)); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgWorkerFinalApproveListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgWorkerFinalApproveListenerTest.java new file mode 100644 index 0000000..6835026 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterBgWorkerFinalApproveListenerTest.java @@ -0,0 +1,61 @@ +package org.jeecg.modules.supervision.xispeak.listener; + +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties; +import org.jeecg.modules.supervision.xispeak.constant.XiSpeakConstant; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * AfterBgWorkerFinalApproveListener 测试:最终审批将业务表单 completionStatus 置为已完成。 + */ +@ExtendWith(MockitoExtension.class) +class AfterBgWorkerFinalApproveListenerTest { + + @Mock private XiSpeakConfig xiSpeakConfig; + @Mock private IBgXiSpeakService bgXiSpeakService; + @Mock private DelegateTask delegateTask; + + private AfterBgWorkerFinalApproveListener listener; + + @BeforeEach + void setUp() { + XiSpeakProperties props = new XiSpeakProperties(); + props.setBusinessKey("business_id"); + when(xiSpeakConfig.getXiSpeak()).thenReturn(props); + listener = new AfterBgWorkerFinalApproveListener(xiSpeakConfig, bgXiSpeakService); + } + + @Test + void should_skip_when_business_key_missing() { + when(delegateTask.getVariable("business_id")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_update_completion_status_finished() { + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(bgXiSpeakService.updateById(any())).thenReturn(true); + + listener.notify(delegateTask); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BgXiSpeak.class); + verify(bgXiSpeakService).updateById(captor.capture()); + assertEquals("biz-1", captor.getValue().getId()); + assertEquals(XiSpeakConstant.CompletionStatus.FINISHED, captor.getValue().getCompletionStatus()); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveFeedbackListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveFeedbackListenerTest.java new file mode 100644 index 0000000..77952e1 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveFeedbackListenerTest.java @@ -0,0 +1,126 @@ +package org.jeecg.modules.supervision.xispeak.listener; + +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakFeedbackConfig; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakFeedbackService; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +import org.jeecg.modules.tasktask.service.ITaskTaskService; +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.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * AfterImplDeptLeaderApproveFeedbackListener 测试(反馈流程部门领导审批)。 + * 规则:将部门领导写入 approveInfo 对应部门的 approverName(幂等)。 + * parseDeptId 语义:Collection 取第一个;String 去 [] 保留全部(与 StoreJson 版不同)。 + */ +@ExtendWith(MockitoExtension.class) +class AfterImplDeptLeaderApproveFeedbackListenerTest { + + @Mock private XiSpeakFeedbackConfig xiSpeakFeedbackConfig; + @Mock private IBgXiSpeakFeedbackService bgXiSpeakFeedbackService; + @Mock private ITaskTaskService taskTaskService; + @Mock private XiSpeakConfig xiSpeakConfig; + @Mock private IBgXiSpeakService bgXiSpeakService; + @Mock private DelegateTask delegateTask; + + private AfterImplDeptLeaderApproveFeedbackListener listener; + + @BeforeEach + void setUp() { + XiSpeakProperties props = new XiSpeakProperties(); + props.setBusinessKey("business_id"); + props.setImplDeptCollectionUsedKey("impl_dept"); + when(xiSpeakConfig.getXiSpeak()).thenReturn(props); + listener = new AfterImplDeptLeaderApproveFeedbackListener( + xiSpeakFeedbackConfig, bgXiSpeakFeedbackService, taskTaskService, xiSpeakConfig, bgXiSpeakService); + } + + @Test + void should_skip_when_assignee_blank() { + when(delegateTask.getAssignee()).thenReturn(" "); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_when_business_key_null() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_when_entity_not_found() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_store_approver_name_when_changed() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + // parseDeptId("[dept-1]"):String 形式去 [] 保留全部 → "dept-1" + assertEquals("leader1", entity.getApproveInfo().get("dept-1").getApproverName()); + verify(bgXiSpeakService).updateById(entity); + } + + @Test + void should_skip_update_when_approver_name_unchanged() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setApproverName("leader1"); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + entity.setApproveInfo(map); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_parse_first_element_from_collection() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn(List.of("dept-a", "dept-b")); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + assertEquals("leader1", entity.getApproveInfo().get("dept-a").getApproverName()); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListenerTest.java new file mode 100644 index 0000000..0d8fe8c --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderApproveListenerTest.java @@ -0,0 +1,116 @@ +package org.jeecg.modules.supervision.xispeak.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.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; + +/** + * AfterImplDeptLeaderApproveListener 测试:将部门领导写入上两级执行实例局部变量(tem_impl_leader)。 + */ +@ExtendWith(MockitoExtension.class) +class AfterImplDeptLeaderApproveListenerTest { + + @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 AfterImplDeptLeaderApproveListener listener; + + @BeforeEach + void setUp() { + listener = new AfterImplDeptLeaderApproveListener(runtimeService); + } + + 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_write_to_grand_parent_when_both_levels_exist() { + mockExecutionChain("parent-1", "grand-1"); + listener.notify(delegateTask); + verify(runtimeService).setVariableLocal("grand-1", "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", "tem_impl_leader", "leader1"); + } + + @Test + void should_throw_when_set_variable_fails() { + // 与 fixcontact 不同:xispeak 监听器 catch 后重新抛出 RuntimeException(写入失败即中断流程) + mockExecutionChain("parent-1", "grand-1"); + doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", "tem_impl_leader", "leader1"); + org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, () -> listener.notify(delegateTask)); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderStoreJsonListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderStoreJsonListenerTest.java new file mode 100644 index 0000000..064fce0 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplDeptLeaderStoreJsonListenerTest.java @@ -0,0 +1,148 @@ +package org.jeecg.modules.supervision.xispeak.listener; + +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +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.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * AfterImplDeptLeaderStoreJsonListener 测试。 + * 规则:将部门领导写入 approveInfo 对应部门的 approverName(幂等:值相同跳过更新)。 + * parseDeptId 语义:取第一个部门 ID(与 FlowVariableSupport.cleanDeptId 不同,保留单独实现)。 + */ +@ExtendWith(MockitoExtension.class) +class AfterImplDeptLeaderStoreJsonListenerTest { + + @Mock private XiSpeakConfig xiSpeakConfig; + @Mock private IBgXiSpeakService bgXiSpeakService; + @Mock private DelegateTask delegateTask; + + private AfterImplDeptLeaderStoreJsonListener listener; + + @BeforeEach + void setUp() { + XiSpeakProperties props = new XiSpeakProperties(); + props.setBusinessKey("business_id"); + props.setImplDeptCollectionUsedKey("impl_dept"); + when(xiSpeakConfig.getXiSpeak()).thenReturn(props); + listener = new AfterImplDeptLeaderStoreJsonListener(xiSpeakConfig, bgXiSpeakService); + } + + @Test + void should_skip_when_assignee_blank() { + when(delegateTask.getAssignee()).thenReturn(" "); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_when_business_key_null() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_when_entity_not_found() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_when_dept_var_missing() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(new BgXiSpeak()); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn(null); + when(delegateTask.getVariable("impl_dept")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_store_approver_name_when_changed() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + // parseDeptId("[dept-1]"):字符串形式,取第一个元素 → "dept-1" + DeptApproveDetail detail = entity.getApproveInfo().get("dept-1"); + assertEquals("leader1", detail.getApproverName()); + verify(bgXiSpeakService).updateById(entity); + } + + @Test + void should_skip_update_when_approver_name_unchanged() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]"); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setDeptId("dept-1"); + detail.setApproverName("leader1"); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + entity.setApproveInfo(map); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_parse_first_element_from_list_like_string() { + // 字符串 "[d1,d2]":只取第一个元素 d1(与 cleanDeptId 保留全部不同) + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[d1,d2]"); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + assertEquals("leader1", entity.getApproveInfo().get("d1").getApproverName()); + assertEquals(1, entity.getApproveInfo().size()); + } + + @Test + void should_parse_first_element_from_collection() { + when(delegateTask.getAssignee()).thenReturn("leader1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn(List.of("dept-a", "dept-b")); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(bgXiSpeakService.getById("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + assertEquals("leader1", entity.getApproveInfo().get("dept-a").getApproverName()); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListenerTest.java new file mode 100644 index 0000000..8919454 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerApproveListenerTest.java @@ -0,0 +1,134 @@ +package org.jeecg.modules.supervision.xispeak.listener; + +import org.flowable.engine.RuntimeService; +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; +import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * AfterImplWorkerApproveListener 经办人会签监听器测试。 + * 规则:写子流程局部变量 tem_impl_worker + 记录经办人到 approveInfo 对应部门(去重)。 + */ +@ExtendWith(MockitoExtension.class) +class AfterImplWorkerApproveListenerTest { + + @Mock private XiSpeakConfig xiSpeakConfig; + @Mock private IBgXiSpeakService bgXiSpeakService; + @Mock private RuntimeService runtimeService; + @Mock private DelegateTask delegateTask; + + private AfterImplWorkerApproveListener listener; + + @BeforeEach + void setUp() { + XiSpeakProperties props = new XiSpeakProperties(); + props.setBusinessKey("business_id"); + props.setTemImplWorkerKey("tem_impl_worker"); + props.setImplDeptCollectionUsedKey("impl_dept"); + // lenient:notify(null) 用例在 NPE 前不会用到 config + lenient().when(xiSpeakConfig.getXiSpeak()).thenReturn(props); + listener = new AfterImplWorkerApproveListener(xiSpeakConfig, bgXiSpeakService, runtimeService); + } + + private void mockNormalTask(String deptVar) { + when(delegateTask.getAssignee()).thenReturn("worker1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + // lenient:getVariableLocal 在 getByIdForUpdate 之后才调用,entity 为 null 时不会走到(短路径) + lenient().when(delegateTask.getVariableLocal("impl_dept")).thenReturn(deptVar); + } + + @Test + void should_throw_npe_when_task_null() { + // 现状行为:该监听器没有 null 防御(与 FixContactWorkerApproveListener 不同),notify(null) 抛 NPE + org.junit.jupiter.api.Assertions.assertThrows(NullPointerException.class, () -> listener.notify(null)); + } + + @Test + void should_skip_when_assignee_blank() { + when(delegateTask.getAssignee()).thenReturn(" "); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_record_when_business_key_missing() { + when(delegateTask.getAssignee()).thenReturn("worker1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(delegateTask.getVariable("business_id")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_record_when_dept_var_missing() { + // deptVar 读取在 getByIdForUpdate 之后:须先返回 entity 才能走到 deptVar 检查 + when(delegateTask.getAssignee()).thenReturn("worker1"); + when(delegateTask.getExecutionId()).thenReturn("exec-1"); + when(delegateTask.getVariable("business_id")).thenReturn("biz-1"); + when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(new BgXiSpeak()); + when(delegateTask.getVariableLocal("impl_dept")).thenReturn(null); + when(delegateTask.getVariable("impl_dept")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_skip_record_when_entity_not_found() { + mockNormalTask("dept-1"); + when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(null); + listener.notify(delegateTask); + verify(bgXiSpeakService, never()).updateById(any()); + } + + @Test + void should_write_var_and_record_worker() { + mockNormalTask("dept-1"); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + entity.setApproveInfo(new DeptApproveDetailMap()); + when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + verify(runtimeService).setVariableLocal("exec-1", "tem_impl_worker", "worker1"); + assertEquals(List.of("worker1"), entity.getApproveInfo().get("dept-1").getImplUserNameList()); + verify(bgXiSpeakService).updateById(entity); + } + + @Test + void should_not_update_when_worker_already_in_list() { + mockNormalTask("dept-1"); + DeptApproveDetail detail = new DeptApproveDetail(); + detail.setImplUserNameList(new ArrayList<>(List.of("worker1"))); + DeptApproveDetailMap map = new DeptApproveDetailMap(); + map.put("dept-1", detail); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + entity.setApproveInfo(map); + when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(entity); + + listener.notify(delegateTask); + + verify(bgXiSpeakService, never()).updateById(any()); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListenerTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListenerTest.java new file mode 100644 index 0000000..2a435cd --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/listener/AfterImplWorkerTemStoreListenerTest.java @@ -0,0 +1,116 @@ +package org.jeecg.modules.supervision.xispeak.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.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; + +/** + * AfterImplWorkerTemStoreListener 测试:将部门经办人写入上两级执行实例局部变量(tem_impl_worker)。 + */ +@ExtendWith(MockitoExtension.class) +class AfterImplWorkerTemStoreListenerTest { + + @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 AfterImplWorkerTemStoreListener listener; + + @BeforeEach + void setUp() { + listener = new AfterImplWorkerTemStoreListener(runtimeService); + } + + private void mockExecutionChain(String parentId, String grandParentId) { + when(delegateTask.getAssignee()).thenReturn("worker1"); + 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("worker1"); + 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("worker1"); + 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("worker1"); + 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_write_to_grand_parent_when_both_levels_exist() { + mockExecutionChain("parent-1", "grand-1"); + listener.notify(delegateTask); + verify(runtimeService).setVariableLocal("grand-1", "tem_impl_worker", "worker1"); + } + + @Test + void should_write_to_parent_when_no_grand_parent() { + mockExecutionChain("parent-1", null); + listener.notify(delegateTask); + verify(runtimeService).setVariableLocal("parent-1", "tem_impl_worker", "worker1"); + } + + @Test + void should_throw_when_set_variable_fails() { + // 与 fixcontact 不同:xispeak 监听器 catch 后重新抛出 RuntimeException(写入失败即中断流程) + mockExecutionChain("parent-1", "grand-1"); + doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", "tem_impl_worker", "worker1"); + org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, () -> listener.notify(delegateTask)); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakFeedbackServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakFeedbackServiceImplTest.java new file mode 100644 index 0000000..cc602ed --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakFeedbackServiceImplTest.java @@ -0,0 +1,62 @@ +package org.jeecg.modules.supervision.xispeak.service.impl; + +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeakFeedback; +import org.jeecg.modules.supervision.xispeak.mapper.BgXiSpeakFeedbackMapper; +import org.jeecg.modules.supervision.xispeak.mapper.BgXiSpeakMapper; +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * BgXiSpeakFeedbackServiceImpl 测试。 + * 核心:syncFeedbackCounts 统计反馈总数/已闭环数并回写主表 implCount/closedCount。 + */ +@ExtendWith(MockitoExtension.class) +class BgXiSpeakFeedbackServiceImplTest { + + @Mock private BgXiSpeakFeedbackMapper baseMapper; + @Mock private BgXiSpeakMapper bgXiSpeakMapper; + + private BgXiSpeakFeedbackServiceImpl service; + + @BeforeEach + void setUp() { + service = new BgXiSpeakFeedbackServiceImpl(); + ReflectionTestUtils.setField(service, "baseMapper", baseMapper); + ReflectionTestUtils.setField(service, "bgXiSpeakMapper", bgXiSpeakMapper); + } + + @Test + void should_return_when_main_id_empty() { + // oConvertUtils.isEmpty 只认 null/空串(不认空格) + service.syncFeedbackCounts(""); + verify(baseMapper, never()).selectCount(any()); + verify(bgXiSpeakMapper, never()).updateById(any()); + } + + @Test + void should_sync_counts_to_main() { + when(baseMapper.selectCount(any())).thenReturn(5L, 2L); + + service.syncFeedbackCounts("biz-1"); + + // 两次 count:全部反馈 + 已闭环 + verify(baseMapper, times(2)).selectCount(any()); + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(BgXiSpeak.class); + verify(bgXiSpeakMapper).updateById(captor.capture()); + assertEquals("biz-1", captor.getValue().getId()); + assertEquals(5, captor.getValue().getImplCount()); + assertEquals(2, captor.getValue().getClosedCount()); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakServiceImplTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakServiceImplTest.java new file mode 100644 index 0000000..62af766 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/xispeak/service/impl/BgXiSpeakServiceImplTest.java @@ -0,0 +1,345 @@ +package org.jeecg.modules.supervision.xispeak.service.impl; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.runtime.ProcessInstanceQuery; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.modules.extbpm.process.entity.ExtActTaskCc; +import org.jeecg.modules.extbpm.process.service.IExtActTaskCcService; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeakFeedback; +import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.supervision.xispeak.mapper.BgXiSpeakMapper; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakFeedbackService; +import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; +import org.jeecg.modules.taskapprovalopinion.entity.TaskApprovalOpinion; +import org.jeecg.modules.taskapprovalopinion.service.ITaskApprovalOpinionService; +import org.jeecg.modules.tasktask.entity.TaskTask; +import org.jeecg.modules.tasktask.service.impl.TaskTaskServiceImpl; +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 org.springframework.test.util.ReflectionTestUtils; + +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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * BgXiSpeakServiceImpl 测试(树节点 CRUD 规则 + 流程清理 + 审批保存)。 + */ +@ExtendWith(MockitoExtension.class) +class BgXiSpeakServiceImplTest { + + @Mock private TaskTaskServiceImpl taskTaskServiceImpl; + @Mock private RuntimeService runtimeService; + @Mock private HistoryService historyService; + @Mock private ITaskApprovalOpinionService taskApprovalOpinionService; + @Mock private IExtActTaskCcService extActTaskCcService; + @Mock private IBgXiSpeakFeedbackService bgXiSpeakFeedbackService; + @Mock private BgXiSpeakMapper baseMapper; + @Mock private ProcessInstanceQuery processInstanceQuery; + + private BgXiSpeakServiceImpl service; + + @BeforeEach + void setUp() { + service = new BgXiSpeakServiceImpl(taskTaskServiceImpl, runtimeService, historyService, + taskApprovalOpinionService, extActTaskCcService, bgXiSpeakFeedbackService); + ReflectionTestUtils.setField(service, "baseMapper", baseMapper); + } + + // ==================== addBgXiSpeak 树节点规则 ==================== + + @Nested + class AddNode { + + @Test + void should_set_root_values_when_pid_empty() { + BgXiSpeak node = new BgXiSpeak(); + service.addBgXiSpeak(node); + assertEquals(IBgXiSpeakService.NOCHILD, node.getHasChild()); + assertEquals(IBgXiSpeakService.ROOT_PID_VALUE, node.getPid()); + assertEquals(0, node.getTreeDepth()); + assertEquals("1", node.getBpmStatus()); + verify(baseMapper).insert(node); + } + + @Test + void should_mark_parent_has_child_and_increment_depth() { + BgXiSpeak parent = new BgXiSpeak(); + parent.setPid(IBgXiSpeakService.ROOT_PID_VALUE); + parent.setHasChild(IBgXiSpeakService.NOCHILD); + parent.setTreeDepth(1); + when(baseMapper.selectById("p-1")).thenReturn(parent); + + BgXiSpeak node = new BgXiSpeak(); + node.setPid("p-1"); + service.addBgXiSpeak(node); + + assertEquals("1", parent.getHasChild()); + verify(baseMapper).updateById(parent); + assertEquals(2, node.getTreeDepth()); + verify(baseMapper).insert(node); + } + + @Test + void should_set_depth_zero_when_parent_missing() { + when(baseMapper.selectById("p-1")).thenReturn(null); + BgXiSpeak node = new BgXiSpeak(); + node.setPid("p-1"); + service.addBgXiSpeak(node); + assertEquals(0, node.getTreeDepth()); + verify(baseMapper).insert(node); + } + } + + // ==================== updateBgXiSpeak ==================== + + @Nested + class UpdateNode { + + @Test + void should_throw_when_entity_not_found() { + BgXiSpeak node = new BgXiSpeak(); + node.setId("n-1"); + when(baseMapper.selectById("n-1")).thenReturn(null); + assertThrows(JeecgBootException.class, () -> service.updateBgXiSpeak(node)); + } + + @Test + void should_update_new_parent_status_when_pid_changed() { + BgXiSpeak old = new BgXiSpeak(); + old.setId("n-1"); + old.setPid("old-p"); + when(baseMapper.selectById("n-1")).thenReturn(old); + // 旧父节点无其他子节点 → 置 NOCHILD + when(baseMapper.selectCount(any(Wrapper.class))).thenReturn(1L); + + BgXiSpeak node = new BgXiSpeak(); + node.setId("n-1"); + node.setPid("new-p"); + service.updateBgXiSpeak(node); + + verify(baseMapper).updateTreeNodeStatus("old-p", IBgXiSpeakService.NOCHILD); + verify(baseMapper).updateTreeNodeStatus("new-p", IBgXiSpeakService.HASCHILD); + verify(baseMapper).updateById(node); + } + + @Test + void should_not_touch_parents_when_pid_unchanged() { + BgXiSpeak old = new BgXiSpeak(); + old.setId("n-1"); + old.setPid("p-1"); + when(baseMapper.selectById("n-1")).thenReturn(old); + + BgXiSpeak node = new BgXiSpeak(); + node.setId("n-1"); + node.setPid("p-1"); + service.updateBgXiSpeak(node); + + verify(baseMapper, never()).updateTreeNodeStatus(any(), any()); + verify(baseMapper).updateById(node); + } + } + + // ==================== deleteBgXiSpeak ==================== + + @Nested + class DeleteNode { + + @Test + void should_delete_single_node_and_cleanup() { + BgXiSpeak node = new BgXiSpeak(); + node.setId("n-1"); + node.setPid("p-1"); + when(baseMapper.selectById("n-1")).thenReturn(node); + when(baseMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + // 无关联 task_task:cleanup 只删反馈子表 + when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of()); + + service.deleteBgXiSpeak("n-1"); + + verify(baseMapper).updateTreeNodeStatus("p-1", IBgXiSpeakService.NOCHILD); + verify(baseMapper).deleteById("n-1"); + verify(bgXiSpeakFeedbackService).remove(any(Wrapper.class)); + } + + @Test + void should_throw_when_single_node_not_found() { + when(baseMapper.selectById("n-1")).thenReturn(null); + assertThrows(JeecgBootException.class, () -> service.deleteBgXiSpeak("n-1")); + } + + @Test + void should_delete_tree_with_children() { + // n-1 有两个子节点 n-2/n-3:递归收集后批量删除 + BgXiSpeak n1 = new BgXiSpeak(); + n1.setId("n-1"); + n1.setPid("p-1"); + BgXiSpeak n2 = new BgXiSpeak(); + n2.setId("n-2"); + n2.setPid("n-1"); + BgXiSpeak n3 = new BgXiSpeak(); + n3.setId("n-3"); + n3.setPid("n-1"); + when(baseMapper.selectById("n-1")).thenReturn(n1); + when(baseMapper.selectById("n-2")).thenReturn(n2); + when(baseMapper.selectById("n-3")).thenReturn(n3); + // selectList 按调用序列:查 n-1 子→[n2,n3];递归查 n-2/n-3 子→空(终止递归);查 p-1 其他子→空 + when(baseMapper.selectList(any(Wrapper.class))) + .thenReturn(List.of(n2, n3), List.of(), List.of(), List.of()); + + service.deleteBgXiSpeak("n-1"); + + verify(baseMapper).deleteBatchIds(any()); + } + } + + // ==================== withDrawXiSpeak ==================== + + @Nested + class WithDraw { + + @Test + void should_return_false_when_no_related_task() { + when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of()); + assertFalse(service.withDrawXiSpeak("biz-1")); + } + + @Test + void should_terminate_process_and_cleanup() { + TaskTask tt = new TaskTask(); + tt.setBusinessId("biz-1"); + tt.setProcessInstId("proc-1"); + when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of(tt)); + when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery); + when(processInstanceQuery.processInstanceId("proc-1")).thenReturn(processInstanceQuery); + when(processInstanceQuery.count()).thenReturn(1L); + + assertTrue(service.withDrawXiSpeak("biz-1")); + + verify(runtimeService).deleteProcessInstance(eq("proc-1"), any()); + verify(taskApprovalOpinionService).remove(any(Wrapper.class)); + verify(extActTaskCcService).remove(any(Wrapper.class)); + verify(taskTaskServiceImpl).remove(any(Wrapper.class)); + } + } + + // ==================== checkAndMarkCompleted ==================== + + @Nested + class CheckCompleted { + + @Test + void should_return_when_process_inst_id_empty() { + service.checkAndMarkCompletedByProcessInstId(""); + verify(taskTaskServiceImpl, never()).getOne(any(Wrapper.class), anyBoolean()); + } + + @Test + void should_not_mark_when_any_process_running() { + when(taskTaskServiceImpl.getOne(any(Wrapper.class), anyBoolean())).thenReturn(taskWithProc("biz-1", "proc-1")); + when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of(taskWithProc("biz-1", "proc-1"))); + when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery); + when(processInstanceQuery.processInstanceId("proc-1")).thenReturn(processInstanceQuery); + when(processInstanceQuery.count()).thenReturn(1L); + + service.checkAndMarkCompletedByProcessInstId("proc-1"); + + verify(baseMapper, never()).updateById(any()); + } + + @Test + void should_mark_completed_when_all_processes_finished() { + when(taskTaskServiceImpl.getOne(any(Wrapper.class), anyBoolean())).thenReturn(taskWithProc("biz-1", "proc-1")); + when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of(taskWithProc("biz-1", "proc-1"))); + when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery); + when(processInstanceQuery.processInstanceId("proc-1")).thenReturn(processInstanceQuery); + when(processInstanceQuery.count()).thenReturn(0L); + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(baseMapper.selectById("biz-1")).thenReturn(entity); + + service.checkAndMarkCompletedByProcessInstId("proc-1"); + + assertEquals(1, entity.getCompletionStatus()); + verify(baseMapper).updateById(entity); + } + + private TaskTask taskWithProc(String biz, String proc) { + TaskTask tt = new TaskTask(); + tt.setBusinessId(biz); + tt.setProcessInstId(proc); + return tt; + } + } + + // ==================== saveSubApprove ==================== + + @Nested + class SaveSubApprove { + + @Test + void should_throw_when_main_not_exists() { + when(baseMapper.selectById("biz-1")).thenReturn(null); + assertThrows(JeecgBootException.class, () -> service.saveSubApprove("biz-1", "dept-1", 1, "w1", null)); + } + + @Test + void should_store_impl_worker_when_is_end_one() { + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(baseMapper.selectById("biz-1")).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() { + BgXiSpeak entity = new BgXiSpeak(); + entity.setId("biz-1"); + when(baseMapper.selectById("biz-1")).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); + } + } + + // ==================== saveBpmFormAndSyncVariable ==================== + + @Nested + class SaveBpmForm { + + @Test + void should_update_form_and_sync_variable() { + BgXiSpeak form = new BgXiSpeak(); + service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}"); + verify(baseMapper).updateById(form); + verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}"); + } + } +}