!133 fix(supervision): 修正 InspectCloseoutFlowTest 角色参数匹配实际调用

* fix(supervision): 修正 InspectCloseoutFlowTest 角色参数匹配实际调用
* test(fixcontact): 新增流程表达式/监听器/Service 单测,fixcontact 可测代码接近全覆盖
* refactor(fixcontact): 提炼上两级解析与部门ID清洗纯函数,监听器依赖注入化
* chore(bpm): 安装 JaCoCo 覆盖率插件并修复构建链(补齐 base-core 依赖、排除环境依赖测试)
This commit is contained in:
wsm
2026-08-18 08:04:26 +00:00
parent aea4a0e312
commit 4e9a57651b
17 changed files with 1414 additions and 34 deletions
@@ -31,6 +31,11 @@
</repositories> </repositories>
<dependencies> <dependencies>
<!-- 基础核心(代码直接使用 base-core 的 Result 等) -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- system单体 api--> <!-- system单体 api-->
<dependency> <dependency>
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
@@ -189,4 +194,20 @@
</dependencies> </dependencies>
<build>
<plugins>
<!-- 排除 airag/test 包:历史遗留的调试型测试(TestFlows 连本地 7008 AI 服务、TestFileParse 依赖测试文件),
不属于可自动化单测,避免 mvn test 全量时因环境缺失而失败;IDE 中仍可手动单独运行 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/airag/test/**</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>
@@ -57,6 +57,11 @@
</repositories> </repositories>
<dependencies> <dependencies>
<!-- 基础核心(代码直接使用 base-core 的 Result/SymbolConstant/QueryGenerator 等) -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- 单体system api --> <!-- 单体system api -->
<dependency> <dependency>
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
+16
View File
@@ -30,5 +30,21 @@
<artifactId>jeecg-system-biz</artifactId> <artifactId>jeecg-system-biz</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<!-- flowregression 是 E2E 回归测试(HTTP 驱动真实后端 localhost:8080),
后端未启动时必然失败,从全量 mvn test 排除;需要时启动后端或 -Dtest=*RegressionTest 单独跑 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/flowregression/**</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>
@@ -258,7 +258,7 @@ public class FixContactFlow {
execution.getProcessInstanceId()); execution.getProcessInstanceId());
return null; return null;
} }
return deptVar.toString().replace("[", "").replace("]", "").trim(); return FixContactFlowSupport.cleanDeptId(deptVar);
} }
// ${fixContactFlow.getCurrentDeptLeaderList(execution, deptId)} // ${fixContactFlow.getCurrentDeptLeaderList(execution, deptId)}
@@ -357,14 +357,7 @@ public class FixContactFlow {
if (value == null) { if (value == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
String str = value.toString(); return FixContactFlowSupport.splitToTrimmedList(value.toString());
if (StringUtils.isBlank(str)) {
return Collections.emptyList();
}
return Arrays.stream(str.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
} }
// ${fixContactFlow.getVarAsListCount(execution, varName)} // ${fixContactFlow.getVarAsListCount(execution, varName)}
@@ -374,13 +367,7 @@ public class FixContactFlow {
// ${fixContactFlow.getListSize(data)} // ${fixContactFlow.getListSize(data)}
public int getListSize(String data) { public int getListSize(String data) {
if (StringUtils.isEmpty(data)) { return FixContactFlowSupport.countTokens(data);
return 0;
}
return (int) Arrays.stream(data.split(","))
.map(String::trim)
.filter(StringUtils::isNotEmpty)
.count();
} }
// ${fixContactFlow.getIsNeedLeaderApprove(execution)} // ${fixContactFlow.getIsNeedLeaderApprove(execution)}
@@ -619,10 +606,7 @@ public class FixContactFlow {
if (StringUtils.isBlank(rawValue)) { if (StringUtils.isBlank(rawValue)) {
return Collections.emptyList(); return Collections.emptyList();
} }
List<String> result = Arrays.stream(rawValue.split(",")) List<String> result = FixContactFlowSupport.splitToTrimmedList(rawValue);
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
log.info("【定点联系-流程表达式】{} 解析结果: {}", logLabel, result); log.info("【定点联系-流程表达式】{} 解析结果: {}", logLabel, result);
return result; return result;
} catch (Exception e) { } catch (Exception e) {
@@ -0,0 +1,43 @@
package org.jeecg.modules.supervision.fixcontact.flow;
import org.apache.commons.lang.StringUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 定点联系流程表达式纯逻辑支撑(无外部依赖,可独立测试)。
*
* 规则(提炼自 FixContactFlow 内联重复逻辑):
* - splitToTrimmedList:逗号分割 → trim → 过滤空白元素
* - countTokens:分割后有效元素个数
* - cleanDeptId:流程变量部门 ID 清洗(去 [] 与首尾空白)
*/
public final class FixContactFlowSupport {
private FixContactFlowSupport() {
}
public static List<String> splitToTrimmedList(String raw) {
if (StringUtils.isBlank(raw)) {
return Collections.emptyList();
}
return Arrays.stream(raw.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
}
public static int countTokens(String raw) {
return splitToTrimmedList(raw).size();
}
public static String cleanDeptId(Object raw) {
if (raw == null) {
return null;
}
return raw.toString().replace("[", "").replace("]", "").trim();
}
}
@@ -0,0 +1,32 @@
package org.jeecg.modules.supervision.fixcontact.listener;
import org.apache.commons.lang3.StringUtils;
/**
* 定点联系流程审批辅助逻辑(纯函数,无外部依赖,可独立测试)。
*
* 规则:部门领导审批后,领导变量写入"上两级"执行实例;
* 存在第二层父级存第二层(大会签容器/外层作用域),否则存第一层。
*/
public final class FixContactApprovalHelper {
private FixContactApprovalHelper() {
}
/**
* 解析领导变量应写入的目标执行实例 ID。
*
* @param parentId 第一层父级执行实例 ID
* @param grandParentId 第二层父级执行实例 ID(可能为空)
* @return 有第二层返回 grandParentId;否则返回 parentIdparentId 为空返回 null(调用方跳过)
*/
public static String resolveTargetExecutionId(String parentId, String grandParentId) {
if (StringUtils.isBlank(parentId)) {
return null;
}
if (StringUtils.isNotBlank(grandParentId)) {
return grandParentId;
}
return parentId;
}
}
@@ -1,13 +1,14 @@
package org.jeecg.modules.supervision.fixcontact.listener; package org.jeecg.modules.supervision.fixcontact.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.flowable.engine.RuntimeService; import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.TaskListener; import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.Execution;
import org.flowable.task.service.delegate.DelegateTask; import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@@ -16,16 +17,13 @@ import org.springframework.stereotype.Component;
*/ */
@Slf4j @Slf4j
@Component("FixContactDeptLeaderApproveListener") @Component("FixContactDeptLeaderApproveListener")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class FixContactDeptLeaderApproveListener implements TaskListener { public class FixContactDeptLeaderApproveListener implements TaskListener {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final String VARIABLE_NAME = FixContactConstant.FlowVarName.TEM_IMPL_LEADER; private static final String VARIABLE_NAME = FixContactConstant.FlowVarName.TEM_IMPL_LEADER;
private static RuntimeService runtimeService; private final RuntimeService runtimeService;
static {
runtimeService = SpringContextUtils.getBean(RuntimeService.class);
}
@Override @Override
public void notify(DelegateTask delegateTask) { public void notify(DelegateTask delegateTask) {
@@ -64,9 +62,8 @@ public class FixContactDeptLeaderApproveListener implements TaskListener {
if (parentExecution == null) { if (parentExecution == null) {
return; return;
} }
String grandParentId = parentExecution.getParentId(); // 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层(规则见 FixContactApprovalHelper
// 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层 String targetId = FixContactApprovalHelper.resolveTargetExecutionId(parentId, parentExecution.getParentId());
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
try { try {
runtimeService.setVariableLocal(targetId, VARIABLE_NAME, assignee); runtimeService.setVariableLocal(targetId, VARIABLE_NAME, assignee);
log.info("【定点联系-部门领导审批监听器】已存入上两级局部变量, targetExecutionId: {}, variableName: {}, assignee: {}", log.info("【定点联系-部门领导审批监听器】已存入上两级局部变量, targetExecutionId: {}, variableName: {}, assignee: {}",
@@ -8,6 +8,7 @@ import org.flowable.engine.delegate.TaskListener;
import org.flowable.task.service.delegate.DelegateTask; import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.flow.FixContactFlowSupport;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service; import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
@@ -74,7 +75,7 @@ public class FixContactWorkerApproveListener implements TaskListener {
log.warn("【定点联系-经办人会签监听器】未获取到子流程当前部门ID,跳过记录经办人, taskId: {}", delegateTask.getId()); log.warn("【定点联系-经办人会签监听器】未获取到子流程当前部门ID,跳过记录经办人, taskId: {}", delegateTask.getId());
return; return;
} }
String deptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); String deptId = FixContactFlowSupport.cleanDeptId(rawDeptVar);
FixedContact20260730 fixedContact = fixedContact20260730Service.getByIdForUpdate(rawBusinessKey.toString()); FixedContact20260730 fixedContact = fixedContact20260730Service.getByIdForUpdate(rawBusinessKey.toString());
if (fixedContact == null) { if (fixedContact == null) {
@@ -124,7 +124,7 @@ class InspectCloseoutFlowTest {
@Test @Test
void getDqDeptLdUserIdList_shouldReturnUserList() { void getDqDeptLdUserIdList_shouldReturnUserList() {
List<String> users = List.of("dq_user1"); List<String> users = List.of("dq_user1");
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker")) when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld"))
.thenReturn(users); .thenReturn(users);
assertEquals(users, flow.getDqDeptLdUserIdList()); assertEquals(users, flow.getDqDeptLdUserIdList());
@@ -132,7 +132,7 @@ class InspectCloseoutFlowTest {
@Test @Test
void getDqDeptLdUserIdListLength_shouldReturnCount() { void getDqDeptLdUserIdListLength_shouldReturnCount() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker")) when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld"))
.thenReturn(List.of("user1", "user2")); .thenReturn(List.of("user1", "user2"));
assertEquals(2, flow.getDqDeptLdUserIdListLength()); assertEquals(2, flow.getDqDeptLdUserIdListLength());
@@ -140,7 +140,7 @@ class InspectCloseoutFlowTest {
@Test @Test
void getDqDeptLdUserIdListLength_shouldReturnZeroWhenEmpty() { void getDqDeptLdUserIdListLength_shouldReturnZeroWhenEmpty() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker")) when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld"))
.thenReturn(Collections.emptyList()); .thenReturn(Collections.emptyList());
assertEquals(0, flow.getDqDeptLdUserIdListLength()); assertEquals(0, flow.getDqDeptLdUserIdListLength());
@@ -0,0 +1,103 @@
package org.jeecg.modules.supervision.fixcontact.flow;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* FixContactFlowSupport 纯函数规则测试(TDD 红-绿-重构,阶段 A 示范)。
*
* 业务规则(来自 FixContactFlow 内联逻辑,3 处重复的逗号分割 + 部门 ID 清洗):
* - splitToTrimmedList:逗号分割 → 去首尾空白 → 过滤空白元素(null/空 → 空列表)
* - countTokens:分割后元素个数(等价于 splitToTrimmedList().size()
* - cleanDeptId:执行实例部门变量清洗(去掉 [] 与首尾空白,与 XiSpeak 监听器同款规则)
*/
class FixContactFlowSupportTest {
// ============ splitToTrimmedList:正常路径 ============
@Test
void should_split_multi_tokens() {
assertEquals(List.of("a", "b", "c"), FixContactFlowSupport.splitToTrimmedList("a,b,c"));
}
@Test
void should_trim_tokens_with_spaces() {
// 逗号前后有空格:逐 token trim
assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList(" a , b "));
}
@Test
void should_return_single_token() {
assertEquals(List.of("a"), FixContactFlowSupport.splitToTrimmedList("a"));
}
// ============ splitToTrimmedList:边界路径 ============
@Test
void should_filter_empty_tokens() {
// 空元素(连续逗号/全空白元素)被过滤
assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList("a,,b"));
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " ", " , , "})
void should_return_empty_list_when_no_valid_token(String raw) {
assertEquals(List.of(), FixContactFlowSupport.splitToTrimmedList(raw));
}
// ============ countTokens:正常 + 边界 ============
@Test
void should_count_tokens() {
assertEquals(3, FixContactFlowSupport.countTokens("a,b,c"));
}
@Test
void should_count_trimmed_tokens() {
assertEquals(2, FixContactFlowSupport.countTokens(" a , b "));
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " , , "})
void should_return_zero_when_no_valid_token(String raw) {
assertEquals(0, FixContactFlowSupport.countTokens(raw));
}
// ============ cleanDeptId:正常 + 边界 ============
@Test
void should_remove_square_brackets() {
assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("[dept-1]"));
}
@Test
void should_remove_brackets_and_trim() {
// 多值列表变量场景:整体去括号 + trim
assertEquals("dept-1, dept-2", FixContactFlowSupport.cleanDeptId("[dept-1, dept-2]"));
}
@Test
void should_keep_plain_id_unchanged() {
assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("dept-1"));
}
@Test
void should_handle_non_string_object() {
// 流程变量可能是任意对象,toString 后处理
assertEquals("123", FixContactFlowSupport.cleanDeptId(123L));
}
@Test
void should_return_null_when_raw_null() {
assertNull(FixContactFlowSupport.cleanDeptId(null));
}
}
@@ -0,0 +1,603 @@
package org.jeecg.modules.supervision.fixcontact.flow;
import com.alibaba.fastjson.JSONObject;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContactFeedback20260730Service;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
* FixContactFlow 流程表达式方法测试(mock 外部依赖:service/iSysBaseAPI/runtimeService)。
* 覆盖:业务字段读取、部门角色查询、办结状态、当前部门、反馈、流程变量、JSON 解析、待办人汇总。
*/
@ExtendWith(MockitoExtension.class)
class FixContactFlowTest {
@Mock private IFixedContact20260730Service fixedContactService;
@Mock private IFixedContactFeedback20260730Service feedbackService;
@Mock private ISysBaseAPI iSysBaseAPI;
@Mock private RuntimeService runtimeService;
@Mock private DelegateExecution execution;
private FixContactFlow flow;
@BeforeEach
void setUp() {
flow = new FixContactFlow(fixedContactService, feedbackService, null, iSysBaseAPI, runtimeService);
}
/** stub getFixedContact 读取业务表单的公共链路 */
private void mockEntity(FixedContact20260730 entity) {
when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
when(fixedContactService.getById("biz-1")).thenReturn(entity);
}
// ==================== 业务字段读取 ====================
@Nested
class BusinessFieldRead {
private FixedContact20260730 fullEntity() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setContactLeader("sld_01");
e.setRelatedLeader("sz");
e.setApplicant("admin");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
e.setIsNeedAppro("1");
e.setSupDeptleaderid("bg_ld_01");
e.setContactDept("外单位");
return e;
}
@Test
void should_return_contactLeader_when_entity_exists() {
mockEntity(fullEntity());
assertEquals("sld_01", flow.getContactLeader(execution));
}
@Test
void should_return_null_when_entity_not_found() {
mockEntity(null);
assertNull(flow.getContactLeader(execution));
}
@Test
void should_return_relatedLeader() {
mockEntity(fullEntity());
assertEquals("sz", flow.getRelatedLeader(execution));
}
@Test
void should_return_applicant() {
mockEntity(fullEntity());
assertEquals("admin", flow.getApplicant(execution));
}
@Test
void should_return_hostDept() {
mockEntity(fullEntity());
assertEquals("dept-host", flow.getHostDept(execution));
}
@Test
void should_return_coDept() {
mockEntity(fullEntity());
assertEquals("dept-co", flow.getCoDept(execution));
}
@Test
void should_return_isNeedAppro() {
mockEntity(fullEntity());
assertEquals("1", flow.getIsNeedBgLdApprove(execution));
}
@Test
void should_return_supDeptLeaderId() {
mockEntity(fullEntity());
assertEquals("bg_ld_01", flow.getBgLd(execution));
}
@Test
void should_return_contactDept() {
mockEntity(fullEntity());
assertEquals("外单位", flow.getContactDept(execution));
}
@Test
void should_return_null_when_contactLeader_blank() {
FixedContact20260730 e = fullEntity();
e.setContactLeader(" ");
mockEntity(e);
assertEquals(" ", flow.getContactLeader(execution));
}
}
// ==================== 部门+角色查询 ====================
@Nested
class DeptRoleQuery {
@Test
void should_return_office_leader_list() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi(
FixContactConstant.DeptId.BG_DEPT_ID, FixContactConstant.RoleCode.SLD_ROLE_ID))
.thenReturn(List.of("sld_01", "sld_02"));
assertEquals(List.of("sld_01", "sld_02"), flow.getOfficeLeaderList(execution));
}
@Test
void should_return_office_leader_list_length() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi(
FixContactConstant.DeptId.BG_DEPT_ID, FixContactConstant.RoleCode.SLD_ROLE_ID))
.thenReturn(List.of("sld_01", "sld_02"));
assertEquals(2, flow.getOfficeLeaderListLength(execution));
}
@Test
void should_return_users_by_dept_and_role() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-100", "ld"))
.thenReturn(List.of("u1", "u2"));
assertEquals(List.of("u1", "u2"), flow.getUsersByDeptAndRole("dept-100", "ld"));
}
@Test
void should_return_count_zero_when_no_users() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-100", "ld"))
.thenReturn(List.of());
assertEquals(0, flow.getUsersByDeptAndRoleCount("dept-100", "ld"));
}
@Test
void should_return_host_dept_leader_list() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
mockEntity(e);
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld"))
.thenReturn(List.of("leader1", "leader2"));
assertEquals(List.of("leader1", "leader2"), flow.getHostDeptLeaderList(execution));
}
@Test
void should_return_empty_when_host_dept_has_no_leader() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
mockEntity(e);
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld"))
.thenReturn(List.of());
assertEquals(List.of(), flow.getHostDeptLeaderList(execution));
}
@Test
void should_throw_when_host_dept_blank() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
mockEntity(e);
assertThrows(IllegalStateException.class, () -> flow.getHostDeptLeaderList(execution));
}
@Test
void should_throw_when_entity_not_found_for_host_leader() {
mockEntity(null);
assertThrows(IllegalStateException.class, () -> flow.getHostDeptLeaderList(execution));
}
}
// ==================== 办结状态 ====================
@Nested
class IsEndRead {
@Test
void should_return_null_when_dept_id_blank() {
// deptId 空白直接短路返回,不触达业务表查询
assertNull(flow.getIsEnd(execution, " "));
}
@Test
void should_return_null_when_entity_not_found() {
mockEntity(null);
assertNull(flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_null_when_approve_info_empty() {
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
mockEntity(e);
assertNull(flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_isEnd_one_when_finished() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(1);
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(map);
mockEntity(e);
assertEquals("1", flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_isEnd_zero_when_unfinished() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(0);
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(map);
mockEntity(e);
assertEquals("0", flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_null_when_detail_missing() {
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
mockEntity(e);
assertNull(flow.getIsEnd(execution, "no-such-dept"));
}
}
// ==================== 当前部门 ====================
@Nested
class CurrentDept {
@Test
void should_return_null_when_execution_null() {
assertNull(flow.getCurrentDeptId(null));
}
@Test
void should_clean_local_dept_var() {
when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn("[dept-1]");
assertEquals("dept-1", flow.getCurrentDeptId(execution));
}
@Test
void should_fallback_to_global_var() {
when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
when(execution.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn("dept-2");
assertEquals("dept-2", flow.getCurrentDeptId(execution));
}
@Test
void should_return_null_when_no_dept_var() {
when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
when(execution.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
assertNull(flow.getCurrentDeptId(execution));
}
}
// ==================== 反馈子表 ====================
@Nested
class Feedback {
private FixedContactFeedback20260730 feedback(String user, String dept, String finishStatus) {
FixedContactFeedback20260730 f = new FixedContactFeedback20260730();
f.setFeedbackUser(user);
f.setFeedbackDept(dept);
f.setFinishStatus(finishStatus);
return f;
}
private void mockEntityWithId() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
mockEntity(e);
}
@Test
void should_return_empty_when_entity_not_found() {
mockEntity(null);
assertEquals(List.of(), flow.getFeedbackList(execution));
}
@Test
void should_return_feedback_list() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of(feedback("u1", "d1", "1")));
assertEquals(1, flow.getFeedbackList(execution).size());
}
@Test
void should_return_null_latest_user_when_no_feedback() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of());
assertNull(flow.getLatestFeedbackUser(execution));
}
@Test
void should_return_latest_feedback_user() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1")));
assertEquals("u2", flow.getLatestFeedbackUser(execution));
}
@Test
void should_return_latest_feedback_dept() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1")));
assertEquals("d2", flow.getLatestFeedbackDept(execution));
}
@Test
void should_return_false_when_no_feedback() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of());
assertFalse(flow.isAllFeedbackFinished(execution));
}
@Test
void should_return_true_when_all_finished() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1")));
assertTrue(flow.isAllFeedbackFinished(execution));
}
@Test
void should_return_false_when_any_feedback_unfinished() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", " ")));
assertFalse(flow.isAllFeedbackFinished(execution));
}
}
// ==================== 流程变量 ====================
@Nested
class ProcessVariable {
@Test
void should_return_var_value() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", "name")).thenReturn("val");
assertEquals("val", flow.getVar(execution, "name"));
org.mockito.Mockito.verify(execution).getProcessInstanceId();
}
@Test
void should_return_null_when_var_absent() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", "name")).thenReturn(null);
assertNull(flow.getVar(execution, "name"));
}
@Test
void should_default_zero_when_need_leader_approve_blank() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.IS_NEED_LEADER_APPROVE))
.thenReturn(null);
assertEquals("0", flow.getIsNeedLeaderApprove(execution));
}
@Test
void should_return_need_leader_approve_value() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.IS_NEED_LEADER_APPROVE))
.thenReturn("1");
assertEquals("1", flow.getIsNeedLeaderApprove(execution));
}
@Test
void should_split_var_as_list() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.LEADER_APPROVE_USER))
.thenReturn("sld_01, sld_02");
assertEquals(List.of("sld_01", "sld_02"), flow.getLeaderApproveUserList(execution));
}
@Test
void should_return_empty_list_when_var_null() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.LEADER_APPROVE_USER))
.thenReturn(null);
assertEquals(List.of(), flow.getLeaderApproveUserList(execution));
}
}
// ==================== JSON 解析 ====================
@Nested
class JsonParse {
@Test
void should_parse_from_json_object() {
JSONObject data = new JSONObject().fluentPut("contactLeader", "sld_01");
assertEquals("sld_01", flow.getContactLeaderFromJson(data));
}
@Test
void should_parse_from_json_string() {
assertEquals("sld_01", flow.getContactLeaderFromJson("{\"contactLeader\":\"sld_01\"}"));
}
@Test
void should_return_null_when_json_null() {
assertNull(flow.getContactLeaderFromJson(null));
}
@Test
void should_return_null_when_invalid_json() {
assertNull(flow.getContactLeaderFromJson("not-a-json"));
}
@Test
void should_return_host_dept_leader_list_from_json() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld"))
.thenReturn(List.of("leader1"));
assertEquals(List.of("leader1"), flow.getHostDeptLeaderListFromJson(
new JSONObject().fluentPut("hostDept", "dept-host")));
}
@Test
void should_return_empty_when_host_dept_blank_in_json() {
assertEquals(List.of(), flow.getHostDeptLeaderListFromJson(
new JSONObject().fluentPut("hostDept", " ")));
}
}
// ==================== 部门列表组装 ====================
@Nested
class DeptIdList {
@Test
void should_compose_host_and_co_dept() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
mockEntity(e);
assertEquals(List.of("dept-host", "dept-co"), flow.getDeptIdList(execution));
}
@Test
void should_compose_only_host_dept() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
mockEntity(e);
assertEquals(List.of("dept-host"), flow.getDeptIdList(execution));
}
@Test
void should_return_empty_when_entity_not_found() {
mockEntity(null);
assertEquals(List.of(), flow.getDeptIdList(execution));
}
}
// ==================== 待办人汇总 ====================
@Nested
class TodoUser {
private FixedContact20260730 entityWithApproveInfo() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
mockEntity(e);
return e;
}
@Test
void should_return_empty_when_approve_info_empty() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
e.setApproveInfo(new DeptApproveDetailMap());
mockEntity(e);
assertEquals(List.of(), flow.getTodoUserList(execution));
}
@Test
void should_use_impl_user_name_list_when_not_finished() {
// isEnd=0:多选经办人 implUserNameList
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(0);
detail.setImplUserNameList(List.of("worker1", "worker2"));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-host", detail);
entityWithApproveInfo().setApproveInfo(map);
assertEquals(List.of("worker1", "worker2"), flow.getTodoUserList(execution));
}
@Test
void should_use_impl_worker_when_finished() {
// isEnd=1:单选经办人 implWorker
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(1);
detail.setImplWorker("worker1");
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-host", detail);
entityWithApproveInfo().setApproveInfo(map);
assertEquals(List.of("worker1"), flow.getTodoUserList(execution));
}
@Test
void should_deduplicate_users_across_depts() {
// 两部门分配同一经办人:LinkedHashSet 去重保序
DeptApproveDetail d1 = new DeptApproveDetail();
d1.setIsEnd(0);
d1.setImplUserNameList(List.of("shared"));
DeptApproveDetail d2 = new DeptApproveDetail();
d2.setIsEnd(0);
d2.setImplUserNameList(List.of("shared", "worker2"));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-host", d1);
map.put("dept-co", d2);
entityWithApproveInfo().setApproveInfo(map);
assertEquals(List.of("shared", "worker2"), flow.getTodoUserList(execution));
}
}
// ==================== 当前部门经办人 ====================
@Nested
class CurrentDeptWorker {
@Test
void should_return_empty_when_detail_null() {
assertEquals(List.of(), flow.getCurrentDeptWorkerList(execution, "dept-1"));
}
@Test
void should_return_impl_user_name_list() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplUserNameList(List.of("w1", "w2"));
when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
e.getApproveInfo().put("dept-1", detail);
when(fixedContactService.getById("biz-1")).thenReturn(e);
assertEquals(List.of("w1", "w2"), flow.getCurrentDeptWorkerList(execution, "dept-1"));
}
@Test
void should_return_singleton_impl_worker() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplWorker("w1");
when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
e.getApproveInfo().put("dept-1", detail);
when(fixedContactService.getById("biz-1")).thenReturn(e);
assertEquals(List.of("w1"), flow.getCurrentDeptWorkerList(execution, "dept-1"));
}
}
}
@@ -0,0 +1,58 @@
package org.jeecg.modules.supervision.fixcontact.listener;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* FixContactApprovalHelper.resolveTargetExecutionId 规则测试(TDD 红-绿-重构 演示)。
*
* 业务规则(来自 FixContactDeptLeaderApproveListener 第 53-69 行):
* 部门领导审批后,领导变量要写入"上两级"执行实例:
* - 存在第二层父级(大会签容器/外层作用域)→ 写入第二层 grandParentId
* - 不存在第二层父级 → 写入第一层 parentId
* - 第一层 parentId 为空 → 无目标,返回 null(调用方跳过写入)
*/
class FixContactApprovalHelperTest {
// ============ 正常路径 ============
@Test
void should_return_grandParent_when_both_levels_exist() {
// 两级父级都存在(子流程内会签容器场景)
String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", "grand-1");
assertEquals("grand-1", target);
}
@Test
void should_return_parent_when_no_grand_parent() {
// 只有第一层父级(无外层容器)
String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", null);
assertEquals("parent-1", target);
}
// ============ 边界路径 ============
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " "})
void should_return_parent_when_grand_parent_blank(String grandParentId) {
// 第二层为空串/空白串:视为不存在,退回第一层(原逻辑 isNotBlank 判定)
String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", grandParentId);
assertEquals("parent-1", target);
}
// ============ 异常路径 ============
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " "})
void should_return_null_when_parent_blank(String parentId) {
// 第一层父级都没有:无目标执行实例,返回 null 供调用方跳过
assertNull(FixContactApprovalHelper.resolveTargetExecutionId(parentId, "grand-1"));
}
}
@@ -0,0 +1,134 @@
package org.jeecg.modules.supervision.fixcontact.listener;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ExecutionQuery;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* FixContactDeptLeaderApproveListener 部门领导审批监听器测试(重构依赖注入后可测)。
* 规则:将 assignee 写入"上两级"执行实例局部变量——有第二层父级存第二层,否则存第一层。
*/
@ExtendWith(MockitoExtension.class)
class FixContactDeptLeaderApproveListenerTest {
@Mock private RuntimeService runtimeService;
@Mock private DelegateTask delegateTask;
@Mock private Execution currentExecution;
@Mock private Execution parentExecution;
@Mock private ExecutionQuery query;
@Mock private ExecutionQuery parentQuery;
private FixContactDeptLeaderApproveListener listener;
@BeforeEach
void setUp() {
listener = new FixContactDeptLeaderApproveListener(runtimeService);
}
/** 链式 stub:第一次 createExecutionQuery 查当前执行,第二次查父级执行 */
private void mockExecutionChain(String parentId, String grandParentId) {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query, parentQuery);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(parentId);
when(parentQuery.executionId(parentId)).thenReturn(parentQuery);
when(parentQuery.singleResult()).thenReturn(parentExecution);
when(parentExecution.getParentId()).thenReturn(grandParentId);
}
@Test
void should_return_when_task_null() {
listener.notify(null);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_execution_id_blank() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_current_execution_not_found() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(null);
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_parent_id_blank() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_parent_execution_not_found() {
// 单独 stub(不复用 mockExecutionChain,避免无用 stub
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query, parentQuery);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn("parent-1");
when(parentQuery.executionId("parent-1")).thenReturn(parentQuery);
when(parentQuery.singleResult()).thenReturn(null);
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_write_to_grand_parent_when_both_levels_exist() {
mockExecutionChain("parent-1", "grand-1");
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("grand-1", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1");
}
@Test
void should_write_to_parent_when_no_grand_parent() {
mockExecutionChain("parent-1", null);
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("parent-1", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1");
}
@Test
void should_not_throw_when_set_variable_fails() {
mockExecutionChain("parent-1", "grand-1");
doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1");
// 不抛异常即通过(异常被 catch)
listener.notify(delegateTask);
}
}
@@ -0,0 +1,179 @@
package org.jeecg.modules.supervision.fixcontact.listener;
import org.flowable.engine.RuntimeService;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* FixContactWorkerApproveListener 经办人会签监听器测试。
* 规则:写入子流程局部变量 tem_impl_worker + 记录经办人到 approveInfo 对应部门(去重)。
*/
@ExtendWith(MockitoExtension.class)
class FixContactWorkerApproveListenerTest {
@Mock private IFixedContact20260730Service fixedContactService;
@Mock private RuntimeService runtimeService;
@Mock private DelegateTask delegateTask;
private FixContactWorkerApproveListener listener;
@BeforeEach
void setUp() {
listener = new FixContactWorkerApproveListener(fixedContactService, runtimeService);
}
/** 正常办理链路:assignee + executionId + businessKey + deptVar 齐备 */
private void mockNormalTask(String deptVar) {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
when(delegateTask.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(deptVar);
}
@Test
void should_return_when_task_null() {
listener.notify(null);
verify(runtimeService, never()).setVariableLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_write_worker_local_var() {
mockNormalTask("dept-1");
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(new DeptApproveDetailMap());
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("exec-1", FixContactConstant.FlowVarName.TEM_IMPL_WORKER, "worker1");
verify(fixedContactService).updateById(entity);
}
@Test
void should_skip_record_when_business_key_missing() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn(null);
listener.notify(delegateTask);
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_skip_record_when_dept_var_missing() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
when(delegateTask.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
when(delegateTask.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
listener.notify(delegateTask);
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_skip_record_when_entity_not_found() {
mockNormalTask("dept-1");
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(null);
listener.notify(delegateTask);
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_append_worker_to_existing_detail() {
mockNormalTask("dept-1");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setDeptId("dept-1");
detail.setImplUserNameList(new ArrayList<>(List.of("old_worker")));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(map);
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals(List.of("old_worker", "worker1"), detail.getImplUserNameList());
verify(fixedContactService).updateById(entity);
}
@Test
void should_create_detail_when_dept_not_in_map() {
// approveInfo 为 null + dept 无 detail:新建 map + detail
mockNormalTask("dept-1");
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals(List.of("worker1"), entity.getApproveInfo().get("dept-1").getImplUserNameList());
verify(fixedContactService).updateById(entity);
}
@Test
void should_not_update_when_worker_already_in_list() {
mockNormalTask("dept-1");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setDeptId("dept-1");
detail.setImplUserNameList(new ArrayList<>(List.of("worker1")));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(map);
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals(1, detail.getImplUserNameList().size());
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_clean_dept_var_with_brackets() {
// 部门变量带 []:记录到清洗后的 dept key
mockNormalTask("[dept-1]");
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(new DeptApproveDetailMap());
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertTrue(entity.getApproveInfo().containsKey("dept-1"));
assertEquals("dept-1", entity.getApproveInfo().get("dept-1").getDeptId());
}
}
@@ -0,0 +1,178 @@
package org.jeecg.modules.supervision.fixcontact.service.impl;
import org.flowable.engine.RuntimeService;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730;
import org.jeecg.modules.supervision.fixcontact.mapper.FixedContact20260730Mapper;
import org.jeecg.modules.supervision.fixcontact.mapper.FixedContactFeedback20260730Mapper;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* FixedContact20260730ServiceImpl 测试(mock mapper + runtimeService)。
* 核心:saveSubApprove 部门审批保存规则(isEnd=1 存 implWorker / isEnd=0 存 implUserNameList+ 主从 CRUD。
*/
@ExtendWith(MockitoExtension.class)
class FixedContact20260730ServiceImplTest {
@Mock private FixedContact20260730Mapper baseMapper;
@Mock private FixedContactFeedback20260730Mapper feedbackMapper;
@Mock private RuntimeService runtimeService;
private FixedContact20260730ServiceImpl service;
@BeforeEach
void setUp() {
service = new FixedContact20260730ServiceImpl();
ReflectionTestUtils.setField(service, "baseMapper", baseMapper);
ReflectionTestUtils.setField(service, "fixedContactFeedback20260730Mapper", feedbackMapper);
ReflectionTestUtils.setField(service, "runtimeService", runtimeService);
}
// ==================== saveSubApprove(核心业务规则) ====================
@Test
void should_throw_when_main_not_exists() {
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(null);
assertThrows(JeecgBootException.class, () -> service.saveSubApprove("no-such", "dept-1", 1, "w1", null));
}
@Test
void should_store_impl_worker_when_is_end_one() {
// isEnd=1(办结):单选经办人 implWorker,清空 implUserNameList
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-1", 1, "w1", List.of("w2"));
DeptApproveDetail detail = entity.getApproveInfo().get("dept-1");
assertEquals("w1", detail.getImplWorker());
assertNull(detail.getImplUserNameList());
verify(baseMapper).updateById(entity);
}
@Test
void should_store_impl_user_name_list_when_is_end_zero() {
// isEnd=0(分配经办人):多选 implUserNameList,清空 implWorker
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-1", 0, "w1", List.of("w2", "w3"));
DeptApproveDetail detail = entity.getApproveInfo().get("dept-1");
assertNull(detail.getImplWorker());
assertEquals(List.of("w2", "w3"), detail.getImplUserNameList());
verify(baseMapper).updateById(entity);
}
@Test
void should_merge_into_existing_detail() {
// 同一部门二次保存:覆盖原有 detail 而不重建
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(new org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap());
DeptApproveDetail existing = new DeptApproveDetail();
existing.setDeptId("dept-1");
entity.getApproveInfo().put("dept-1", existing);
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-1", 0, null, List.of("w9"));
DeptApproveDetail detail = entity.getApproveInfo().get("dept-1");
assertEquals(List.of("w9"), detail.getImplUserNameList());
verify(baseMapper).updateById(entity);
}
@Test
void should_create_map_when_approve_info_null() {
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-9", 0, null, List.of("w1"));
assertEquals(List.of("w1"), entity.getApproveInfo().get("dept-9").getImplUserNameList());
}
// ==================== 主从 CRUD ====================
@Test
void should_save_main_and_sub_list() {
FixedContact20260730 main = new FixedContact20260730();
main.setId("biz-1");
FixedContactFeedback20260730 sub = new FixedContactFeedback20260730();
service.saveMain(main, List.of(sub));
verify(baseMapper).insert(main);
assertEquals("biz-1", sub.getFixedContact20260730Id());
verify(feedbackMapper).insert(sub);
}
@Test
void should_save_main_without_sub_when_list_null() {
FixedContact20260730 main = new FixedContact20260730();
service.saveMain(main, null);
verify(baseMapper).insert(main);
verify(feedbackMapper, never()).insert(any());
}
@Test
void should_update_main_and_replace_sub() {
FixedContact20260730 main = new FixedContact20260730();
main.setId("biz-1");
FixedContactFeedback20260730 sub = new FixedContactFeedback20260730();
service.updateMain(main, List.of(sub));
verify(baseMapper).updateById(main);
verify(feedbackMapper).deleteByMainId("biz-1");
assertEquals("biz-1", sub.getFixedContact20260730Id());
verify(feedbackMapper).insert(sub);
}
@Test
void should_delete_main_with_sub() {
service.delMain("biz-1");
verify(feedbackMapper).deleteByMainId("biz-1");
verify(baseMapper).deleteById("biz-1");
}
@Test
void should_delete_batch_with_sub() {
service.delBatchMain(List.of("biz-1", "biz-2"));
verify(feedbackMapper).deleteByMainId("biz-1");
verify(baseMapper).deleteById("biz-1");
verify(feedbackMapper).deleteByMainId("biz-2");
verify(baseMapper).deleteById("biz-2");
}
// ==================== 流程变量同步 ====================
@Test
void should_update_form_and_sync_variable() {
FixedContact20260730 form = new FixedContact20260730();
service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}");
verify(baseMapper).updateById(form);
verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}");
}
}
@@ -12,6 +12,11 @@
<artifactId>jeecg-system-cloud-api</artifactId> <artifactId>jeecg-system-cloud-api</artifactId>
<dependencies> <dependencies>
<!-- 基础核心(代码直接使用 base-core 的 Result/LoginUser/LowAppCopyMenu 等) -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- feign --> <!-- feign -->
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
+22 -1
View File
@@ -40,7 +40,7 @@
<properties> <properties>
<jeecgboot.version>3.8.0</jeecgboot.version> <jeecgboot.version>3.8.0</jeecgboot.version>
<!-- 默认跳过单测,需显式 -DskipTests=false 开启(回归测试等场景) --> <!-- 默认跳过单测,需显式 -DskipTests=false 开启(回归测试等场景) -->
<skipTests>true</skipTests> <skipTests>false</skipTests>
<!-- JDK版本支持17和1.8 --> <!-- JDK版本支持17和1.8 -->
<java.version>17</java.version> <java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -556,6 +556,27 @@
<skipTests>${skipTests}</skipTests> <skipTests>${skipTests}</skipTests>
</configuration> </configuration>
</plugin> </plugin>
<!-- JaCoCo 代码覆盖率:prepare-agent 给测试 JVM 插桩,report 在 test 阶段生成 target/site/jacoco 报告 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>jacoco-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 避免font文件的二进制文件格式压缩破坏 --> <!-- 避免font文件的二进制文件格式压缩破坏 -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>