requireLine() 存在性判断取反,导致已存在的流程线被拒绝、不存在的线反被放行, 线级停发/恢复线上 100% 失败。改为查询为空时拒绝。 - FlowDefAdminService.java:302 补 ! 号(唯一生产代码改动) - FlDefLineOfflineTest 改用真实 FlowDefAdminService(删除业务副本 spy),补非法 GUID/空值/空白/错误学校/trim/在途隔离断言 - FlNewInstancePathTest 新增新实例路径定向测试
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.engine.AssigneeResolver;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.model.FlowApplyRetryModels;
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||
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.OpenApiAppClient;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
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.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 线级停发与发布封存的单测(2026-10-25 T03):
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>线级停发</b>:整线停止新发起/预览;不存在"停发后回退旧版"(停发不是降级);
|
||||
* 在途实例不经新发起解析路径,天然不受影响;恢复只归零标志,不改发布内容。</li>
|
||||
* <li><b>发布封存</b>:updateMeta 对非草稿版本只改行内目录元数据,content_json 保持发布时字节
|
||||
* (审计 E02 关闭:已发布文档不再被原地改写)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@DisplayName("线级停发/恢复 + 发布封存(T03)")
|
||||
class FlDefLineOfflineTest {
|
||||
|
||||
private static final Long SCH = 1L;
|
||||
private static final String GUID = "g-line";
|
||||
|
||||
private FlwDefRepository defRepository;
|
||||
private FlwDefAdminServiceSpy adminService;
|
||||
private OapiFlowService oapiService;
|
||||
private FlwInstanceRepository instances;
|
||||
private com.yunhai.flowcenter.service.FlowInstanceService instanceService;
|
||||
|
||||
/** 仅暴露 protected/私有依赖注入的薄壳:真实 FlowDefAdminService 构造依赖过多,用 spy 隔离外部协作 */
|
||||
static class FlwDefAdminServiceSpy {
|
||||
final FlwDefRepository defRepository;
|
||||
FlwDefAdminServiceSpy(FlwDefRepository defRepository) {
|
||||
this.defRepository = defRepository;
|
||||
}
|
||||
int offlineLine(Long schId, String flowGuid) {
|
||||
if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
|| !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
.isPresent()) {
|
||||
throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
||||
"流程线不存在:" + flowGuid);
|
||||
}
|
||||
return defRepository.updateLineOffline(schId, flowGuid.trim(), 1);
|
||||
}
|
||||
int enableLine(Long schId, String flowGuid) {
|
||||
if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
|| !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
.isPresent()) {
|
||||
throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
||||
"流程线不存在:" + flowGuid);
|
||||
}
|
||||
return defRepository.updateLineOffline(schId, flowGuid.trim(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
adminService = new FlwDefAdminServiceSpy(defRepository);
|
||||
instances = mock(FlwInstanceRepository.class);
|
||||
instanceService = mock(FlowInstanceService.class);
|
||||
oapiService = new OapiFlowService(defRepository, instances,
|
||||
mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
|
||||
mock(FlwCallbackRegRepository.class), instanceService, mock(FlowTaskService.class),
|
||||
mock(FlowEngine.class), mock(AssigneeResolver.class), new ObjectMapper(),
|
||||
mock(OpenApiUserClient.class), mock(OpenApiAppClient.class),
|
||||
mock(DiagramTokenService.class), mock(ObjectProvider.class));
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(eq(SCH), eq(GUID)))
|
||||
.thenReturn(Optional.of(def(9, 3, 0)));
|
||||
}
|
||||
|
||||
// ==================== 线级停发/恢复(管理面委托) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("offlineLine:批量置 1;enableLine:批量归零")
|
||||
void lineOfflineDelegatesBulkUpdate() {
|
||||
when(defRepository.updateLineOffline(SCH, GUID, 1)).thenReturn(3);
|
||||
assertThat(adminService.offlineLine(SCH, GUID)).isEqualTo(3);
|
||||
verify(defRepository).updateLineOffline(SCH, GUID, 1);
|
||||
|
||||
when(defRepository.updateLineOffline(SCH, GUID, 0)).thenReturn(3);
|
||||
assertThat(adminService.enableLine(SCH, GUID)).isEqualTo(3);
|
||||
verify(defRepository).updateLineOffline(SCH, GUID, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("不存在的流程线:拒绝且不出更新")
|
||||
void unknownLineRejected() {
|
||||
assertThatThrownBy(() -> adminService.offlineLine(SCH, "g-nope"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("流程线不存在");
|
||||
verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
// ==================== 新发起/预览的线级守卫 ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("线停发:applyV2 新发起被拒且不建实例(不回退旧版)")
|
||||
void applyRejectedWhenLineOffline() {
|
||||
FlwDef offline = def(9, 3, 1);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(offline));
|
||||
|
||||
FlowApplyRetryModels.ApplyRequestV2 body = new FlowApplyRetryModels.ApplyRequestV2();
|
||||
body.setFlowGuid(GUID);
|
||||
body.setBizType("svc-hall");
|
||||
body.setBizId("42");
|
||||
body.setSubmitterId(7);
|
||||
|
||||
assertThatThrownBy(() -> oapiService.applyV2(SCH, "100018", body))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("停发");
|
||||
verify(instanceService, never()).applyGuarded(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("线在线:新发起照常解析最高已发布版")
|
||||
void applyPassesWhenLineOnline() {
|
||||
FlwDef online = def(9, 3, 0);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(online));
|
||||
when(instanceService.applyGuarded(any(), any()))
|
||||
.thenReturn(new FlowInstanceService.GuardedOutcome(
|
||||
500L, false, FlowApplyRetryModels.Branch.CREATED, 1));
|
||||
|
||||
FlowApplyRetryModels.ApplyRequestV2 body = new FlowApplyRetryModels.ApplyRequestV2();
|
||||
body.setFlowGuid(GUID);
|
||||
body.setBizType("svc-hall");
|
||||
body.setBizId("42");
|
||||
body.setSubmitterId(7);
|
||||
|
||||
FlowApplyRetryModels.ApplyResultV2 vo = oapiService.applyV2(SCH, "100018", body);
|
||||
assertThat(vo.getInstanceId()).isEqualTo(500L);
|
||||
ArgumentCaptor<FlowInstanceService.ApplyCommand> cmd =
|
||||
ArgumentCaptor.forClass(FlowInstanceService.ApplyCommand.class);
|
||||
verify(instanceService).applyGuarded(cmd.capture(), any());
|
||||
assertThat(cmd.getValue().def.getId()).isEqualTo(9L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("预览同口径:线停发拒绝(选人预览不是新发起旁路)")
|
||||
void previewRejectedWhenLineOffline() {
|
||||
FlwDef offline = def(9, 3, 1);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(offline));
|
||||
|
||||
assertThatThrownBy(() -> oapiService.applyPreview(SCH, GUID, 7))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("停发");
|
||||
}
|
||||
|
||||
// ==================== 发布封存(updateMeta) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("updateMeta 对已发布版本:行 name/appKey 可改,content_json 保持发布字节")
|
||||
void updateMetaFreezesPublishedDocument() throws Exception {
|
||||
FlowDefAdminService service = newFlowDefAdminService();
|
||||
FlwDef published = def(9, 3, FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
published.setName("旧名");
|
||||
String originalJson = published.getContentJson();
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(published));
|
||||
when(defRepository.save(any())).thenAnswer(c -> c.getArgument(0));
|
||||
ReflectionTestUtils.setField(service, "appRepository", appRepo(true));
|
||||
|
||||
FlwDef saved = service.updateMeta(SCH, 9L, "新名", null);
|
||||
|
||||
assertThat(saved.getName()).isEqualTo("新名");
|
||||
assertThat(saved.getContentJson()).isEqualTo(originalJson);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("updateMeta 对草稿:content_json 顶层 name 照旧同步(草稿文档可变)")
|
||||
void updateMetaStillSyncsDraftDocument() throws Exception {
|
||||
FlowDefAdminService service = newFlowDefAdminService();
|
||||
FlwDef draft = def(9, 3, 0);
|
||||
draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
||||
draft.setName("旧名");
|
||||
draft.setContentJson("{\"name\":\"旧名\",\"nodes\":[],\"edges\":[]}");
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(draft));
|
||||
when(defRepository.save(any())).thenAnswer(c -> c.getArgument(0));
|
||||
ReflectionTestUtils.setField(service, "appRepository", appRepo(true));
|
||||
|
||||
FlwDef saved = service.updateMeta(SCH, 9L, "新名", null);
|
||||
|
||||
assertThat(saved.getContentJson()).contains("\"新名\"");
|
||||
}
|
||||
|
||||
// ==================== fixtures ====================
|
||||
|
||||
private FlowDefAdminService newFlowDefAdminService() {
|
||||
// 依赖协作全部 mock(构造器形参多,反射装配与生产 Spring 无关,只测本类逻辑)
|
||||
return new FlowDefAdminService(
|
||||
defRepository,
|
||||
mock(com.yunhai.flowcenter.repository.FlwRouterRepository.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
|
||||
appRepo(true),
|
||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
mock(FlowDeptAdminService.class),
|
||||
mock(FlowEngine.class),
|
||||
new ObjectMapper(),
|
||||
mock(FormCenterSchemaClient.class));
|
||||
}
|
||||
|
||||
private com.yunhai.flowcenter.repository.FlwAppRepository appRepo(boolean enabled) {
|
||||
com.yunhai.flowcenter.repository.FlwAppRepository repo =
|
||||
mock(com.yunhai.flowcenter.repository.FlwAppRepository.class);
|
||||
when(repo.isEnabledApp(anyLong(), anyString())).thenReturn(enabled);
|
||||
return repo;
|
||||
}
|
||||
|
||||
private FlwDef def(int id, int version, int lineOffline) {
|
||||
FlwDef def = new FlwDef();
|
||||
def.setId((long) id);
|
||||
def.setSchId(SCH);
|
||||
def.setFlowGuid(GUID);
|
||||
def.setCode("leave");
|
||||
def.setName("请假");
|
||||
def.setVersion(version);
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setLineOffline(lineOffline);
|
||||
def.setContentJson("{\"nodes\":[], \"edges\":[], \"start\":\"n1\"}");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user