Files
flow-center-service/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java
T
chenxing b8441065d0
build / build-push (push) Successful in 5m11s
feat: 引擎多出口路由/退回/移交/或签补齐 + 定义 layout 坐标
- 多出口:FlwTask 增 edge_name 列(sql/20260916),FlwTaskRepository.claimAndFinish
  同步落列;FlowTaskService.handle 出度>1 时 edgeName 必填;FlowEngine.continueAdvance
  按 edge name 选路(单出口原路径逐字节兼容,未命中不推进);FlowAdvanceService/
  FlowNodeAdvanceListener 透传;BranchScope 从 FlowEngine 提取
- DefValidator:多出口出口命名必填且同节点唯一;跨分支连线发布期明确拒绝
- 退回 POST /task/{id}/returnBack:默认退主管线最近已批节点,精确失效当前节点待办,
  split 幂等 upsert 使退回重走并行段不撞 flw_branch uk
- 移交 POST /task/{id}/transfer:TRANSFERRED(5)+新建待办,AssigneeResolver.requireUserName
- 或签 ANY 补齐:首个同意失效同节点兄弟待办
- 已办口径/撤回守卫:status in (1,2,4,5);新事件 flow.returned/flow.transferred
- FlowDiagramService:EdgeVo.name/NodeVo.state/DiagramVo.layout,办件图状态计算
- 新 IT:多出口×3、ANY 或签、退回重走并行段、退回/移交/撤回

mvn test 84/84 绿
2026-09-16 22:23:02 +08:00

498 lines
23 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.FlwAssigneeMap;
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.FlwAssigneeMapRepository;
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 FlwAssigneeMapRepository mapRepository;
private FlwTaskRepository taskRepository;
private OpenApiUserClient userClient;
private FlowDiagramService service;
@BeforeEach
void setUp() {
defRepository = mock(FlwDefRepository.class);
mapRepository = mock(FlwAssigneeMapRepository.class);
taskRepository = mock(FlwTaskRepository.class);
userClient = mock(OpenApiUserClient.class);
when(taskRepository.findByInstanceIdOrderByIdAsc(anyLong()))
.thenReturn(Collections.emptyList());
// 真 FlowEngine(真实 JSON 解析通道)+ mock 依赖
FlowEngine engine = new FlowEngine(new ObjectMapper(),
taskRepository, mock(FlwBranchRepository.class), mock(AssigneeResolver.class),
mock(ApplicationEventPublisher.class));
service = new FlowDiagramService(defRepository, mapRepository,
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
taskRepository, engine, userClient);
}
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 FlwAssigneeMap mapRow(String deptName, int userId, String userName) {
FlwAssigneeMap m = new FlwAssigneeMap();
m.setSchId(SCH);
m.setDeptKey("rs");
m.setDeptName(deptName);
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(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
mapRow("人事处", 11, "张三"), mapRow("人事处", 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() {
// 映射行全无名 → deptKey 兜底
when(mapRepository.findEnabled(SCH, "rs"))
.thenReturn(Collections.singletonList(mapRow("", 11, "张三")));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
assertEquals("rs", n1.getLabel());
assertEquals("科室 · 1人 · 时限48h", n1.getSubLabel());
// 无映射行 → 节点名兜底(人数照实为 0)
when(mapRepository.findEnabled(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(mapRepository.findEnabled(SCH, "rs"))
.thenReturn(Collections.singletonList(mapRow("人事处", 11, "张三")));
FlowModels.NodeVo n1 = node(service.build(def(ANY_DEF_JSON), null, "dept"), "n1");
assertEquals("科室 · 1人 · 任一人通过", n1.getSubLabel());
}
@Test
void c1_fixedLabels_andEdgesCarried() {
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
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(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四")));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, null), "n1");
assertEquals("人事处", n1.getLabel());
assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel());
assertEquals("auto", service.build(def(DEPT_DEF_JSON), null, null).getMode());
}
// ==================== 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(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
mapRow("人事处", 11, "张三"), mapRow("人事处", 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(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
FlowModels.DiagramVo vo = service.byGuid(SCH, "g1", null);
assertEquals("教师入职", vo.getName());
assertEquals(Integer.valueOf(2), vo.getVersion());
assertEquals("auto", 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(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
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(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
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);
}
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(FlwInstanceRepository.class), diagramService);
}
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());
}
}
}