From ff7544a1276c1cec089dacbff066e39241cd3391 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 5 Dec 2024 18:00:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/entity/NodeDefinition.java | 2 +- .../backend/workflow/entity/NodeInstance.java | 2 + .../workflow/entity/WorkflowDefinition.java | 2 +- .../workflow/entity/WorkflowInstance.java | 2 +- .../backend/workflow/enums/NodeTypeEnum.java | 13 +- .../workflow/enums/WorkflowStatusEnum.java | 25 +- .../service/IWorkflowInstanceService.java | 47 ++- .../impl/WorkflowInstanceServiceImpl.java | 132 +++++++- .../db/migration/V1.0.0__init_schema.sql | 12 +- .../db/migration/V1.0.1__init_data.sql | 20 +- .../engine/DefaultWorkflowEngineTest.java | 301 ++++++++++++++++++ .../executor/StartNodeExecutorTest.java | 64 ++++ .../engine/executor/TaskNodeExecutorTest.java | 175 ++++++++++ .../WorkflowDefinitionServiceTest.java | 162 ++++++++++ .../service/WorkflowInstanceServiceTest.java | 187 +++++++++++ 15 files changed, 1107 insertions(+), 39 deletions(-) create mode 100644 backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngineTest.java create mode 100644 backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/StartNodeExecutorTest.java create mode 100644 backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutorTest.java create mode 100644 backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java create mode 100644 backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowInstanceServiceTest.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java index 2aa276de..77f00478 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java @@ -39,7 +39,7 @@ public class NodeDefinition extends Entity { /** * 节点类型 */ - @Enumerated(EnumType.STRING) + @Enumerated(EnumType.ORDINAL) @Column(nullable = false) private NodeTypeEnum type; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeInstance.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeInstance.java index cd662273..41794d77 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeInstance.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeInstance.java @@ -39,6 +39,7 @@ public class NodeInstance extends Entity { /** * 节点类型 */ + @Enumerated(EnumType.ORDINAL) @Column(nullable = false) private NodeTypeEnum nodeType; @@ -51,6 +52,7 @@ public class NodeInstance extends Entity { /** * 节点状态 */ + @Enumerated(EnumType.ORDINAL) @Column(nullable = false) private NodeStatusEnum status; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java index db9c3526..6d8dd5d2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java @@ -41,7 +41,7 @@ public class WorkflowDefinition extends Entity { /** * 工作流状态 */ - @Enumerated(EnumType.STRING) + @Enumerated(EnumType.ORDINAL) @Column(nullable = false) private WorkflowStatusEnum status = WorkflowStatusEnum.DRAFT; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java index be6e7ac8..6ffa1ec9 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java @@ -40,7 +40,7 @@ public class WorkflowInstance extends Entity { /** * 状态 */ - @Enumerated(EnumType.STRING) + @Enumerated(EnumType.ORDINAL) @Column(nullable = false) private WorkflowStatusEnum status = WorkflowStatusEnum.RUNNING; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java index 8c8e2992..9fcd788d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java @@ -10,13 +10,14 @@ import lombok.Getter; @AllArgsConstructor public enum NodeTypeEnum { - START("START", "开始节点"), - END("END", "结束节点"), - TASK("TASK", "任务节点"), - GATEWAY("GATEWAY", "网关节点"), - SUB_PROCESS("SUB_PROCESS", "子流程节点"), - SHELL("SHELL", "Shell脚本节点"); + START(0, "START", "开始节点"), + END(1, "END", "结束节点"), + TASK(2, "TASK", "任务节点"), + GATEWAY(3, "GATEWAY", "网关节点"), + SUB_PROCESS(4, "SUB_PROCESS", "子流程节点"), + SHELL(5, "SHELL", "Shell脚本节点"); + private final int value; private final String code; private final String description; } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/WorkflowStatusEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/WorkflowStatusEnum.java index 1bd0dc69..96dfaded 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/WorkflowStatusEnum.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/WorkflowStatusEnum.java @@ -11,21 +11,22 @@ import lombok.Getter; public enum WorkflowStatusEnum { // 工作流定义状态 - DRAFT("DRAFT", "草稿"), - PUBLISHED("PUBLISHED", "已发布"), - DISABLED("DISABLED", "已禁用"), + DRAFT(0, "DRAFT", "草稿"), + PUBLISHED(1, "PUBLISHED", "已发布"), + DISABLED(2, "DISABLED", "已禁用"), // 工作流实例状态 - CREATED("CREATED", "已创建"), - PENDING("PENDING", "等待执行"), - RUNNING("RUNNING", "执行中"), - PAUSED("PAUSED", "已暂停"), - COMPLETED("COMPLETED", "已完成"), - FAILED("FAILED", "执行失败"), - CANCELLED("CANCELLED", "已取消"), - SUSPENDED("SUSPENDED", "已暂停"), - TERMINATED("TERMINATED", "已终止"); + CREATED(3, "CREATED", "已创建"), + PENDING(4, "PENDING", "等待执行"), + RUNNING(5, "RUNNING", "执行中"), + PAUSED(6, "PAUSED", "已暂停"), + COMPLETED(7, "COMPLETED", "已完成"), + FAILED(8, "FAILED", "执行失败"), + CANCELLED(9, "CANCELLED", "已取消"), + SUSPENDED(10, "SUSPENDED", "已暂停"), + TERMINATED(11, "TERMINATED", "已终止"); + private final int value; private final String code; private final String description; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowInstanceService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowInstanceService.java index b73e0037..1b4a429b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowInstanceService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowInstanceService.java @@ -4,8 +4,53 @@ import com.qqchen.deploy.backend.framework.service.IBaseService; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO; import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import java.util.Map; + /** * 工作流实例服务接口 */ public interface IWorkflowInstanceService extends IBaseService { -} \ No newline at end of file + + /** + * 创建工作流实例 + * + * @param definitionId 工作流定义ID + * @param businessKey 业务标识 + * @param variables 初始变量 + * @return 工作流实例 + */ + WorkflowInstanceDTO createInstance(Long definitionId, String businessKey, Map variables); + + /** + * 启动工作流实例 + * + * @param id 实例ID + * @return 工作流实例 + */ + WorkflowInstanceDTO startInstance(Long id); + + /** + * 暂停工作流实例 + * + * @param id 实例ID + * @return 工作流实例 + */ + WorkflowInstanceDTO pauseInstance(Long id); + + /** + * 恢复工作流实例 + * + * @param id 实例ID + * @return 工作流实例 + */ + WorkflowInstanceDTO resumeInstance(Long id); + + /** + * 终止工作流实例 + * + * @param id 实例ID + * @param reason 终止原因 + * @return 工作流实例 + */ + WorkflowInstanceDTO terminateInstance(Long id, String reason); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java index 9083781d..78364409 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java @@ -1,11 +1,23 @@ package com.qqchen.deploy.backend.workflow.service.impl; +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO; +import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository; import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService; +import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Map; /** * 工作流实例服务实现类 @@ -13,4 +25,122 @@ import org.springframework.stereotype.Service; @Slf4j @Service public class WorkflowInstanceServiceImpl extends BaseServiceImpl implements IWorkflowInstanceService { -} \ No newline at end of file + + @Resource + private IWorkflowDefinitionRepository workflowDefinitionRepository; + + @Resource + private IWorkflowInstanceRepository workflowInstanceRepository; + + @Resource + private IWorkflowVariableService workflowVariableService; + + @Override + @Transactional + public WorkflowInstanceDTO createInstance(Long definitionId, String businessKey, Map variables) { + // 1. 查询工作流定义 + WorkflowDefinition definition = workflowDefinitionRepository.findById(definitionId) + .orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND)); + + // 2. 检查工作流定义状态 + if (definition.getStatus() != WorkflowStatusEnum.PUBLISHED) { + throw new BusinessException(ResponseCode.WORKFLOW_NOT_PUBLISHED); + } + if (!definition.getEnabled()) { + throw new BusinessException(ResponseCode.WORKFLOW_DISABLED); + } + + // 3. 创建工作流实例 + WorkflowInstance instance = new WorkflowInstance(); + instance.setDefinition(definition); + instance.setBusinessKey(businessKey); + instance.setStatus(WorkflowStatusEnum.CREATED); + final WorkflowInstance savedInstance = workflowInstanceRepository.save(instance); + + // 4. 设置初始变量 + if (variables != null && !variables.isEmpty()) { + variables.forEach((key, value) -> workflowVariableService.setVariable(savedInstance.getId(), key, value)); + } + + return converter.toDto(savedInstance); + } + + @Override + @Transactional + public WorkflowInstanceDTO startInstance(Long id) { + // 1. 查询工作流实例 + WorkflowInstance instance = workflowInstanceRepository.findById(id) + .orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND)); + + // 2. 检查状态 + if (instance.getStatus() != WorkflowStatusEnum.CREATED) { + throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING); + } + + // 3. 更新状态 + instance.setStatus(WorkflowStatusEnum.RUNNING); + instance.setStartTime(LocalDateTime.now()); + instance = workflowInstanceRepository.save(instance); + + return converter.toDto(instance); + } + + @Override + @Transactional + public WorkflowInstanceDTO pauseInstance(Long id) { + // 1. 查询工作流实例 + WorkflowInstance instance = workflowInstanceRepository.findById(id) + .orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND)); + + // 2. 检查状态 + if (!instance.getStatus().canPause()) { + throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING); + } + + // 3. 更新状态 + instance.setStatus(WorkflowStatusEnum.PAUSED); + instance = workflowInstanceRepository.save(instance); + + return converter.toDto(instance); + } + + @Override + @Transactional + public WorkflowInstanceDTO resumeInstance(Long id) { + // 1. 查询工作流实例 + WorkflowInstance instance = workflowInstanceRepository.findById(id) + .orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND)); + + // 2. 检查状态 + if (!instance.getStatus().canResume()) { + throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING); + } + + // 3. 更新状态 + instance.setStatus(WorkflowStatusEnum.RUNNING); + instance = workflowInstanceRepository.save(instance); + + return converter.toDto(instance); + } + + @Override + @Transactional + public WorkflowInstanceDTO terminateInstance(Long id, String reason) { + // 1. 查询工作流实例 + WorkflowInstance instance = workflowInstanceRepository.findById(id) + .orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND)); + + // 2. 检查状态 + if (instance.getStatus().isFinalStatus()) { + throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING); + } + + // 3. ���新状态 + instance.setStatus(WorkflowStatusEnum.TERMINATED); + instance.setEndTime(LocalDateTime.now()); + instance.setError(reason); + instance = workflowInstanceRepository.save(instance); + + return converter.toDto(instance); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index bd11bd81..f99d50bf 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -45,7 +45,7 @@ CREATE TABLE sys_department ( CONSTRAINT UK_department_code UNIQUE (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表'; --- 用户表 +-- 用���表 CREATE TABLE IF NOT EXISTS sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', create_by VARCHAR(255) NULL COMMENT '创建人', @@ -394,7 +394,7 @@ CREATE TABLE wf_workflow_definition ( code VARCHAR(100) NOT NULL COMMENT '工作流编码', name VARCHAR(100) NOT NULL COMMENT '工作流名称', description VARCHAR(255) NULL COMMENT '工作流描述', - status VARCHAR(20) NOT NULL COMMENT '工作流状态(DRAFT/PUBLISHED/DISABLED)', + status TINYINT NOT NULL COMMENT '工作流状态(0:草稿,1:已发布,2:已禁用)', version_no INT NOT NULL DEFAULT 1 COMMENT '版本号', node_config TEXT NULL COMMENT '节点配置(JSON)', transition_config TEXT NULL COMMENT '流转配置(JSON)', @@ -418,7 +418,7 @@ CREATE TABLE wf_node_definition ( workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID', node_id VARCHAR(100) NOT NULL COMMENT '节点ID', name VARCHAR(100) NOT NULL COMMENT '节点名称', - type VARCHAR(20) NOT NULL COMMENT '节点类型(START/END/TASK/GATEWAY)', + type TINYINT NOT NULL COMMENT '节点类型(0:开始节点,1:结束节点,2:任务节点,3:网关节点,4:子流程节点,5:Shell脚本节点)', config TEXT NULL COMMENT '节点配置(JSON)', order_num INT NOT NULL DEFAULT 0 COMMENT '排序号', @@ -438,7 +438,7 @@ CREATE TABLE wf_workflow_instance ( workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID', business_key VARCHAR(100) NOT NULL COMMENT '业务标识', - status VARCHAR(20) NOT NULL COMMENT '状态(PENDING/RUNNING/COMPLETED/FAILED/CANCELLED)', + status TINYINT NOT NULL COMMENT '状态(3:已创建,4:等待执行,5:执行中,6:已暂停,7:已完成,8:执行失败,9:已取消,10:已暂停,11:已终止)', start_time DATETIME(6) NULL COMMENT '开始时间', end_time DATETIME(6) NULL COMMENT '结束时间', error TEXT NULL COMMENT '错误信息', @@ -459,9 +459,9 @@ CREATE TABLE wf_node_instance ( workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID', node_id VARCHAR(100) NOT NULL COMMENT '节点ID', - node_type VARCHAR(20) NOT NULL COMMENT '节点类型', + node_type TINYINT NOT NULL COMMENT '节点类型(0:开始节点,1:结束节点,2:任务节点,3:网关节点,4:子流程节点,5:Shell脚本节点)', name VARCHAR(100) NOT NULL COMMENT '节点名称', - status VARCHAR(20) NOT NULL COMMENT '状态(PENDING/RUNNING/COMPLETED/FAILED/CANCELLED/SKIPPED)', + status TINYINT NOT NULL COMMENT '状态(3:已创建,4:等待执行,5:执行中,6:已暂停,7:已完成,8:执行失败,9:已取消,10:已暂停,11:已终止)', start_time DATETIME(6) NULL COMMENT '开始时间', end_time DATETIME(6) NULL COMMENT '结束时间', config TEXT NULL COMMENT '节点配置(JSON)', diff --git a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql index 2966d377..cceeef48 100644 --- a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql +++ b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql @@ -163,7 +163,7 @@ INSERT INTO wf_workflow_definition ( ) VALUES ( 1, 'admin', NOW(), 0, 'admin', NOW(), 0, 'DEPLOY_WORKFLOW', '标准部署流程', '标准的应用部署流程,包含代码拉取、编译构建、部署等步骤', - 'PUBLISHED', 1, + 1, 1, '{"startNode":{"type":"START","name":"开始"},"endNode":{"type":"END","name":"结束"},"taskNodes":[{"id":"build","type":"TASK","name":"构建"},{"id":"deploy","type":"TASK","name":"部署"}]}', '{"transitions":[{"from":"startNode","to":"build"},{"from":"build","to":"deploy"},{"from":"deploy","to":"endNode"}]}', '{"fields":[{"name":"appName","label":"应用名称","type":"input","required":true},{"name":"branch","label":"分支","type":"select","required":true}]}', @@ -178,22 +178,22 @@ INSERT INTO wf_node_definition ( ) VALUES -- 开始节点 (1, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'startNode', '开始', 'START', + 1, 'startNode', '开始', 0, '{"type":"START","name":"开始"}', 1), -- 构建节点 (2, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'build', '构建', 'TASK', + 1, 'build', '构建', 2, '{"type":"TASK","name":"构建","executor":"JENKINS","jenkinsJob":"app-build"}', 2), -- 部署节点 (3, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'deploy', '部署', 'TASK', + 1, 'deploy', '部署', 2, '{"type":"TASK","name":"部署","executor":"SHELL","script":"./deploy.sh"}', 3), -- 结束节点 (4, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'endNode', '结束', 'END', + 1, 'endNode', '结束', 1, '{"type":"END","name":"结束"}', 4); @@ -203,7 +203,7 @@ INSERT INTO wf_workflow_instance ( workflow_definition_id, business_key, status, start_time, end_time, error ) VALUES ( 1, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'TEST-APP-001', 'RUNNING', + 1, 'TEST-APP-001', 5, NOW(), NULL, NULL ); @@ -214,25 +214,25 @@ INSERT INTO wf_node_instance ( ) VALUES -- 开始节点实例 (1, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'startNode', 'START', '开始', 'COMPLETED', + 1, 'startNode', 0, '开始', 7, DATE_SUB(NOW(), INTERVAL 5 MINUTE), DATE_SUB(NOW(), INTERVAL 4 MINUTE), '{"type":"START","name":"开始"}', '{"appName":"test-app","branch":"master"}', '{}', NULL, NULL), -- 构建节点实例 (2, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'build', 'TASK', '构建', 'RUNNING', + 1, 'build', 2, '构建', 5, DATE_SUB(NOW(), INTERVAL 3 MINUTE), NULL, '{"type":"TASK","name":"构建","executor":"JENKINS","jenkinsJob":"app-build"}', '{"appName":"test-app","branch":"master"}', NULL, NULL, 'startNode'), -- 部署节点实例 (3, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'deploy', 'TASK', '部署', 'RUNNING', + 1, 'deploy', 2, '部署', 5, DATE_SUB(NOW(), INTERVAL 2 MINUTE), NULL, '{"type":"TASK","name":"部署","executor":"SHELL","script":"./deploy.sh"}', '{"appName":"test-app","branch":"master"}', NULL, NULL, 'build'), -- 结束节点实例 (4, 'admin', NOW(), 0, 'admin', NOW(), 0, - 1, 'endNode', 'END', '结束', 'COMPLETED', + 1, 'endNode', 1, '结束', 7, DATE_SUB(NOW(), INTERVAL 1 MINUTE), DATE_SUB(NOW(), INTERVAL 0 MINUTE), '{"type":"END","name":"结束"}', '{}', '{}', NULL, 'deploy'); diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngineTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngineTest.java new file mode 100644 index 00000000..fddbaae4 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngineTest.java @@ -0,0 +1,301 @@ +package com.qqchen.deploy.backend.workflow.engine; + +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.workflow.engine.context.DefaultWorkflowContext; +import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext; +import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException; +import com.qqchen.deploy.backend.workflow.engine.executor.NodeExecutor; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; +import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum; +import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; +import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; +import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class DefaultWorkflowEngineTest { + + @Mock + private IWorkflowDefinitionRepository workflowDefinitionRepository; + + @Mock + private IWorkflowInstanceRepository workflowInstanceRepository; + + @Mock + private INodeInstanceRepository nodeInstanceRepository; + + @Mock + private Map nodeExecutors; + + @Mock + private DefaultWorkflowContext.Factory workflowContextFactory; + + @Mock + private NodeExecutor startNodeExecutor; + + @Mock + private NodeExecutor taskNodeExecutor; + + @Mock + private DefaultWorkflowContext workflowContext; + + @InjectMocks + private DefaultWorkflowEngine workflowEngine; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + // 配置基本的Mock行为 + when(nodeExecutors.get(NodeTypeEnum.START)).thenReturn(startNodeExecutor); + when(nodeExecutors.get(NodeTypeEnum.TASK)).thenReturn(taskNodeExecutor); + when(workflowContextFactory.create(any())).thenReturn(workflowContext); + } + + @Test + void startWorkflow_Success() { + // 准备测试数据 + String workflowCode = "test-workflow"; + String businessKey = "test-key"; + Map variables = new HashMap<>(); + variables.put("key1", "value1"); + + WorkflowDefinition definition = new WorkflowDefinition(); + definition.setCode(workflowCode); + definition.setStatus(WorkflowStatusEnum.PUBLISHED); + + NodeInstance startNode = new NodeInstance(); + startNode.setId(1L); + startNode.setNodeType(NodeTypeEnum.START); + startNode.setStatus(NodeStatusEnum.PENDING); + + // 配置Mock行为 + when(workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode)).thenReturn(definition); + when(workflowInstanceRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + when(nodeInstanceRepository.save(any())).thenReturn(startNode); + doNothing().when(startNodeExecutor).execute(any(), any()); + + // 执行测试 + WorkflowInstance result = workflowEngine.startWorkflow(workflowCode, businessKey, variables); + + // 验证结果 + assertNotNull(result); + assertEquals(WorkflowStatusEnum.RUNNING, result.getStatus()); + assertNotNull(result.getStartTime()); + + verify(workflowDefinitionRepository).findByCodeAndDeletedFalse(workflowCode); + verify(workflowInstanceRepository).save(any()); + verify(nodeInstanceRepository).save(any()); + verify(workflowContextFactory).create(any()); + verify(workflowContext, times(variables.size())).setVariable(anyString(), any()); + } + + @Test + void startWorkflow_WorkflowNotFound() { + // 准备测试数据 + String workflowCode = "non-existent"; + + // 配置Mock行为 + when(workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode)).thenReturn(null); + + // 执行测试并验证异常 + WorkflowEngineException exception = assertThrows(WorkflowEngineException.class, + () -> workflowEngine.startWorkflow(workflowCode, "test", null)); + assertEquals(ResponseCode.WORKFLOW_NOT_FOUND.name(), exception.getMessage()); + } + + @Test + void startWorkflow_WorkflowNotPublished() { + // 准备测试数据 + String workflowCode = "draft-workflow"; + WorkflowDefinition definition = new WorkflowDefinition(); + definition.setCode(workflowCode); + definition.setStatus(WorkflowStatusEnum.DRAFT); + + // 配置Mock行为 + when(workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode)).thenReturn(definition); + + // 执行测试并验证异常 + WorkflowEngineException exception = assertThrows(WorkflowEngineException.class, + () -> workflowEngine.startWorkflow(workflowCode, "test", null)); + assertEquals(ResponseCode.WORKFLOW_NOT_PUBLISHED.name(), exception.getMessage()); + } + + @Test + void executeNode_Success() { + // 准备测试数据 + Long nodeInstanceId = 1L; + NodeInstance nodeInstance = new NodeInstance(); + nodeInstance.setId(nodeInstanceId); + nodeInstance.setNodeType(NodeTypeEnum.TASK); + nodeInstance.setStatus(NodeStatusEnum.PENDING); + + WorkflowInstance instance = new WorkflowInstance(); + instance.setStatus(WorkflowStatusEnum.RUNNING); + nodeInstance.setWorkflowInstance(instance); + + // 配置Mock行为 + when(nodeInstanceRepository.findById(nodeInstanceId)).thenReturn(Optional.of(nodeInstance)); + doNothing().when(taskNodeExecutor).execute(any(), any()); + + // 执行测试 + workflowEngine.executeNode(nodeInstanceId); + + // 验证结果 + assertEquals(NodeStatusEnum.COMPLETED, nodeInstance.getStatus()); + assertNotNull(nodeInstance.getEndTime()); + + verify(nodeInstanceRepository).findById(nodeInstanceId); + verify(taskNodeExecutor).execute(any(), any()); + verify(nodeInstanceRepository).save(nodeInstance); + } + + @Test + void executeNode_NodeNotFound() { + // 准备测试数据 + Long nodeInstanceId = 999L; + + // 配置Mock行为 + when(nodeInstanceRepository.findById(nodeInstanceId)).thenReturn(Optional.empty()); + + // 执行测试并验证异常 + WorkflowEngineException exception = assertThrows(WorkflowEngineException.class, + () -> workflowEngine.executeNode(nodeInstanceId)); + assertEquals(ResponseCode.WORKFLOW_NODE_NOT_FOUND.name(), exception.getMessage()); + } + + @Test + void executeNode_WorkflowNotRunning() { + // 准备测试数据 + Long nodeInstanceId = 1L; + NodeInstance nodeInstance = new NodeInstance(); + nodeInstance.setId(nodeInstanceId); + nodeInstance.setNodeType(NodeTypeEnum.TASK); + + WorkflowInstance instance = new WorkflowInstance(); + instance.setStatus(WorkflowStatusEnum.COMPLETED); + nodeInstance.setWorkflowInstance(instance); + + // 配置Mock行为 + when(nodeInstanceRepository.findById(nodeInstanceId)).thenReturn(Optional.of(nodeInstance)); + + // 执行测试并验证异常 + WorkflowEngineException exception = assertThrows(WorkflowEngineException.class, + () -> workflowEngine.executeNode(nodeInstanceId)); + assertEquals(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING.name(), exception.getMessage()); + } + + @Test + void terminateWorkflow_Success() { + // 准备测试数据 + Long instanceId = 1L; + String reason = "Test termination"; + + WorkflowInstance instance = new WorkflowInstance(); + instance.setId(instanceId); + instance.setStatus(WorkflowStatusEnum.RUNNING); + + NodeInstance runningNode = new NodeInstance(); + runningNode.setNodeType(NodeTypeEnum.TASK); + runningNode.setStatus(NodeStatusEnum.RUNNING); + + // 配置Mock行为 + when(workflowInstanceRepository.findById(instanceId)).thenReturn(Optional.of(instance)); + when(nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING)) + .thenReturn(Arrays.asList(runningNode)); + doNothing().when(taskNodeExecutor).terminate(any(), any()); + + // 执行测试 + workflowEngine.terminateWorkflow(instanceId, reason); + + // 验证结果 + assertEquals(WorkflowStatusEnum.TERMINATED, instance.getStatus()); + assertEquals(reason, instance.getError()); + assertNotNull(instance.getEndTime()); + assertEquals(NodeStatusEnum.TERMINATED, runningNode.getStatus()); + assertNotNull(runningNode.getEndTime()); + + verify(workflowInstanceRepository).findById(instanceId); + verify(nodeInstanceRepository).findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING); + verify(taskNodeExecutor).terminate(any(), any()); + verify(nodeInstanceRepository).save(runningNode); + verify(workflowInstanceRepository).save(instance); + } + + @Test + void pauseWorkflow_Success() { + // 准备测试数据 + Long instanceId = 1L; + WorkflowInstance instance = new WorkflowInstance(); + instance.setId(instanceId); + instance.setStatus(WorkflowStatusEnum.RUNNING); + + NodeInstance runningNode = new NodeInstance(); + runningNode.setStatus(NodeStatusEnum.RUNNING); + + // 配置Mock行为 + when(workflowInstanceRepository.findById(instanceId)).thenReturn(Optional.of(instance)); + when(nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING)) + .thenReturn(Arrays.asList(runningNode)); + + // 执行测试 + workflowEngine.pauseWorkflow(instanceId); + + // 验证结果 + assertEquals(WorkflowStatusEnum.PAUSED, instance.getStatus()); + assertEquals(NodeStatusEnum.PAUSED, runningNode.getStatus()); + + verify(workflowInstanceRepository).findById(instanceId); + verify(nodeInstanceRepository).findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING); + verify(nodeInstanceRepository).save(runningNode); + verify(workflowInstanceRepository).save(instance); + } + + @Test + void resumeWorkflow_Success() { + // 准备测试数据 + Long instanceId = 1L; + WorkflowInstance instance = new WorkflowInstance(); + instance.setId(instanceId); + instance.setStatus(WorkflowStatusEnum.PAUSED); + + NodeInstance pausedNode = new NodeInstance(); + pausedNode.setId(2L); + pausedNode.setStatus(NodeStatusEnum.PAUSED); + + // 配置Mock行为 + when(workflowInstanceRepository.findById(instanceId)).thenReturn(Optional.of(instance)); + when(nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.PAUSED)) + .thenReturn(Arrays.asList(pausedNode)); + + // 执行测试 + workflowEngine.resumeWorkflow(instanceId); + + // 验证结果 + assertEquals(WorkflowStatusEnum.RUNNING, instance.getStatus()); + assertEquals(NodeStatusEnum.RUNNING, pausedNode.getStatus()); + + verify(workflowInstanceRepository).findById(instanceId); + verify(nodeInstanceRepository).findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.PAUSED); + verify(nodeInstanceRepository).save(pausedNode); + verify(workflowInstanceRepository).save(instance); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/StartNodeExecutorTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/StartNodeExecutorTest.java new file mode 100644 index 00000000..46967681 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/StartNodeExecutorTest.java @@ -0,0 +1,64 @@ +package com.qqchen.deploy.backend.workflow.engine.executor; + +import com.qqchen.deploy.backend.system.enums.LogLevelEnum; +import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum; +import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; + +class StartNodeExecutorTest { + + @Mock + private WorkflowContext workflowContext; + + @InjectMocks + private StartNodeExecutor startNodeExecutor; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getNodeType_ShouldReturnStartType() { + assertEquals(NodeTypeEnum.START, startNodeExecutor.getNodeType()); + } + + @Test + void execute_ShouldCompleteNodeAndLog() { + // 准备测试数据 + NodeInstance nodeInstance = new NodeInstance(); + nodeInstance.setNodeType(NodeTypeEnum.START); + nodeInstance.setStatus(NodeStatusEnum.PENDING); + + // 执行测试 + startNodeExecutor.execute(nodeInstance, workflowContext); + + // 验证结果 + assertEquals(NodeStatusEnum.COMPLETED, nodeInstance.getStatus()); + verify(workflowContext).log("开始节点执行完成", LogLevelEnum.INFO); + } + + @Test + void validate_ShouldDoNothing() { + // 开始节点无需配置,验证方法不会抛出异常 + startNodeExecutor.validate(null); + startNodeExecutor.validate(""); + startNodeExecutor.validate("{}"); + } + + @Test + void terminate_ShouldDoNothing() { + // 开始节点无需终止操作,验证方法不会抛出异常 + NodeInstance nodeInstance = new NodeInstance(); + startNodeExecutor.terminate(nodeInstance, workflowContext); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutorTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutorTest.java new file mode 100644 index 00000000..5bbbe530 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutorTest.java @@ -0,0 +1,175 @@ +//package com.qqchen.deploy.backend.workflow.engine.executor; +// +//import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext; +//import com.qqchen.deploy.backend.workflow.engine.executor.task.TaskExecutor; +//import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +//import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +//import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum; +//import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; +//import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.MockitoAnnotations; +// +//import java.util.HashMap; +//import java.util.Map; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.Mockito.*; +// +//class TaskNodeExecutorTest { +// +// @Mock +// private INodeInstanceRepository nodeInstanceRepository; +// +// @Mock +// private Map taskExecutors; +// +// @Mock +// private TaskExecutor shellTaskExecutor; +// +// @Mock +// private WorkflowContext context; +// +// @InjectMocks +// private TaskNodeExecutor taskNodeExecutor; +// +// @BeforeEach +// void setUp() { +// MockitoAnnotations.openMocks(this); +// +// // 设置任务执行器映射 +// when(taskExecutors.get("SHELL")).thenReturn(shellTaskExecutor); +// } +// +// @Test +// void testExecute() { +// // 准备测试数据 +// WorkflowInstance instance = new WorkflowInstance(); +// instance.setId(1L); +// +// NodeInstance taskNode = new NodeInstance(); +// taskNode.setId(1L); +// taskNode.setNodeType(NodeTypeEnum.TASK); +// taskNode.setStatus(NodeStatusEnum.PENDING); +// taskNode.setConfig("{\"type\":\"SHELL\",\"script\":\"echo hello\"}"); +// +// Map input = new HashMap<>(); +// input.put("param1", "value1"); +// +// // 设置Mock行为 +// when(context.getWorkflowInstance()).thenReturn(instance); +// when(context.getVariables()).thenReturn(input); +// when(shellTaskExecutor.execute(any(), any())).thenReturn(true); +// +// // 执行测试 +// boolean result = taskNodeExecutor.execute(context, taskNode); +// +// // 验证结果 +// assertTrue(result); +// assertEquals(NodeStatusEnum.RUNNING, taskNode.getStatus()); +// assertNotNull(taskNode.getStartTime()); +// verify(nodeInstanceRepository, times(1)).save(taskNode); +// verify(shellTaskExecutor, times(1)).execute(any(), any()); +// } +// +// @Test +// void testExecuteWithError() { +// // 准备测试数据 +// WorkflowInstance instance = new WorkflowInstance(); +// instance.setId(1L); +// +// NodeInstance taskNode = new NodeInstance(); +// taskNode.setId(1L); +// taskNode.setNodeType(NodeTypeEnum.TASK); +// taskNode.setStatus(NodeStatusEnum.PENDING); +// taskNode.setConfig("{\"type\":\"SHELL\",\"script\":\"echo hello\"}"); +// +// // 设置Mock行为 +// when(context.getWorkflowInstance()).thenReturn(instance); +// when(shellTaskExecutor.execute(any(), any())).thenReturn(false); +// +// // 执行测试 +// boolean result = taskNodeExecutor.execute(context, taskNode); +// +// // 验证结果 +// assertFalse(result); +// assertEquals(NodeStatusEnum.FAILED, taskNode.getStatus()); +// assertNotNull(taskNode.getStartTime()); +// assertNotNull(taskNode.getEndTime()); +// verify(nodeInstanceRepository, times(1)).save(taskNode); +// verify(shellTaskExecutor, times(1)).execute(any(), any()); +// } +// +// @Test +// void testCanExecute() { +// // 准备测试数据 +// NodeInstance taskNode = new NodeInstance(); +// taskNode.setNodeType(NodeTypeEnum.TASK); +// taskNode.setStatus(NodeStatusEnum.PENDING); +// +// // 执行测试 +// boolean result = taskNodeExecutor.canExecute(taskNode); +// +// // 验证结果 +// assertTrue(result); +// } +// +// @Test +// void testCannotExecuteWrongType() { +// // 准备测试数据 +// NodeInstance startNode = new NodeInstance(); +// startNode.setNodeType(NodeTypeEnum.START); +// startNode.setStatus(NodeStatusEnum.PENDING); +// +// // 执行测试 +// boolean result = taskNodeExecutor.canExecute(startNode); +// +// // 验证结果 +// assertFalse(result); +// } +// +// @Test +// void testCannotExecuteWrongStatus() { +// // 准备测试数据 +// NodeInstance taskNode = new NodeInstance(); +// taskNode.setNodeType(NodeTypeEnum.TASK); +// taskNode.setStatus(NodeStatusEnum.RUNNING); +// +// // 执行测试 +// boolean result = taskNodeExecutor.canExecute(taskNode); +// +// // 验证结果 +// assertFalse(result); +// } +// +// @Test +// void testExecuteWithInvalidConfig() { +// // 准备测试数据 +// WorkflowInstance instance = new WorkflowInstance(); +// instance.setId(1L); +// +// NodeInstance taskNode = new NodeInstance(); +// taskNode.setId(1L); +// taskNode.setNodeType(NodeTypeEnum.TASK); +// taskNode.setStatus(NodeStatusEnum.PENDING); +// taskNode.setConfig("invalid json"); +// +// // 设置Mock行为 +// when(context.getWorkflowInstance()).thenReturn(instance); +// +// // 执行测试 +// boolean result = taskNodeExecutor.execute(context, taskNode); +// +// // 验证结果 +// assertFalse(result); +// assertEquals(NodeStatusEnum.FAILED, taskNode.getStatus()); +// assertNotNull(taskNode.getStartTime()); +// assertNotNull(taskNode.getEndTime()); +// assertNotNull(taskNode.getError()); +// verify(nodeInstanceRepository, times(1)).save(taskNode); +// } +//} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java new file mode 100644 index 00000000..435c4f7b --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java @@ -0,0 +1,162 @@ +package com.qqchen.deploy.backend.workflow.service; + +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO; +import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; +import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; +import com.qqchen.deploy.backend.workflow.repository.INodeDefinitionRepository; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; +import com.qqchen.deploy.backend.workflow.service.impl.WorkflowDefinitionServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class WorkflowDefinitionServiceTest { + + @Mock + private IWorkflowDefinitionRepository workflowDefinitionRepository; + + @Mock + private INodeDefinitionRepository nodeDefinitionRepository; + + @InjectMocks + private WorkflowDefinitionServiceImpl workflowDefinitionService; + + private WorkflowDefinitionDTO dto; + private WorkflowDefinition entity; + + @BeforeEach + void setUp() { + dto = new WorkflowDefinitionDTO(); + dto.setCode("TEST-WF"); + dto.setName("Test Workflow"); + dto.setDescription("Test Description"); + dto.setNodeConfig("{}"); + dto.setTransitionConfig("{}"); + dto.setFormDefinition("{}"); + dto.setGraphDefinition("{}"); + + entity = new WorkflowDefinition(); + entity.setId(1L); + entity.setCode("TEST-WF"); + entity.setName("Test Workflow"); + entity.setStatus(WorkflowStatusEnum.DRAFT); + entity.setEnabled(true); + } + + @Test + void create_Success() { + when(workflowDefinitionRepository.existsByCodeAndDeletedFalse(anyString())).thenReturn(false); + when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(entity); + + WorkflowDefinitionDTO result = workflowDefinitionService.create(dto); + + assertNotNull(result); + assertEquals(WorkflowStatusEnum.DRAFT, result.getStatus()); + assertTrue(result.getEnabled()); + verify(workflowDefinitionRepository).existsByCodeAndDeletedFalse(dto.getCode()); + verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class)); + } + + @Test + void create_CodeExists_ThrowsException() { + when(workflowDefinitionRepository.existsByCodeAndDeletedFalse(anyString())).thenReturn(true); + + BusinessException exception = assertThrows(BusinessException.class, + () -> workflowDefinitionService.create(dto)); + assertEquals(ResponseCode.WORKFLOW_CODE_EXISTS, exception.getErrorCode()); + } + + @Test + void publish_Success() { + entity.setStatus(WorkflowStatusEnum.DRAFT); + when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(entity)); + when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(entity); + + WorkflowDefinitionDTO result = workflowDefinitionService.publish(1L); + + assertNotNull(result); + assertEquals(WorkflowStatusEnum.PUBLISHED, result.getStatus()); + verify(workflowDefinitionRepository).findById(1L); + verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class)); + } + + @Test + void publish_NotDraft_ThrowsException() { + entity.setStatus(WorkflowStatusEnum.PUBLISHED); + when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(entity)); + + BusinessException exception = assertThrows(BusinessException.class, + () -> workflowDefinitionService.publish(1L)); + assertEquals(ResponseCode.WORKFLOW_NOT_DRAFT, exception.getErrorCode()); + } + + @Test + void disable_Success() { + entity.setStatus(WorkflowStatusEnum.PUBLISHED); + when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(entity)); + when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(entity); + + WorkflowDefinitionDTO result = workflowDefinitionService.disable(1L); + + assertNotNull(result); + assertEquals(WorkflowStatusEnum.DISABLED, result.getStatus()); + verify(workflowDefinitionRepository).findById(1L); + verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class)); + } + + @Test + void enable_Success() { + entity.setStatus(WorkflowStatusEnum.DISABLED); + when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(entity)); + when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(entity); + + WorkflowDefinitionDTO result = workflowDefinitionService.enable(1L); + + assertNotNull(result); + assertEquals(WorkflowStatusEnum.PUBLISHED, result.getStatus()); + verify(workflowDefinitionRepository).findById(1L); + verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class)); + } + + @Test + void createNewVersion_Success() { + when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(entity)); + when(workflowDefinitionRepository.findLatestVersionByCode(anyString())).thenReturn(1); + when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(entity); + + WorkflowDefinitionDTO result = workflowDefinitionService.createNewVersion(1L); + + assertNotNull(result); + assertEquals(WorkflowStatusEnum.DRAFT, result.getStatus()); + assertTrue(result.getEnabled()); + verify(workflowDefinitionRepository).findById(1L); + verify(workflowDefinitionRepository).findLatestVersionByCode(entity.getCode()); + verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class)); + } + + @Test + void findAllVersions_Success() { + when(workflowDefinitionRepository.findAllVersionsByCode(anyString())) + .thenReturn(Arrays.asList(entity)); + + var results = workflowDefinitionService.findAllVersions("TEST-WF"); + + assertNotNull(results); + assertFalse(results.isEmpty()); + assertEquals(1, results.size()); + verify(workflowDefinitionRepository).findAllVersionsByCode("TEST-WF"); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowInstanceServiceTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowInstanceServiceTest.java new file mode 100644 index 00000000..e41fb904 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowInstanceServiceTest.java @@ -0,0 +1,187 @@ +//package com.qqchen.deploy.backend.workflow.service; +// +//import com.qqchen.deploy.backend.framework.enums.ResponseCode; +//import com.qqchen.deploy.backend.framework.exception.BusinessException; +//import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO; +//import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; +//import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +//import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; +//import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; +//import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository; +//import com.qqchen.deploy.backend.workflow.service.impl.WorkflowInstanceServiceImpl; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.extension.ExtendWith; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.junit.jupiter.MockitoExtension; +// +//import java.util.HashMap; +//import java.util.Map; +//import java.util.Optional; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.ArgumentMatchers.anyLong; +//import static org.mockito.Mockito.*; +// +//@ExtendWith(MockitoExtension.class) +//class WorkflowInstanceServiceTest { +// +// @Mock +// private IWorkflowDefinitionRepository workflowDefinitionRepository; +// +// @Mock +// private IWorkflowInstanceRepository workflowInstanceRepository; +// +// @Mock +// private IWorkflowVariableService workflowVariableService; +// +// @InjectMocks +// private WorkflowInstanceServiceImpl workflowInstanceService; +// +// private WorkflowDefinition definition; +// private WorkflowInstance instance; +// private Map variables; +// +// @BeforeEach +// void setUp() { +// definition = new WorkflowDefinition(); +// definition.setId(1L); +// definition.setCode("TEST-WF"); +// definition.setName("Test Workflow"); +// definition.setStatus(WorkflowStatusEnum.PUBLISHED); +// definition.setEnabled(true); +// +// instance = new WorkflowInstance(); +// instance.setId(1L); +// instance.setDefinition(definition); +// instance.setBusinessKey("TEST-KEY"); +// instance.setStatus(WorkflowStatusEnum.CREATED); +// +// variables = new HashMap<>(); +// variables.put("key1", "value1"); +// variables.put("key2", "value2"); +// } +// +// @Test +// void createInstance_Success() { +// when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(definition)); +// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance); +// +// WorkflowInstanceDTO result = workflowInstanceService.createInstance(1L, "TEST-KEY", variables); +// +// assertNotNull(result); +// assertEquals(WorkflowStatusEnum.CREATED, result.getStatus()); +// assertEquals("TEST-KEY", result.getBusinessKey()); +// verify(workflowDefinitionRepository).findById(1L); +// verify(workflowInstanceRepository).save(any(WorkflowInstance.class)); +// verify(workflowVariableService, times(2)).setVariable(anyLong(), anyString(), any()); +// } +// +// @Test +// void createInstance_WorkflowNotFound_ThrowsException() { +// when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.empty()); +// +// BusinessException exception = assertThrows(BusinessException.class, +// () -> workflowInstanceService.createInstance(1L, "TEST-KEY", variables)); +// assertEquals(ResponseCode.WORKFLOW_NOT_FOUND, exception.getCode()); +// } +// +// @Test +// void createInstance_WorkflowNotPublished_ThrowsException() { +// definition.setStatus(WorkflowStatusEnum.DRAFT); +// when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(definition)); +// +// BusinessException exception = assertThrows(BusinessException.class, +// () -> workflowInstanceService.createInstance(1L, "TEST-KEY", variables)); +// assertEquals(ResponseCode.WORKFLOW_NOT_PUBLISHED, exception.getCode()); +// } +// +// @Test +// void startInstance_Success() { +// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance)); +// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance); +// +// WorkflowInstanceDTO result = workflowInstanceService.startInstance(1L); +// +// assertNotNull(result); +// assertEquals(WorkflowStatusEnum.RUNNING, result.getStatus()); +// assertNotNull(result.getStartTime()); +// verify(workflowInstanceRepository).findById(1L); +// verify(workflowInstanceRepository).save(any(WorkflowInstance.class)); +// } +// +// @Test +// void startInstance_NotFound_ThrowsException() { +// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.empty()); +// +// BusinessException exception = assertThrows(BusinessException.class, +// () -> workflowInstanceService.startInstance(1L)); +// assertEquals(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND, exception.getCode()); +// } +// +// @Test +// void startInstance_NotCreated_ThrowsException() { +// instance.setStatus(WorkflowStatusEnum.RUNNING); +// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance)); +// +// BusinessException exception = assertThrows(BusinessException.class, +// () -> workflowInstanceService.startInstance(1L)); +// assertEquals(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING, exception.getCode()); +// } +// +// @Test +// void pauseInstance_Success() { +// instance.setStatus(WorkflowStatusEnum.RUNNING); +// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance)); +// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance); +// +// WorkflowInstanceDTO result = workflowInstanceService.pauseInstance(1L); +// +// assertNotNull(result); +// assertEquals(WorkflowStatusEnum.PAUSED, result.getStatus()); +// verify(workflowInstanceRepository).findById(1L); +// verify(workflowInstanceRepository).save(any(WorkflowInstance.class)); +// } +// +// @Test +// void resumeInstance_Success() { +// instance.setStatus(WorkflowStatusEnum.PAUSED); +// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance)); +// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance); +// +// WorkflowInstanceDTO result = workflowInstanceService.resumeInstance(1L); +// +// assertNotNull(result); +// assertEquals(WorkflowStatusEnum.RUNNING, result.getStatus()); +// verify(workflowInstanceRepository).findById(1L); +// verify(workflowInstanceRepository).save(any(WorkflowInstance.class)); +// } +// +// @Test +// void terminateInstance_Success() { +// instance.setStatus(WorkflowStatusEnum.RUNNING); +// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance)); +// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance); +// +// WorkflowInstanceDTO result = workflowInstanceService.terminateInstance(1L, "Test reason"); +// +// assertNotNull(result); +// assertEquals(WorkflowStatusEnum.TERMINATED, result.getStatus()); +// assertEquals("Test reason", result.getError()); +// assertNotNull(result.getEndTime()); +// verify(workflowInstanceRepository).findById(1L); +// verify(workflowInstanceRepository).save(any(WorkflowInstance.class)); +// } +// +// @Test +// void terminateInstance_AlreadyTerminated_ThrowsException() { +// instance.setStatus(WorkflowStatusEnum.TERMINATED); +// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance)); +// +// BusinessException exception = assertThrows(BusinessException.class, +// () -> workflowInstanceService.terminateInstance(1L, "Test reason")); +// assertEquals(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING, exception.getCode()); +// } +//} \ No newline at end of file