Files
flow-center-service/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java
T
chenxing a06b610d9d
build / build-push (push) Successful in 54s
feat(dept): 科室管理——flw_dept/flw_dept_member 组+成员模型取代单用户映射行(议题3,拍板D1选B)
- deptKey=本地自增 id;source_group_id 幂等(一键拉取=初始化/刷新语义,不重复建组);不做存量迁移
- 四处读源全部切新表:AssigneeResolver(宽容口径:deptKey 不存在/停用返回空,图示/轨迹不炸)、
  enabledDeptKeys 发布校验白名单、DeptLabelSupport 唯一出口(图示科室视图+轨迹富化)、编辑器 deptList 聚合
- /admin/dept/* 全套端点(CRUD/成员/拉取预览导入);旧 /map/* 映射 CRUD 端点删除
  (保留 /map/lookupUser 选人检索与 /user/teacherTree);flw_assignee_map 实体/表保留不删
- IT 种子切源(FlowTestSupport tag→deptKey 注册表,用例主体零改动);全量 191 绿;E2E 22/22
  (local-debug/four-topics-e-e2e.py,含切源后果验证与生产盘点 SQL)
2026-10-10 19:47:58 +08:00

529 lines
25 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.flowcenter.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.controller.api.FlowAdminController;
import com.yunhai.flowcenter.controller.api.FlowUserController;
import com.yunhai.flowcenter.entity.FlwDeptMember;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.entity.FlwTask;
import com.yunhai.flowcenter.engine.AssigneeResolver;
import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwBranchRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwTaskRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.base.OpenApiUserClient;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationEventPublisher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
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.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 流程示意图出图(方案 §3.2 标签解析规则 + §5 TDD C1-C6):
* C1/CC3 视图分支与兜底链、C2 UserBase 补名降级、C4 flowGuid 取版、C5/C6 控制器守卫接线。
* 科室视图不泄人员名单(USERS 只显"指定办理人"且不调 UserBase)是硬口径。
*/
class FlowDiagramServiceTest {
private static final Long SCH = 990001L;
/** 并行链:start→sp1→n1(DEPT_KEY rs)→j1→end(教师入职缩样) */
private static final String DEPT_DEF_JSON = "{"
+ "\"code\":\"teacher_in\",\"name\":\"教师入职\",\"start\":\"start\","
+ "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\"},"
+ "{\"id\":\"sp1\",\"type\":\"PARALLEL_SPLIT\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"人事处审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"rs\"},\"countersign\":\"ALL\",\"deadlineHours\":48},"
+ "{\"id\":\"j1\",\"type\":\"PARALLEL_JOIN\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"sp1\"},{\"from\":\"sp1\",\"to\":\"n1\"},"
+ "{\"from\":\"n1\",\"to\":\"j1\"},{\"from\":\"j1\",\"to\":\"end\"}]}";
/** ANY 会签变体(无时限) */
private static final String ANY_DEF_JSON = "{"
+ "\"code\":\"c2\",\"name\":\"任一通过\",\"start\":\"start\","
+ "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"rs\"},\"countersign\":\"ANY\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}]}";
/** USERS 直配变体(Notice 先审后发缩样) */
private static final String USERS_DEF_JSON = "{"
+ "\"code\":\"notice_audit\",\"name\":\"Notice先审后发\",\"start\":\"start\","
+ "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\"},"
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"甲乙会签\",\"assignee\":{\"type\":\"USERS\",\"userIds\":[11,12,13]}},"
+ "{\"id\":\"end\",\"type\":\"END\"}],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n2\"},{\"from\":\"n2\",\"to\":\"end\"}]}";
private FlwDefRepository defRepository;
private com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService;
private FlwTaskRepository taskRepository;
private com.yunhai.flowcenter.repository.FlwCcRepository ccRepository;
private OpenApiUserClient userClient;
private FlowDiagramService service;
@BeforeEach
void setUp() {
defRepository = mock(FlwDefRepository.class);
deptAdminService = mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class);
taskRepository = mock(FlwTaskRepository.class);
ccRepository = mock(com.yunhai.flowcenter.repository.FlwCcRepository.class);
userClient = mock(OpenApiUserClient.class);
when(taskRepository.findByInstanceIdOrderByIdAsc(anyLong()))
.thenReturn(Collections.emptyList());
when(ccRepository.findByInstanceIdOrderByIdAsc(anyLong()))
.thenReturn(Collections.emptyList());
// 真 FlowEngine(真实 JSON 解析通道)+ mock 依赖(ConditionRouteService 不触达:纯示意图测试)
FlowEngine engine = new FlowEngine(new ObjectMapper(),
taskRepository, mock(FlwBranchRepository.class), ccRepository,
mock(AssigneeResolver.class), mock(com.yunhai.flowcenter.engine.ConditionRouteService.class),
mock(ApplicationEventPublisher.class));
service = new FlowDiagramService(defRepository, deptAdminService,
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
taskRepository, ccRepository,
mock(com.yunhai.flowcenter.repository.FlwRouteLogRepository.class),
engine, userClient, new DiagramLayoutService());
}
private FlwDef def(String json) {
FlwDef def = new FlwDef();
def.setId(12L);
def.setSchId(SCH);
def.setFlowGuid("g1");
def.setCode("c1");
def.setName("教师入职");
def.setVersion(2);
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
def.setContentJson(json);
return def;
}
private FlwDeptMember memberRow(int userId, String userName) {
FlwDeptMember m = new FlwDeptMember();
m.setDeptId(7L);
m.setUserId(userId);
m.setUserName(userName);
m.setStatus(0);
return m;
}
private BoaUser boaUser(String name) {
BoaUser u = mock(BoaUser.class);
when(u.getName()).thenReturn(name);
return u;
}
private FlowModels.NodeVo node(FlowModels.DiagramVo vo, String id) {
return vo.getNodes().stream().filter(n -> id.equals(n.getId())).findFirst()
.orElseThrow(() -> new AssertionError("节点缺失:" + id));
}
// ==================== C1:DEPT_KEY 节点科室视图 ====================
@Test
void c1_deptView_deptKeyNode_labelDeptName_subLabelCountAndDeadline() {
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, "李四")));
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
FlowModels.NodeVo n1 = node(vo, "n1");
assertEquals("人事处", n1.getLabel());
assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel());
assertEquals("rs", n1.getDeptKey());
assertEquals("DEPT_KEY", n1.getAssigneeType());
assertEquals("ALL", n1.getCountersign());
assertEquals(Integer.valueOf(48), n1.getDeadlineHours());
// 科室视图不出现映射成员姓名
assertFalse(n1.getSubLabel().contains("张三"));
assertFalse(n1.getSubLabel().contains("李四"));
}
@Test
void c1_deptView_fallbackChain_deptKeyThenNodeName() {
// 科室已被删(deptName=null)→ 节点名兜底(成员解析空,人数照实)
when(deptAdminService.resolveEnabled(SCH, "rs"))
.thenReturn(Collections.singletonList(memberRow(11, "张三")));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
assertEquals("人事处审批", n1.getLabel());
assertEquals("科室 · 1人 · 时限48h", n1.getSubLabel());
// 科室存在但成员为空 → 科室名照常,人数 0
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Collections.emptyList());
n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
assertEquals("人事处", n1.getLabel());
assertEquals("科室 · 0人 · 时限48h", n1.getSubLabel());
}
@Test
void c1_anyCountersign_appendsSuffix() {
when(deptAdminService.resolveEnabled(SCH, "rs"))
.thenReturn(Collections.singletonList(memberRow(11, "张三")));
FlowModels.NodeVo n1 = node(service.build(def(ANY_DEF_JSON), null, "dept"), "n1");
assertEquals("科室 · 1人 · 任一人通过", n1.getSubLabel());
}
@Test
void c1_fixedLabels_andEdgesCarried() {
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
assertEquals("开始", node(vo, "start").getLabel());
assertEquals("并行分发", node(vo, "sp1").getLabel());
assertEquals("汇聚等待", node(vo, "j1").getLabel());
assertEquals("结束", node(vo, "end").getLabel());
assertNull(node(vo, "start").getSubLabel());
assertNull(node(vo, "end").getSubLabel());
assertEquals(4, vo.getEdges().size());
assertEquals("start", vo.getEdges().get(0).getFrom());
assertEquals("sp1", vo.getEdges().get(0).getTo());
// 出图元信息(前端"按流程定义 vN 生成"与工具条 Tag 数据源)
assertEquals("g1", vo.getFlowGuid());
assertEquals(Long.valueOf(12L), vo.getDefId());
assertEquals("教师入职", vo.getName());
assertEquals(Integer.valueOf(2), vo.getVersion());
assertEquals(Integer.valueOf(1), vo.getStatus());
assertEquals("dept", vo.getMode());
}
@Test
void c1_autoView_deptKeyNode_rendersDeptStyle() {
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, "李四")));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "auto"), "n1");
assertEquals("人事处", n1.getLabel());
assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel());
assertEquals("auto", service.build(def(DEPT_DEF_JSON), null, "auto").getMode());
}
/** 2026-09-18:mode 缺省改为人员视图(原 auto 自适应)——老深链显式带 auto 仍按自适应出图 */
@Test
void c1_defaultMode_isUserView() {
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, "李四")));
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, null);
assertEquals("user", vo.getMode());
FlowModels.NodeVo n1 = node(vo, "n1");
assertEquals("人事处审批", n1.getLabel());
assertEquals("张三、李四 · 时限48h", n1.getSubLabel());
}
// ==================== C2:USERS 节点人员视图(UserBase 补名降级) ====================
@Test
void c2_userView_usersNode_namesFromUserBase_degradeToId() {
BoaUser jia = boaUser("甲老师");
BoaUser yi = boaUser("乙老师");
when(userClient.getUserById("11")).thenReturn(jia);
when(userClient.getUserById("12")).thenReturn(yi);
when(userClient.getUserById("13")).thenThrow(new RuntimeException("UserBase 不可达"));
FlowModels.NodeVo n2 = node(service.build(def(USERS_DEF_JSON), null, "user"), "n2");
assertEquals("甲乙会签", n2.getLabel());
assertEquals("甲老师、乙老师、#13", n2.getSubLabel());
assertEquals("USERS", n2.getAssigneeType());
assertNull(n2.getDeptKey());
}
@Test
void c2_userBaseReturnsNullUser_degradesToId() {
when(userClient.getUserById(anyString())).thenReturn(null);
FlowModels.NodeVo n2 = node(service.build(def(USERS_DEF_JSON), null, "user"), "n2");
assertEquals("#11、#12、#13", n2.getSubLabel());
}
@Test
void c2_autoView_usersNode_rendersUserStyle() {
BoaUser jia = boaUser("甲老师");
BoaUser yi = boaUser("乙老师");
when(userClient.getUserById("11")).thenReturn(jia);
when(userClient.getUserById("12")).thenReturn(yi);
when(userClient.getUserById("13")).thenReturn(null);
FlowModels.NodeVo n2 = node(service.build(def(USERS_DEF_JSON), null, "auto"), "n2");
assertEquals("甲老师、乙老师、#13", n2.getSubLabel());
}
// ==================== C3:视图分支防泄名单 / DEPT_KEY 人员视图 ====================
@Test
void c3_deptView_usersNode_subLabelOnlyAssigneeHint_neverCallsUserBase() {
BoaUser jia = boaUser("甲老师");
BoaUser yi = boaUser("乙老师");
when(userClient.getUserById("11")).thenReturn(jia);
when(userClient.getUserById("12")).thenReturn(yi);
FlowModels.NodeVo n2 = node(service.build(def(USERS_DEF_JSON), null, "dept"), "n2");
assertEquals("甲乙会签", n2.getLabel());
assertEquals("指定办理人", n2.getSubLabel());
assertFalse(n2.getSubLabel().contains("甲老师"));
assertFalse(n2.getSubLabel().contains("乙老师"));
// 防泄名单硬保证:科室视图连 UserBase 都不查
verify(userClient, never()).getUserById(anyString());
}
@Test
void c3_userView_deptKeyNode_listsMemberNames_blankNameDegradesToId() {
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, null)));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "user"), "n1");
// 人员视图:label=节点名,subLabel=映射成员名(空名降级 #id)+ 会签/时限后缀
assertEquals("人事处审批", n1.getLabel());
assertEquals("张三、#12 · 时限48h", n1.getSubLabel());
}
// ==================== C4:flowGuid 分支取版与参数校验 ====================
@Test
void c4_byGuid_returnsHighestPublishedDef() {
FlwDef def = def(DEPT_DEF_JSON);
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
SCH, "g1", FlwEnums.DefStatus.PUBLISHED.getCode()))
.thenReturn(Optional.of(def));
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.byGuid(SCH, "g1", null);
assertEquals("教师入职", vo.getName());
assertEquals(Integer.valueOf(2), vo.getVersion());
// 2026-09-18:缺省口径=人员视图
assertEquals("user", vo.getMode());
assertNull(vo.getInstanceId());
}
@Test
void c4_byGuid_noPublished_throwsBizException() {
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
anyLong(), anyString(), anyInt()))
.thenReturn(Optional.empty());
BizException e = assertThrows(BizException.class, () -> service.byGuid(SCH, "g1", "dept"));
assertEquals("流程不存在或未发布", e.getMessage());
assertEquals(ErrorCode.PARAM_ERROR, e.getCode());
}
@Test
void c4_byGuid_crossSchoolDef_forbidden() {
FlwDef other = def(DEPT_DEF_JSON);
other.setSchId(888888L);
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
anyLong(), anyString(), anyInt()))
.thenReturn(Optional.of(other));
BizException e = assertThrows(BizException.class, () -> service.byGuid(SCH, "g1", "dept"));
assertEquals("跨校不可查看", e.getMessage());
assertEquals(ErrorCode.FORBIDDEN, e.getCode());
}
@Test
void c4_invalidMode_rejected() {
BizException e = assertThrows(BizException.class,
() -> service.build(def(DEPT_DEF_JSON), null, "xxx"));
assertEquals("mode 仅支持 dept/user", e.getMessage());
}
@Test
void c4_byInstance_usesDefSnapshotOfInstance() {
FlwInstance instance = new FlwInstance();
instance.setId(100L);
instance.setSchId(SCH);
instance.setDefId(12L);
when(defRepository.findById(12L)).thenReturn(Optional.of(def(DEPT_DEF_JSON)));
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.byInstance(instance, "dept");
assertEquals(Long.valueOf(100L), vo.getInstanceId());
assertEquals(Long.valueOf(12L), vo.getDefId());
assertEquals(Integer.valueOf(2), vo.getVersion());
}
@Test
void c6_build_draftDef_ok() {
FlwDef draft = def(DEPT_DEF_JSON);
draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.build(draft, null, "auto");
assertEquals(Integer.valueOf(0), vo.getStatus());
assertEquals(5, vo.getNodes().size());
}
// ==================== C5:用户面 /api/flow/diagram 控制器守卫 ====================
@Nested
class UserDiagramEndpoint {
private FlowQueryService queryService;
private FlowTaskService taskService;
private FlowInstanceService instanceService;
private FlowUserController controller;
private FlowDiagramService diagramService;
@BeforeEach
void setUpController() {
queryService = mock(FlowQueryService.class);
taskService = mock(FlowTaskService.class);
instanceService = mock(FlowInstanceService.class);
diagramService = mock(FlowDiagramService.class);
// 真守卫(同 FlowAccessGuardTest 口径):提交人/办理人/本校管理员
FlowAccessGuard accessGuard = new FlowAccessGuard(taskRepository);
controller = new FlowUserController(queryService, taskService, instanceService,
accessGuard, diagramService, mock(com.yunhai.flowcenter.service.DiagramImageService.class));
}
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
BoaUser u = mock(BoaUser.class, RETURNS_DEEP_STUBS);
when(u.getId()).thenReturn(id);
when(u.getType()).thenReturn(type);
when(u.getRoleIds()).thenReturn(roleIds.length == 0
? null : new ArrayList<>(Arrays.asList(roleIds)));
when(u.getCurrentRole().getSchId()).thenReturn(schId);
return u;
}
@Test
void c5_flowGuid_sameSchoolUser_ok() {
FlowModels.DiagramVo vo = new FlowModels.DiagramVo();
when(diagramService.byGuid(SCH, "g1", "dept")).thenReturn(vo);
assertSame(vo, controller.diagram(user(11, 2, SCH.intValue(), 9), "g1", null, "dept"));
verify(diagramService).byGuid(SCH, "g1", "dept");
}
@Test
void c5_flowGuid_notLoggedIn_forbidden() {
BizException e = assertThrows(BizException.class,
() -> controller.diagram(null, "g1", null, null));
assertEquals("未登录", e.getMessage());
assertEquals(ErrorCode.FORBIDDEN, e.getCode());
}
@Test
void c5_noParam_paramError() {
assertThrows(BizException.class,
() -> controller.diagram(user(11, 2, SCH.intValue(), 9), null, null, null));
}
@Test
void c5_instance_submitter_ok() {
FlwInstance instance = new FlwInstance();
instance.setId(100L);
instance.setSchId(SCH);
instance.setSubmitterId(11);
when(instanceService.requireInstance(SCH, 100L)).thenReturn(instance);
FlowModels.DiagramVo vo = new FlowModels.DiagramVo();
when(diagramService.byInstance(instance, "dept")).thenReturn(vo);
assertSame(vo, controller.diagram(user(11, 2, SCH.intValue(), 9), null, 100L, "dept"));
verify(diagramService).byInstance(instance, "dept");
}
@Test
void c5_instance_assignee_ok() {
FlwInstance instance = new FlwInstance();
instance.setId(100L);
instance.setSchId(SCH);
instance.setSubmitterId(11);
FlwTask task = new FlwTask();
task.setAssigneeId(22);
when(instanceService.requireInstance(SCH, 100L)).thenReturn(instance);
when(taskRepository.findByInstanceIdOrderByIdAsc(100L))
.thenReturn(Collections.singletonList(task));
FlowModels.DiagramVo vo = new FlowModels.DiagramVo();
when(diagramService.byInstance(instance, "dept")).thenReturn(vo);
assertSame(vo, controller.diagram(user(22, 2, SCH.intValue(), 9), null, 100L, "dept"));
}
@Test
void c5_instance_unrelatedUser_forbidden() {
FlwInstance instance = new FlwInstance();
instance.setId(100L);
instance.setSchId(SCH);
instance.setSubmitterId(11);
when(instanceService.requireInstance(SCH, 100L)).thenReturn(instance);
BizException e = assertThrows(BizException.class,
() -> controller.diagram(user(36, 2, SCH.intValue(), 9), null, 100L, "dept"));
assertEquals("仅提交人/办理人/本校管理员可查看该办件", e.getMessage());
verify(diagramService, never()).byInstance(any(), any());
}
}
// ==================== C6:管理面 /api/flow/admin/def/diagram ====================
@Nested
class AdminDefDiagramEndpoint {
private FlowAdminController controller;
private FlowDefAdminService defAdminService;
private FlowDiagramService diagramService;
@BeforeEach
void setUpController() {
defAdminService = mock(FlowDefAdminService.class);
diagramService = mock(FlowDiagramService.class);
// 真管理守卫(纯逻辑无依赖)+ 其余依赖 mock
controller = new FlowAdminController(new FlowAdminGuard(), defAdminService,
mock(FlowMapAdminService.class), mock(FlowCallbackAdminService.class),
mock(com.yunhai.flowcenter.service.FlowRouterAdminService.class),
mock(FlwInstanceRepository.class), diagramService,
mock(com.yunhai.flowcenter.service.DiagramImageService.class),
new ObjectMapper(),
mock(com.yunhai.flowcenter.service.FlowAppAdminService.class),
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class));
}
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
BoaUser u = mock(BoaUser.class, RETURNS_DEEP_STUBS);
when(u.getId()).thenReturn(id);
when(u.getType()).thenReturn(type);
when(u.getRoleIds()).thenReturn(roleIds.length == 0
? null : new ArrayList<>(Arrays.asList(roleIds)));
when(u.getCurrentRole().getSchId()).thenReturn(schId);
return u;
}
@Test
void c6_admin_draftDef_ok() {
FlwDef draft = def(DEPT_DEF_JSON);
draft.setId(55L);
draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
when(defAdminService.get(SCH, 55L)).thenReturn(draft);
FlowModels.DiagramVo vo = new FlowModels.DiagramVo();
when(diagramService.build(draft, null, "auto")).thenReturn(vo);
assertSame(vo, controller.defDiagram(user(1, 1, SCH.intValue()), 55L, "auto"));
verify(diagramService).build(draft, null, "auto");
}
@Test
void c6_nonAdmin_forbidden() {
BoaUser ordinary = user(77, 2, SCH.intValue(), 9);
BizException e = assertThrows(BizException.class,
() -> controller.defDiagram(ordinary, 55L, "auto"));
assertEquals("无流程管理权限", e.getMessage());
verify(defAdminService, never()).get(any(), anyLong());
}
}
}