docs(guidelines): 更新鉴权规范文档并移除需求服务单元测试
- 在 CLAUDE.md 中补充对象域权限校验的第二种正规写法:属主放行 + 权限码兜底 - 移除 ProductRequirementServiceImplTest.java 文件中的所有单元测试代码 - 更新 ProjectExecutionServiceImpl 添加属主放行的权限校验实现 - 在执行服务中新增 requireExecutionEditDeletePermission 方法处理负责人权限 - 修正执行更新、删除及删除预检接口的权限验证逻辑
This commit is contained in:
@@ -78,7 +78,8 @@
|
|||||||
红线:
|
红线:
|
||||||
|
|
||||||
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。
|
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。
|
||||||
- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。
|
- **对象域权限校验必须落在 Service 层**,默认写法是方法上挂 `@CheckObjectPermission`,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionAssigneeServiceImpl` 等)保持一致。
|
||||||
|
- **第二种正规写法:属主放行 + 权限码兜底(方法体内裁决)**。任务、执行的创建 / 编辑 / 删除 / 删除预检采用"项目/执行/任务负责人身份命中直接放行,未命中回落对象域权限码"的口径,权限裁决在方法体内调 `ProjectObjectAuthorizationService`(入口见 `ProjectTaskServiceImpl.requireTaskEditDeletePermission` / `ProjectExecutionServiceImpl.requireExecutionEditDeletePermission`)。这些方法**故意不挂** `@CheckObjectPermission`——注解是前置拦截,会把"无权限码的负责人"挡死。**不要误判为漏鉴权把注解补回去**;盘点权限面时除了搜注解,还要搜 `ProjectObjectAuthorizationService` 的调用点。口径来源:`docs/superpowers/specs/2026-07-07-执行编辑删除权限口径-design.md`。
|
||||||
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
||||||
- 对象内**读路径**(列表 / 详情 / 状态看板 / 聚合)同样要挂 `@CheckObjectPermission(objectType=PROJECT, permission=...PERMISSION_QUERY)`——查询同样扫库耗资源,必须按对象域鉴权。**Controller 方法层一律不挂权限注解**,新增读接口照此在 Service 层挂对象域权限;不要只在 Controller 留空,更不要误判"Controller 没注解 = 无鉴权"。
|
- 对象内**读路径**(列表 / 详情 / 状态看板 / 聚合)同样要挂 `@CheckObjectPermission(objectType=PROJECT, permission=...PERMISSION_QUERY)`——查询同样扫库耗资源,必须按对象域鉴权。**Controller 方法层一律不挂权限注解**,新增读接口照此在 Service 层挂对象域权限;不要只在 Controller 留空,更不要误判"Controller 没注解 = 无鉴权"。
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ public final class ProjectTaskConstants {
|
|||||||
/**
|
/**
|
||||||
* 删除任务权限码。
|
* 删除任务权限码。
|
||||||
* 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。
|
* 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。
|
||||||
* 实际拦截:service 内按 task.parentTaskId 分流走 checkOwnerOrProjectPermission
|
* 实际拦截:项目负责人 / 执行负责人 / 任务负责人 / 父任务负责人直接放行;
|
||||||
* (一级任务 → execution.ownerId 字段身份;子任务 → parentTask.ownerId 字段身份;不命中字段时回落本权限码)。
|
* 不命中业务身份时回落本权限码,作为管理员兜底。
|
||||||
*/
|
*/
|
||||||
public static final String PERMISSION_DELETE = "project:task:delete";
|
public static final String PERMISSION_DELETE = "project:task:delete";
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ public class ProjectTaskController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{taskId}")
|
@DeleteMapping("/{taskId}")
|
||||||
@Operation(summary = "删除任务(已完成态禁止,三重确认 + 上级 owner 硬卡)")
|
@Operation(summary = "删除任务(已完成态禁止,三重确认 + 多身份授权)")
|
||||||
public CommonResult<Boolean> deleteTask(@PathVariable("projectId") Long projectId,
|
public CommonResult<Boolean> deleteTask(@PathVariable("projectId") Long projectId,
|
||||||
@PathVariable("executionId") Long executionId,
|
@PathVariable("executionId") Long executionId,
|
||||||
@PathVariable("taskId") Long taskId,
|
@PathVariable("taskId") Long taskId,
|
||||||
|
|||||||
@@ -4,10 +4,17 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目对象业务授权服务。
|
* 项目对象业务授权服务:属主(业务负责人)身份命中直接放行,未命中回落对象域权限码兜底。
|
||||||
|
*
|
||||||
|
* <p>供任务 / 执行的创建、编辑、删除、删除预检等入口在<b>方法体内</b>调用,替代方法上的
|
||||||
|
* {@code @CheckObjectPermission} 前置拦截——注解拦截发生在进入方法体之前,无法先认负责人身份。
|
||||||
|
*
|
||||||
|
* <p>红线:调用本服务做裁决的 service 方法<b>不要再挂 {@code @CheckObjectPermission}</b>,
|
||||||
|
* 叠加会把"无权限码的负责人"在注解层直接挡死。口径登记见 CLAUDE.md §鉴权。
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ProjectObjectAuthorizationService {
|
public class ProjectObjectAuthorizationService {
|
||||||
@@ -26,4 +33,15 @@ public class ProjectObjectAuthorizationService {
|
|||||||
projectObjectPermissionService.checkPermission(projectId, permission, false);
|
projectObjectPermissionService.checkPermission(projectId, permission, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前登录人命中任一业务负责人时直接放行,否则回落到项目对象角色权限校验。
|
||||||
|
*/
|
||||||
|
public void checkAnyOwnerOrProjectPermission(Long projectId, Collection<Long> ownerUserIds, String permission) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (ownerUserIds != null && ownerUserIds.stream().anyMatch(ownerUserId -> Objects.equals(loginUserId, ownerUserId))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
projectObjectPermissionService.checkPermission(projectId, permission, false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
|||||||
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
||||||
import com.njcn.rdms.module.project.util.DueRangeSupport;
|
import com.njcn.rdms.module.project.util.DueRangeSupport;
|
||||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
|
import com.njcn.rdms.module.project.framework.security.service.ProjectObjectAuthorizationService;
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectService;
|
import com.njcn.rdms.module.project.service.project.ProjectService;
|
||||||
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
||||||
@@ -128,6 +129,8 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ProjectRequirementMapper projectRequirementMapper;
|
private ProjectRequirementMapper projectRequirementMapper;
|
||||||
@Resource
|
@Resource
|
||||||
|
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
||||||
|
@Resource
|
||||||
private StatusActionTextResolver statusActionTextResolver;
|
private StatusActionTextResolver statusActionTextResolver;
|
||||||
/** 站内信事件发布:创建执行后发「执行指派」通知,由 NotifySendEventListener 事务提交后统一发送。 */
|
/** 站内信事件发布:创建执行后发「执行指派」通知,由 NotifySendEventListener 事务提交后统一发送。 */
|
||||||
@Resource
|
@Resource
|
||||||
@@ -201,16 +204,16 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限:方法体内属主放行 + 权限码兜底(requireExecutionEditDeletePermission),勿挂 @CheckObjectPermission
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
|
||||||
permission = ProjectExecutionConstants.PERMISSION_UPDATE)
|
|
||||||
public void updateExecution(Long projectId, ProjectExecutionUpdateReqVO reqVO) {
|
public void updateExecution(Long projectId, ProjectExecutionUpdateReqVO reqVO) {
|
||||||
if (reqVO.getId() == null) {
|
if (reqVO.getId() == null) {
|
||||||
throw invalidParamException("执行编号不能为空");
|
throw invalidParamException("执行编号不能为空");
|
||||||
}
|
}
|
||||||
validateEditableProject(projectId);
|
ProjectDO project = validateEditableProject(projectId);
|
||||||
ProjectExecutionDO execution = validateExecutionExists(projectId, reqVO.getId());
|
ProjectExecutionDO execution = validateExecutionExists(projectId, reqVO.getId());
|
||||||
|
requireExecutionEditDeletePermission(project, execution, ProjectExecutionConstants.PERMISSION_UPDATE);
|
||||||
validateExecutionAllowEdit(execution);
|
validateExecutionAllowEdit(execution);
|
||||||
projectRequirementService.validateUsableForExecution(projectId, reqVO.getProjectRequirementId());
|
projectRequirementService.validateUsableForExecution(projectId, reqVO.getProjectRequirementId());
|
||||||
String executionName = normalizeRequiredName(reqVO.getExecutionName());
|
String executionName = normalizeRequiredName(reqVO.getExecutionName());
|
||||||
@@ -448,13 +451,13 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN, reason);
|
ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限:方法体内属主放行 + 权限码兜底(requireExecutionEditDeletePermission),勿挂 @CheckObjectPermission
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
|
||||||
permission = ProjectExecutionConstants.PERMISSION_DELETE)
|
|
||||||
public void deleteExecution(Long projectId, Long executionId, ProjectExecutionDeleteReqVO reqVO) {
|
public void deleteExecution(Long projectId, Long executionId, ProjectExecutionDeleteReqVO reqVO) {
|
||||||
validateProjectExists(projectId);
|
ProjectDO project = validateProjectExists(projectId);
|
||||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||||
|
requireExecutionEditDeletePermission(project, execution, ProjectExecutionConstants.PERMISSION_DELETE);
|
||||||
validateDeleteConfirmText(reqVO.getConfirmText());
|
validateDeleteConfirmText(reqVO.getConfirmText());
|
||||||
if (!Objects.equals(execution.getExecutionName(), reqVO.getExecutionName().trim())) {
|
if (!Objects.equals(execution.getExecutionName(), reqVO.getExecutionName().trim())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_DELETE_NAME_MISMATCH);
|
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_DELETE_NAME_MISMATCH);
|
||||||
@@ -482,12 +485,12 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
writeExecutionAuditLog(execution, ObjectActivityConstants.EXECUTION_ACTION_DELETE, fromStatus, null, null, reason);
|
writeExecutionAuditLog(execution, ObjectActivityConstants.EXECUTION_ACTION_DELETE, fromStatus, null, null, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限:方法体内属主放行 + 权限码兜底(requireExecutionEditDeletePermission),勿挂 @CheckObjectPermission
|
||||||
@Override
|
@Override
|
||||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
|
||||||
permission = ProjectExecutionConstants.PERMISSION_DELETE)
|
|
||||||
public ProjectExecutionDeletePrecheckRespVO precheckDeleteExecution(Long projectId, Long executionId) {
|
public ProjectExecutionDeletePrecheckRespVO precheckDeleteExecution(Long projectId, Long executionId) {
|
||||||
validateProjectExists(projectId);
|
ProjectDO project = validateProjectExists(projectId);
|
||||||
validateExecutionExists(projectId, executionId);
|
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||||
|
requireExecutionEditDeletePermission(project, execution, ProjectExecutionConstants.PERMISSION_DELETE);
|
||||||
int taskCount = projectTaskMapper.countByExecutionId(executionId);
|
int taskCount = projectTaskMapper.countByExecutionId(executionId);
|
||||||
ProjectExecutionDeletePrecheckRespVO respVO = new ProjectExecutionDeletePrecheckRespVO();
|
ProjectExecutionDeletePrecheckRespVO respVO = new ProjectExecutionDeletePrecheckRespVO();
|
||||||
respVO.setTaskCount(taskCount);
|
respVO.setTaskCount(taskCount);
|
||||||
@@ -495,6 +498,27 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
return respVO;
|
return respVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行编辑/删除授权:项目负责人、执行负责人直接放行;其它用户回落对象域权限码。
|
||||||
|
*
|
||||||
|
* <p>红线:调用本方法的入口(updateExecution / deleteExecution / precheckDeleteExecution)<b>故意不挂
|
||||||
|
* {@code @CheckObjectPermission}</b>——注解是前置拦截,会把"无权限码的负责人"挡死。
|
||||||
|
* 不要误判为漏鉴权把注解补回去;口径见 CLAUDE.md §鉴权。
|
||||||
|
*/
|
||||||
|
private void requireExecutionEditDeletePermission(ProjectDO project, ProjectExecutionDO execution,
|
||||||
|
String permission) {
|
||||||
|
List<Long> ownerUserIds = new ArrayList<>();
|
||||||
|
addIfNotNull(ownerUserIds, project.getManagerUserId());
|
||||||
|
addIfNotNull(ownerUserIds, execution.getOwnerId());
|
||||||
|
projectObjectAuthorizationService.checkAnyOwnerOrProjectPermission(project.getId(), ownerUserIds, permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIfNotNull(Collection<Long> values, Long value) {
|
||||||
|
if (value != null) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void validateDeleteConfirmText(String confirmText) {
|
private void validateDeleteConfirmText(String confirmText) {
|
||||||
String normalizedConfirmText = normalizeNullableText(confirmText);
|
String normalizedConfirmText = normalizeNullableText(confirmText);
|
||||||
if (normalizedConfirmText == null
|
if (normalizedConfirmText == null
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ public interface ProjectTaskService {
|
|||||||
void changeTaskStatus(Long projectId, Long executionId, Long taskId, ProjectTaskStatusActionReqVO reqVO);
|
void changeTaskStatus(Long projectId, Long executionId, Long taskId, ProjectTaskStatusActionReqVO reqVO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除任务(软删 + 三重确认 + 上级硬卡 + 级联软删 + 父进度回算 + 审计)。
|
* 删除任务(软删 + 三重确认 + 多身份授权 + 级联软删 + 父进度回算 + 审计)。
|
||||||
* <p>删除规则:非 completed 即可删(pending / active / paused / cancelled 全允许);
|
* <p>删除规则:非 completed 即可删(pending / active / paused / cancelled 全允许);
|
||||||
* 已 completed 的任务用专门错误码 PROJECT_TASK_NOT_ALLOW_DELETE 拦截。
|
* 已 completed 的任务用专门错误码 PROJECT_TASK_NOT_ALLOW_DELETE 拦截。
|
||||||
* <p>上级硬卡:一级任务由"执行负责人 OR 项目负责人(权限码 {@code project:task:delete})"删;
|
* <p>授权:项目负责人 / 执行负责人 / 任务负责人 / 父任务负责人均可删;
|
||||||
* 子任务由"父任务负责人 OR 项目负责人(权限码)"删。
|
* 其它用户持有 {@code project:task:delete} 时作为管理员兜底放行。
|
||||||
* <p>三重确认:{@code taskName} / {@code confirmText}(DELETE 或 删除)/ {@code reason} 全部必填,
|
* <p>三重确认:{@code taskName} / {@code confirmText}(DELETE 或 删除)/ {@code reason} 全部必填,
|
||||||
* 任意一项缺失或不匹配即拒。
|
* 任意一项缺失或不匹配即拒。
|
||||||
* <p>级联范围:软删该任务及其所有子孙任务、所有相关工作日志、所有相关任务协办;
|
* <p>级联范围:软删该任务及其所有子孙任务、所有相关工作日志、所有相关任务协办;
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
@Resource
|
@Resource
|
||||||
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
// 权限:方法体内属主放行 + 权限码兜底(checkOwnerOrProjectPermission),勿挂 @CheckObjectPermission
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Long createTask(Long projectId, Long executionId, ProjectTaskSaveReqVO reqVO) {
|
public Long createTask(Long projectId, Long executionId, ProjectTaskSaveReqVO reqVO) {
|
||||||
@@ -215,24 +216,19 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限:方法体内属主放行 + 权限码兜底(requireTaskEditDeletePermission),勿挂 @CheckObjectPermission
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void updateTask(Long projectId, Long executionId, ProjectTaskSaveReqVO reqVO) {
|
public void updateTask(Long projectId, Long executionId, ProjectTaskSaveReqVO reqVO) {
|
||||||
if (reqVO.getId() == null) {
|
if (reqVO.getId() == null) {
|
||||||
throw invalidParamException("任务编号不能为空");
|
throw invalidParamException("任务编号不能为空");
|
||||||
}
|
}
|
||||||
validateEditableProject(projectId);
|
ProjectDO project = validateEditableProject(projectId);
|
||||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||||
validateExecutionAllowEdit(execution);
|
validateExecutionAllowEdit(execution);
|
||||||
ProjectTaskDO task = validateTaskExists(projectId, executionId, reqVO.getId());
|
ProjectTaskDO task = validateTaskExists(projectId, executionId, reqVO.getId());
|
||||||
validateTaskAllowEdit(task);
|
validateTaskAllowEdit(task);
|
||||||
// 上级硬卡:一级任务由"执行负责人 OR 项目负责人(权限码)"改;子任务由"父任务负责人 OR 项目负责人(权限码)"改
|
requireTaskEditDeletePermission(project, execution, task, ProjectTaskConstants.PERMISSION_UPDATE);
|
||||||
// 按当前 task 的 parentTaskId(修改前)判定;改父亲属于改动结果,授权按改动前形态裁决
|
|
||||||
Long upperOwnerId = task.getParentTaskId() == null
|
|
||||||
? execution.getOwnerId()
|
|
||||||
: projectTaskMapper.selectById(task.getParentTaskId()).getOwnerId();
|
|
||||||
projectObjectAuthorizationService.checkOwnerOrProjectPermission(
|
|
||||||
projectId, upperOwnerId, ProjectTaskConstants.PERMISSION_UPDATE);
|
|
||||||
ProjectTaskDO parentTask = validateParentTask(projectId, executionId, reqVO.getParentTaskId());
|
ProjectTaskDO parentTask = validateParentTask(projectId, executionId, reqVO.getParentTaskId());
|
||||||
if (parentTask != null && Objects.equals(parentTask.getId(), task.getId())) {
|
if (parentTask != null && Objects.equals(parentTask.getId(), task.getId())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_TASK_PARENT_INVALID);
|
throw exception(ErrorCodeConstants.PROJECT_TASK_PARENT_INVALID);
|
||||||
@@ -281,14 +277,14 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
task.getStatusCode(), buildTaskFieldChanges(before, task), null);
|
task.getStatusCode(), buildTaskFieldChanges(before, task), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限:方法体内属主放行 + 权限码兜底(requireTaskEditDeletePermission),勿挂 @CheckObjectPermission
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteTask(Long projectId, Long executionId, Long taskId, ProjectTaskDeleteReqVO reqVO) {
|
public void deleteTask(Long projectId, Long executionId, Long taskId, ProjectTaskDeleteReqVO reqVO) {
|
||||||
validateProjectExists(projectId);
|
ProjectDO project = validateProjectExists(projectId);
|
||||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||||
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
||||||
// 上级硬卡:一级任务由"执行负责人 OR 项目负责人(权限码)"删;子任务由"父任务负责人 OR 项目负责人(权限码)"删
|
requireTaskEditDeletePermission(project, execution, task, ProjectTaskConstants.PERMISSION_DELETE);
|
||||||
requireDeletePermissionOnTask(projectId, task, execution);
|
|
||||||
validateDeleteConfirmText(reqVO.getConfirmText());
|
validateDeleteConfirmText(reqVO.getConfirmText());
|
||||||
if (!Objects.equals(task.getTaskTitle(), reqVO.getTaskName().trim())) {
|
if (!Objects.equals(task.getTaskTitle(), reqVO.getTaskName().trim())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_TASK_DELETE_NAME_MISMATCH);
|
throw exception(ErrorCodeConstants.PROJECT_TASK_DELETE_NAME_MISMATCH);
|
||||||
@@ -326,13 +322,14 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_DELETE, fromStatus, null, null, reason);
|
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_DELETE, fromStatus, null, null, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限:方法体内属主放行 + 权限码兜底(requireTaskEditDeletePermission),勿挂 @CheckObjectPermission
|
||||||
@Override
|
@Override
|
||||||
public ProjectTaskDeletePrecheckRespVO precheckDeleteTask(Long projectId, Long executionId, Long taskId) {
|
public ProjectTaskDeletePrecheckRespVO precheckDeleteTask(Long projectId, Long executionId, Long taskId) {
|
||||||
validateProjectExists(projectId);
|
ProjectDO project = validateProjectExists(projectId);
|
||||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||||
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
||||||
// 预检也走"上级硬卡",防止"无权限的人调 precheck 试探下挂数量"
|
// 预检也走同款编辑/删除授权,防止无权限用户调 precheck 试探下挂数量。
|
||||||
requireDeletePermissionOnTask(projectId, task, execution);
|
requireTaskEditDeletePermission(project, execution, task, ProjectTaskConstants.PERMISSION_DELETE);
|
||||||
|
|
||||||
int childTaskCount = projectTaskMapper.countChildrenByParentTaskId(taskId);
|
int childTaskCount = projectTaskMapper.countChildrenByParentTaskId(taskId);
|
||||||
int worklogCount = taskWorklogMapper.countByTaskId(taskId);
|
int worklogCount = taskWorklogMapper.countByTaskId(taskId);
|
||||||
@@ -345,26 +342,32 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务删除"上级硬卡":一级任务由执行负责人 OR 项目负责人(权限码)操作;
|
* 任务编辑/删除授权:项目负责人、执行负责人、任务负责人、父任务负责人均可操作;
|
||||||
* 子任务由父任务负责人 OR 项目负责人(权限码)操作。
|
* 其它用户回落到项目对象角色权限码,作为管理员兜底。
|
||||||
* <p>同时被 deleteTask 与 precheckDeleteTask 使用,保证删除路径与预检入口走同款权限通道。
|
*
|
||||||
|
* <p>红线:调用本方法的入口(updateTask / deleteTask / precheckDeleteTask)<b>故意不挂
|
||||||
|
* {@code @CheckObjectPermission}</b>——注解是前置拦截,会把"无权限码的负责人"挡死。
|
||||||
|
* 不要误判为漏鉴权把注解补回去;口径见 CLAUDE.md §鉴权。
|
||||||
*/
|
*/
|
||||||
private void requireDeletePermissionOnTask(Long projectId, ProjectTaskDO task, ProjectExecutionDO execution) {
|
private void requireTaskEditDeletePermission(ProjectDO project, ProjectExecutionDO execution,
|
||||||
Long upperOwnerId;
|
ProjectTaskDO task, String permission) {
|
||||||
if (task.getParentTaskId() == null) {
|
List<Long> ownerUserIds = new ArrayList<>();
|
||||||
upperOwnerId = execution.getOwnerId();
|
addIfNotNull(ownerUserIds, project.getManagerUserId());
|
||||||
} else {
|
addIfNotNull(ownerUserIds, execution.getOwnerId());
|
||||||
|
addIfNotNull(ownerUserIds, task.getOwnerId());
|
||||||
|
if (task.getParentTaskId() != null) {
|
||||||
ProjectTaskDO parent = projectTaskMapper.selectById(task.getParentTaskId());
|
ProjectTaskDO parent = projectTaskMapper.selectById(task.getParentTaskId());
|
||||||
if (parent == null) {
|
if (parent != null) {
|
||||||
// 父任务被并发硬删的极端场景:当前任务已是孤儿,按"无上级负责人"处理
|
addIfNotNull(ownerUserIds, parent.getOwnerId());
|
||||||
// 仍要求调用方持有项目级权限码,由 checkOwnerOrProjectPermission 兜底拦截
|
|
||||||
upperOwnerId = null;
|
|
||||||
} else {
|
|
||||||
upperOwnerId = parent.getOwnerId();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
projectObjectAuthorizationService.checkOwnerOrProjectPermission(
|
projectObjectAuthorizationService.checkAnyOwnerOrProjectPermission(project.getId(), ownerUserIds, permission);
|
||||||
projectId, upperOwnerId, ProjectTaskConstants.PERMISSION_DELETE);
|
}
|
||||||
|
|
||||||
|
private void addIfNotNull(Collection<Long> values, Long value) {
|
||||||
|
if (value != null) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateDeleteConfirmText(String confirmText) {
|
private void validateDeleteConfirmText(String confirmText) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -42,6 +44,30 @@ class ProjectObjectAuthorizationServiceTest extends BaseMockitoUnitTest {
|
|||||||
verify(projectObjectPermissionService).checkPermission(projectId, "project:task:status", false);
|
verify(projectObjectPermissionService).checkPermission(projectId, "project:task:status", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkAnyOwnerOrProjectPermission_whenCurrentUserMatchesAnyOwner_shouldSkipProjectPermissionCheck() {
|
||||||
|
Long projectId = 1003L;
|
||||||
|
|
||||||
|
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3002L)) {
|
||||||
|
authorizationService.checkAnyOwnerOrProjectPermission(projectId,
|
||||||
|
List.of(3001L, 3002L, 3003L), "project:task:update");
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(projectObjectPermissionService, never()).checkPermission(projectId, "project:task:update", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkAnyOwnerOrProjectPermission_whenCurrentUserMatchesNoOwner_shouldCheckProjectPermission() {
|
||||||
|
Long projectId = 1004L;
|
||||||
|
|
||||||
|
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3999L)) {
|
||||||
|
authorizationService.checkAnyOwnerOrProjectPermission(projectId,
|
||||||
|
List.of(3001L, 3002L, 3003L), "project:task:delete");
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(projectObjectPermissionService).checkPermission(projectId, "project:task:delete", false);
|
||||||
|
}
|
||||||
|
|
||||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId) {
|
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId) {
|
||||||
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
||||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||||
|
|||||||
@@ -1,726 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.service.product;
|
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
|
||||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
|
||||||
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.MockedStatic;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 产品需求 Service 单元测试
|
|
||||||
*/
|
|
||||||
class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private ProductRequirementServiceImpl requirementService;
|
|
||||||
@Mock
|
|
||||||
private ProductRequirementMapper requirementMapper;
|
|
||||||
@Mock
|
|
||||||
private ProductRequirementModuleMapper moduleMapper;
|
|
||||||
@Mock
|
|
||||||
private ProductRequirementStatusLogMapper statusLogMapper;
|
|
||||||
@Mock
|
|
||||||
private BizAuditLogMapper bizAuditLogMapper;
|
|
||||||
@Mock
|
|
||||||
private ObjectStatusTransitionMapper statusTransitionMapper;
|
|
||||||
@Mock
|
|
||||||
private ObjectStatusModelMapper statusModelMapper;
|
|
||||||
@Mock
|
|
||||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
|
||||||
@Mock
|
|
||||||
private com.njcn.rdms.module.project.dal.mysql.product.ProductMapper productMapper;
|
|
||||||
@Mock
|
|
||||||
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
|
||||||
|
|
||||||
// ========== 创建需求通知处理人测试(Task 9) ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task 9:创建产品需求后通知处理人(currentHandlerUserId),操作人由监听器统一排除。
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void createRequirement_notifiesHandler() {
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
|
|
||||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(9L);
|
|
||||||
ProductRequirementDO req = new ProductRequirementDO();
|
|
||||||
req.setId(1L);
|
|
||||||
req.setProductId(20L);
|
|
||||||
req.setTitle("电池告警");
|
|
||||||
req.setCurrentHandlerUserId(5L);
|
|
||||||
com.njcn.rdms.module.project.dal.dataobject.product.ProductDO product =
|
|
||||||
new com.njcn.rdms.module.project.dal.dataobject.product.ProductDO();
|
|
||||||
product.setName("智能终端");
|
|
||||||
when(productMapper.selectById(20L)).thenReturn(product);
|
|
||||||
|
|
||||||
ArgumentCaptor<com.njcn.rdms.module.project.framework.notify.NotifySendEvent> captor =
|
|
||||||
ArgumentCaptor.forClass(com.njcn.rdms.module.project.framework.notify.NotifySendEvent.class);
|
|
||||||
requirementService.publishRequirementAssignedNotify(req);
|
|
||||||
verify(applicationEventPublisher).publishEvent(captor.capture());
|
|
||||||
com.njcn.rdms.module.project.framework.notify.NotifySendEvent ev = captor.getValue();
|
|
||||||
assertEquals("product_requirement_assigned", ev.getTemplateCode());
|
|
||||||
assertEquals(9L, ev.getOperatorUserId());
|
|
||||||
assertTrue(ev.getUserIds().contains(5L));
|
|
||||||
assertEquals("电池告警", ev.getParams().get("requirementTitle"));
|
|
||||||
assertEquals("智能终端", ev.getParams().get("productName"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task 9:处理人为空时不发通知。
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void createRequirement_whenHandlerNull_shouldNotPublish() {
|
|
||||||
ProductRequirementDO req = new ProductRequirementDO();
|
|
||||||
req.setId(1L);
|
|
||||||
req.setProductId(20L);
|
|
||||||
req.setTitle("电池告警");
|
|
||||||
req.setCurrentHandlerUserId(null);
|
|
||||||
|
|
||||||
requirementService.publishRequirementAssignedNotify(req);
|
|
||||||
verify(applicationEventPublisher, never()).publishEvent(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 创建需求测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createRequirement_withoutReview_shouldCreateWithPendingDispatchStatus() {
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
Long defaultModuleId = 50L;
|
|
||||||
ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO();
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setTitle("测试需求");
|
|
||||||
reqVO.setDescription("测试描述");
|
|
||||||
reqVO.setCategory("function");
|
|
||||||
reqVO.setReviewRequired(0); // 不需要评审
|
|
||||||
reqVO.setPriority(1);
|
|
||||||
reqVO.setProposerId(2001L);
|
|
||||||
reqVO.setProposerNickname("proposer-a");
|
|
||||||
reqVO.setCurrentHandlerUserId(2002L);
|
|
||||||
reqVO.setCurrentHandlerUserNickname("handler-a");
|
|
||||||
|
|
||||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
|
||||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
|
||||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.createRequirement(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
|
||||||
verify(requirementMapper, times(1)).insert(captor.capture());
|
|
||||||
ProductRequirementDO created = captor.getValue();
|
|
||||||
assertEquals("pending_dispatch", created.getStatusCode()); // 不需要评审时初始状态为待指派
|
|
||||||
assertEquals("manual", created.getSourceType());
|
|
||||||
assertEquals(0L, created.getParentId());
|
|
||||||
assertEquals("测试需求", created.getTitle());
|
|
||||||
assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
|
||||||
assertEquals(100L, created.getProductId());
|
|
||||||
assertEquals("handler-a", created.getCurrentHandlerUserNickname());
|
|
||||||
|
|
||||||
ArgumentCaptor<BizAuditLogDO> auditCaptor = ArgumentCaptor.forClass(BizAuditLogDO.class);
|
|
||||||
verify(bizAuditLogMapper, times(1)).insert(auditCaptor.capture());
|
|
||||||
assertNotNull(auditCaptor.getValue().getFieldChanges());
|
|
||||||
assertTrue(auditCaptor.getValue().getFieldChanges().contains("currentHandlerUserNickname"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createRequirement_withReview_shouldCreateWithPendingReviewStatus() {
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
Long defaultModuleId = 50L;
|
|
||||||
ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO();
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setTitle("需评审的需求");
|
|
||||||
reqVO.setCategory("function");
|
|
||||||
reqVO.setReviewRequired(1); // 需要评审
|
|
||||||
reqVO.setPriority(2);
|
|
||||||
reqVO.setProposerId(2001L);
|
|
||||||
|
|
||||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
|
||||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
|
||||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.createRequirement(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
|
||||||
verify(requirementMapper, times(1)).insert(captor.capture());
|
|
||||||
ProductRequirementDO created = captor.getValue();
|
|
||||||
assertEquals("pending_review", created.getStatusCode()); // 需要评审时初始状态为待评审
|
|
||||||
assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 更新需求测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateRequirement_whenTerminalStatus_shouldThrowException() {
|
|
||||||
Long requirementId = 1001L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求",
|
|
||||||
"closed", 0L, 0);
|
|
||||||
ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setTitle("修改标题");
|
|
||||||
reqVO.setCategory("function");
|
|
||||||
reqVO.setReviewRequired(0);
|
|
||||||
reqVO.setPriority(1);
|
|
||||||
reqVO.setProposerId(2001L);
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.updateRequirement(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT.getCode(), ex.getCode());
|
|
||||||
verify(requirementMapper, never()).updateById(any(ProductRequirementDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateRequirement_whenNormalStatus_shouldUpdateSuccessfully() {
|
|
||||||
Long requirementId = 1002L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
Long defaultModuleId = 50L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
|
||||||
"pending_dispatch", 0L, 0);
|
|
||||||
ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setTitle("修改后的标题");
|
|
||||||
reqVO.setCategory("security");
|
|
||||||
reqVO.setReviewRequired(0);
|
|
||||||
reqVO.setPriority(2);
|
|
||||||
reqVO.setProposerId(2001L);
|
|
||||||
|
|
||||||
// 模拟"全部需求"模块存在(未选择模块时自动归属,parentId = 0L 的根模块)
|
|
||||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
|
||||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.updateRequirement(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
|
||||||
verify(requirementMapper, times(1)).updateById(captor.capture());
|
|
||||||
assertEquals(defaultModuleId, captor.getValue().getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
|
||||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 状态变更测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeRequirementStatus_whenActionAllowed_shouldUpdateStatusAndWriteLogs() {
|
|
||||||
Long requirementId = 1003L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
|
||||||
"pending_dispatch", 0L, 0);
|
|
||||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setActionCode("dispatch");
|
|
||||||
reqVO.setReason(null);
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
|
||||||
"product_requirement", "pending_dispatch", "dispatch"))
|
|
||||||
.thenReturn(createTransition("dispatch", "implementing", false));
|
|
||||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null))
|
|
||||||
.thenReturn(1);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.changeRequirementStatus(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requirementMapper, times(1))
|
|
||||||
.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null);
|
|
||||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
|
||||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeRequirementStatus_whenActionNotAllowed_shouldThrowException() {
|
|
||||||
Long requirementId = 1004L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求",
|
|
||||||
"closed", 0L, 0);
|
|
||||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setActionCode("dispatch");
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
|
||||||
"product_requirement", "closed", "dispatch"))
|
|
||||||
.thenReturn(null);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.changeRequirementStatus(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeRequirementStatus_whenReasonRequiredButMissing_shouldThrowException() {
|
|
||||||
Long requirementId = 1005L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
|
||||||
"pending_dispatch", 0L, 0);
|
|
||||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setActionCode("cancel");
|
|
||||||
reqVO.setReason(" "); // 空原因
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
|
||||||
"product_requirement", "pending_dispatch", "cancel"))
|
|
||||||
.thenReturn(createTransition("cancel", "cancelled", true));
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.changeRequirementStatus(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeRequirementStatus_whenConcurrentModified_shouldThrowException() {
|
|
||||||
Long requirementId = 1006L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
|
||||||
"pending_dispatch", 0L, 0);
|
|
||||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setActionCode("dispatch");
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
|
||||||
"product_requirement", "pending_dispatch", "dispatch"))
|
|
||||||
.thenReturn(createTransition("dispatch", "implementing", false));
|
|
||||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null))
|
|
||||||
.thenReturn(0); // 并发修改,更新失败
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.changeRequirementStatus(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
verify(statusLogMapper, never()).insert(any(ProductRequirementStatusLogDO.class));
|
|
||||||
verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 删除需求测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void deleteRequirement_shouldDeleteByConditionAndWriteLogs() {
|
|
||||||
Long requirementId = 1007L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
|
||||||
"pending_dispatch", 0L, 0);
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
when(requirementMapper.deleteByIdAndStatus(requirementId, "pending_dispatch")).thenReturn(1);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.deleteRequirement(requirementId, 100L);
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requirementMapper, times(1)).deleteByIdAndStatus(requirementId, "pending_dispatch");
|
|
||||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 拆分需求测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void splitRequirement_whenParentPendingDispatch_shouldChangeParentToImplementing() {
|
|
||||||
Long parentId = 1008L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "大需求",
|
|
||||||
"pending_dispatch", 0L, 0);
|
|
||||||
parent.setModuleId(50L);
|
|
||||||
parent.setProposerId(2001L);
|
|
||||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
|
||||||
reqVO.setParentId(parentId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setTitle("子需求");
|
|
||||||
reqVO.setCategory("function");
|
|
||||||
reqVO.setPriority(1);
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
|
||||||
when(requirementMapper.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null))
|
|
||||||
.thenReturn(1);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.splitRequirement(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证子需求被创建
|
|
||||||
ArgumentCaptor<ProductRequirementDO> childCaptor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
|
||||||
verify(requirementMapper, times(1)).insert(childCaptor.capture());
|
|
||||||
ProductRequirementDO child = childCaptor.getValue();
|
|
||||||
assertEquals(parentId, child.getParentId());
|
|
||||||
assertEquals("pending_dispatch", child.getStatusCode());
|
|
||||||
assertEquals(parent.getModuleId(), child.getModuleId());
|
|
||||||
assertEquals(reqVO.getProposerId(), child.getProposerId());
|
|
||||||
|
|
||||||
// 验证父需求状态变为实施中
|
|
||||||
verify(requirementMapper, times(1))
|
|
||||||
.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null);
|
|
||||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void splitRequirement_whenParentImplementing_shouldNotChangeParentStatus() {
|
|
||||||
Long parentId = 1009L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "实施中的大需求",
|
|
||||||
"implementing", 0L, 0);
|
|
||||||
parent.setModuleId(50L);
|
|
||||||
parent.setProposerId(2001L);
|
|
||||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
|
||||||
reqVO.setParentId(parentId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setTitle("子需求2");
|
|
||||||
reqVO.setCategory("function");
|
|
||||||
reqVO.setPriority(1);
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.splitRequirement(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 父需求已经是实施中,不需要再更新状态
|
|
||||||
verify(requirementMapper, never())
|
|
||||||
.updateStatusByIdAndStatus(parentId, "implementing", "implementing", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void splitRequirement_whenParentNotAllowSplit_shouldThrowException() {
|
|
||||||
Long parentId = 1010L;
|
|
||||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "已验收的大需求",
|
|
||||||
"accepted", 0L, 0);
|
|
||||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
|
||||||
reqVO.setParentId(parentId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setTitle("子需求");
|
|
||||||
reqVO.setCategory("function");
|
|
||||||
reqVO.setPriority(1);
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.splitRequirement(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT.getCode(), ex.getCode());
|
|
||||||
verify(requirementMapper, never()).insert(any(ProductRequirementDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 关闭需求测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void closeRequirement_whenAccepted_shouldCloseSuccessfully() {
|
|
||||||
Long requirementId = 1011L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已验收需求",
|
|
||||||
"accepted", 0L, 0);
|
|
||||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setReason("需求已完成");
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
when(requirementMapper.selectListByParentId(requirementId)).thenReturn(Collections.emptyList());
|
|
||||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成"))
|
|
||||||
.thenReturn(1);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.closeRequirement(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requirementMapper, times(1))
|
|
||||||
.updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成");
|
|
||||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
|
||||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void closeRequirement_whenNotAccepted_shouldThrowException() {
|
|
||||||
Long requirementId = 1012L;
|
|
||||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "实施中需求",
|
|
||||||
"implementing", 0L, 0);
|
|
||||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
|
||||||
reqVO.setId(requirementId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setReason("需求已完成");
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.closeRequirement(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void closeRequirement_withChildren_shouldCascadeCloseAcceptedChildren() {
|
|
||||||
Long parentId = 1013L;
|
|
||||||
Long childId = 1014L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求",
|
|
||||||
"accepted", 0L, 0);
|
|
||||||
ProductRequirementDO child = createRequirement(childId, 100L, "子需求",
|
|
||||||
"accepted", parentId, 0);
|
|
||||||
|
|
||||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
|
||||||
reqVO.setId(parentId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setReason("全部验收完成");
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
|
||||||
when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child));
|
|
||||||
when(requirementMapper.updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成"))
|
|
||||||
.thenReturn(1);
|
|
||||||
when(requirementMapper.updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成"))
|
|
||||||
.thenReturn(1);
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.closeRequirement(reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 子需求被关闭
|
|
||||||
verify(requirementMapper, times(1))
|
|
||||||
.updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成");
|
|
||||||
// 父需求被关闭
|
|
||||||
verify(requirementMapper, times(1))
|
|
||||||
.updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void closeRequirement_withChildNotAllowClose_shouldThrowException() {
|
|
||||||
Long parentId = 1015L;
|
|
||||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求",
|
|
||||||
"accepted", 0L, 0);
|
|
||||||
ProductRequirementDO child = createRequirement(1016L, 100L, "实施中子需求",
|
|
||||||
"implementing", parentId, 0);
|
|
||||||
|
|
||||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
|
||||||
reqVO.setId(parentId);
|
|
||||||
reqVO.setProductId(100L);
|
|
||||||
reqVO.setReason("全部验收完成");
|
|
||||||
|
|
||||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
|
||||||
when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child));
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.closeRequirement(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 模块管理测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createRequirementModule_whenNameDuplicate_shouldThrowException() {
|
|
||||||
Long productId = 100L;
|
|
||||||
ProductRequirementModuleDO existModule = new ProductRequirementModuleDO();
|
|
||||||
existModule.setId(50L);
|
|
||||||
existModule.setProductId(productId);
|
|
||||||
existModule.setModuleName("核心功能");
|
|
||||||
|
|
||||||
ProductRequirementModuleReqVO reqVO = new ProductRequirementModuleReqVO();
|
|
||||||
reqVO.setProductId(productId);
|
|
||||||
reqVO.setModuleName("核心功能");
|
|
||||||
|
|
||||||
when(moduleMapper.selectByProductIdAndModuleName(productId, "核心功能")).thenReturn(existModule);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.createRequirementModule(reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void deleteRequirementModule_whenHasNonTerminalRequirements_shouldThrowException() {
|
|
||||||
Long moduleId = 50L;
|
|
||||||
Long productId = 100L;
|
|
||||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
|
||||||
module.setId(moduleId);
|
|
||||||
module.setProductId(productId);
|
|
||||||
module.setModuleName("核心功能");
|
|
||||||
|
|
||||||
ProductRequirementDO nonTerminalReq = createRequirement(2001L, productId, "实施中需求",
|
|
||||||
"implementing", 0L, 0);
|
|
||||||
nonTerminalReq.setModuleId(moduleId);
|
|
||||||
|
|
||||||
when(moduleMapper.selectById(moduleId)).thenReturn(module);
|
|
||||||
when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(nonTerminalReq));
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.deleteRequirementModule(moduleId, productId));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS.getCode(), ex.getCode());
|
|
||||||
verify(moduleMapper, never()).deleteById(moduleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void deleteRequirementModule_whenAllTerminal_shouldDeleteSuccessfully() {
|
|
||||||
Long moduleId = 50L;
|
|
||||||
Long productId = 100L;
|
|
||||||
Long loginUserId = 1001L;
|
|
||||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
|
||||||
module.setId(moduleId);
|
|
||||||
module.setProductId(productId);
|
|
||||||
module.setModuleName("核心功能");
|
|
||||||
|
|
||||||
ProductRequirementDO closedReq = createRequirement(2001L, productId, "已关闭需求",
|
|
||||||
"closed", 0L, 0);
|
|
||||||
closedReq.setModuleId(moduleId);
|
|
||||||
|
|
||||||
when(moduleMapper.selectById(moduleId)).thenReturn(module);
|
|
||||||
when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(closedReq));
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
|
||||||
requirementService.deleteRequirementModule(moduleId, productId);
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requirementMapper, times(1)).deleteById(2001L);
|
|
||||||
verify(moduleMapper, times(1)).deleteById(moduleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== resolveModuleId 测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveModuleId_whenModuleIdIsNull_shouldReturnDefaultModuleId() {
|
|
||||||
Long defaultModuleId = 50L;
|
|
||||||
Long productId = 100L;
|
|
||||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, productId);
|
|
||||||
|
|
||||||
when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(defaultModule);
|
|
||||||
|
|
||||||
Long result = requirementService.resolveModuleId(null, productId);
|
|
||||||
|
|
||||||
assertEquals(defaultModuleId, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveModuleId_whenModuleIdProvided_shouldReturnSameId() {
|
|
||||||
Long moduleId = 60L;
|
|
||||||
Long productId = 100L;
|
|
||||||
|
|
||||||
// 传入有效moduleId时直接返回,不查询数据库
|
|
||||||
Long result = requirementService.resolveModuleId(moduleId, productId);
|
|
||||||
|
|
||||||
assertEquals(moduleId, result);
|
|
||||||
verify(moduleMapper, never()).selectByProductIdAndParentId(any(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== isAllRequirementsModule 测试 ==========
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAllRequirementsModule_whenRootModule_shouldReturnTrue() {
|
|
||||||
Long moduleId = 50L;
|
|
||||||
ProductRequirementModuleDO rootModule = createDefaultModule(moduleId, 100L);
|
|
||||||
|
|
||||||
when(moduleMapper.selectById(moduleId)).thenReturn(rootModule);
|
|
||||||
|
|
||||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
|
||||||
|
|
||||||
assertEquals(true, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAllRequirementsModule_whenChildModule_shouldReturnFalse() {
|
|
||||||
Long moduleId = 60L;
|
|
||||||
ProductRequirementModuleDO childModule = new ProductRequirementModuleDO();
|
|
||||||
childModule.setId(moduleId);
|
|
||||||
childModule.setProductId(100L);
|
|
||||||
childModule.setParentId(50L); // parentId != 0
|
|
||||||
childModule.setModuleName("子模块");
|
|
||||||
|
|
||||||
when(moduleMapper.selectById(moduleId)).thenReturn(childModule);
|
|
||||||
|
|
||||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
|
||||||
|
|
||||||
assertEquals(false, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAllRequirementsModule_whenModuleNotExists_shouldReturnFalse() {
|
|
||||||
Long moduleId = 70L;
|
|
||||||
|
|
||||||
when(moduleMapper.selectById(moduleId)).thenReturn(null);
|
|
||||||
|
|
||||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
|
||||||
|
|
||||||
assertEquals(false, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveModuleId_whenDefaultModuleNotExists_shouldThrowException() {
|
|
||||||
Long productId = 100L;
|
|
||||||
|
|
||||||
when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(null);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> requirementService.resolveModuleId(null, productId));
|
|
||||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 辅助方法 ==========
|
|
||||||
|
|
||||||
private ProductRequirementDO createRequirement(Long id, Long productId, String title,
|
|
||||||
String statusCode, Long parentId, Integer reviewRequired) {
|
|
||||||
ProductRequirementDO requirement = new ProductRequirementDO();
|
|
||||||
requirement.setId(id);
|
|
||||||
requirement.setProductId(productId);
|
|
||||||
requirement.setTitle(title);
|
|
||||||
requirement.setStatusCode(statusCode);
|
|
||||||
requirement.setParentId(parentId);
|
|
||||||
requirement.setReviewRequired(reviewRequired);
|
|
||||||
requirement.setCategory("function");
|
|
||||||
requirement.setSourceType("manual");
|
|
||||||
requirement.setPriority(1);
|
|
||||||
requirement.setProposerId(2001L);
|
|
||||||
requirement.setSort(0);
|
|
||||||
return requirement;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProductRequirementModuleDO createDefaultModule(Long id, Long productId) {
|
|
||||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
|
||||||
module.setId(id);
|
|
||||||
module.setProductId(productId);
|
|
||||||
module.setParentId(0L);
|
|
||||||
module.setModuleName("全部需求");
|
|
||||||
module.setRemark("自动创建的模块");
|
|
||||||
module.setSort(0);
|
|
||||||
return module;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObjectStatusTransitionDO createTransition(String actionCode, String toStatus, boolean needReason) {
|
|
||||||
ObjectStatusTransitionDO transition = new ObjectStatusTransitionDO();
|
|
||||||
transition.setActionCode(actionCode);
|
|
||||||
transition.setToStatusCode(toStatus);
|
|
||||||
transition.setNeedReason(needReason);
|
|
||||||
return transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId, String nickname) {
|
|
||||||
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
|
||||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
|
||||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserNickname).thenReturn(nickname);
|
|
||||||
return mockedStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,12 @@ import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execut
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionCreateReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionCreateReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionDeleteReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionUpdateReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionUpdateReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusActionReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusActionReqVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
|
||||||
@@ -35,6 +37,7 @@ import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
|||||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||||
|
import com.njcn.rdms.module.project.framework.security.service.ProjectObjectAuthorizationService;
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
||||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
@@ -107,6 +110,8 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private ProjectRequirementMapper projectRequirementMapper;
|
private ProjectRequirementMapper projectRequirementMapper;
|
||||||
@Mock
|
@Mock
|
||||||
|
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
||||||
|
@Mock
|
||||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||||
@Mock
|
@Mock
|
||||||
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
||||||
@@ -119,6 +124,8 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
void setupDefaultPriorityValidation() {
|
void setupDefaultPriorityValidation() {
|
||||||
lenient().when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
lenient().when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
||||||
.thenReturn(success(true));
|
.thenReturn(success(true));
|
||||||
|
lenient().when(userObjectRoleMapper.selectActiveListByObjectAndUserId(eq("project"), any(), eq(3001L)))
|
||||||
|
.thenAnswer(invocation -> List.of(createProjectMember(invocation.getArgument(1), 3001L, 3101L)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -130,6 +137,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
reqVO.setOwnerId(3001L);
|
reqVO.setOwnerId(3001L);
|
||||||
reqVO.setProjectRequirementId(null);
|
reqVO.setProjectRequirementId(null);
|
||||||
reqVO.setAssigneeUserIds(List.of(3002L, 3003L));
|
reqVO.setAssigneeUserIds(List.of(3002L, 3003L));
|
||||||
|
reqVO.setPriority("1");
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||||
@@ -203,6 +211,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
reqVO.setOwnerId(3001L);
|
reqVO.setOwnerId(3001L);
|
||||||
reqVO.setProjectRequirementId(9001L);
|
reqVO.setProjectRequirementId(9001L);
|
||||||
reqVO.setAssigneeUserIds(List.of(3002L));
|
reqVO.setAssigneeUserIds(List.of(3002L));
|
||||||
|
reqVO.setPriority("1");
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||||
@@ -269,8 +278,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||||
.thenReturn(createProjectStatus("pending", true));
|
.thenReturn(createProjectStatus("pending", true));
|
||||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "后端接口联调")).thenReturn(null);
|
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "后端接口联调")).thenReturn(null);
|
||||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
|
||||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
|
||||||
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
||||||
.thenReturn(success(true));
|
.thenReturn(success(true));
|
||||||
|
|
||||||
@@ -299,8 +306,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
.thenReturn(createExecutionStatus("pending", true));
|
.thenReturn(createExecutionStatus("pending", true));
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调-修订")).thenReturn(null);
|
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调-修订")).thenReturn(null);
|
||||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
|
||||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
|
||||||
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
||||||
.thenReturn(success(true));
|
.thenReturn(success(true));
|
||||||
|
|
||||||
@@ -310,6 +315,36 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
verify(projectExecutionMapper).updateById(execution);
|
verify(projectExecutionMapper).updateById(execution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateExecution_shouldAuthorizeProjectManagerOrExecutionOwnerBeforeUpdating() {
|
||||||
|
Long projectId = 2001L;
|
||||||
|
Long executionId = 5001L;
|
||||||
|
ProjectDO project = createEditableProject(projectId);
|
||||||
|
project.setManagerUserId(3001L);
|
||||||
|
ProjectExecutionDO execution = createExecution(projectId, executionId, 3002L);
|
||||||
|
ProjectExecutionUpdateReqVO reqVO = new ProjectExecutionUpdateReqVO();
|
||||||
|
reqVO.setId(executionId);
|
||||||
|
reqVO.setExecutionName("执行权限联调");
|
||||||
|
reqVO.setExecutionType("feature");
|
||||||
|
reqVO.setPriority("1");
|
||||||
|
reqVO.setProjectRequirementId(null);
|
||||||
|
|
||||||
|
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||||
|
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||||
|
.thenReturn(createProjectStatus("pending", true));
|
||||||
|
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
||||||
|
.thenReturn(createExecutionStatus("pending", true));
|
||||||
|
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||||
|
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "执行权限联调")).thenReturn(null);
|
||||||
|
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
||||||
|
.thenReturn(success(true));
|
||||||
|
|
||||||
|
projectExecutionService.updateExecution(projectId, reqVO);
|
||||||
|
|
||||||
|
verify(projectObjectAuthorizationService).checkAnyOwnerOrProjectPermission(projectId,
|
||||||
|
List.of(3001L, 3002L), ProjectExecutionConstants.PERMISSION_UPDATE);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void changeOwner_shouldUpdateOwnerAndWriteOwnerTransferLogs() {
|
void changeOwner_shouldUpdateOwnerAndWriteOwnerTransferLogs() {
|
||||||
Long projectId = 2001L;
|
Long projectId = 2001L;
|
||||||
@@ -340,6 +375,51 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN, "负责人调整");
|
ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN, "负责人调整");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteExecution_shouldAuthorizeProjectManagerOrExecutionOwnerBeforeDeleting() {
|
||||||
|
Long projectId = 2001L;
|
||||||
|
Long executionId = 5001L;
|
||||||
|
ProjectDO project = createEditableProject(projectId);
|
||||||
|
project.setManagerUserId(3001L);
|
||||||
|
ProjectExecutionDO execution = createExecution(projectId, executionId, 3002L);
|
||||||
|
execution.setExecutionName("Execution auth");
|
||||||
|
ProjectExecutionDeleteReqVO reqVO = new ProjectExecutionDeleteReqVO();
|
||||||
|
reqVO.setExecutionName("Execution auth");
|
||||||
|
reqVO.setConfirmText("DELETE");
|
||||||
|
reqVO.setReason("录入错误");
|
||||||
|
|
||||||
|
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||||
|
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||||
|
when(projectTaskMapper.selectIdsByExecutionId(executionId)).thenReturn(Collections.emptyList());
|
||||||
|
when(projectExecutionMapper.deleteById(executionId)).thenReturn(1);
|
||||||
|
|
||||||
|
projectExecutionService.deleteExecution(projectId, executionId, reqVO);
|
||||||
|
|
||||||
|
verify(projectObjectAuthorizationService).checkAnyOwnerOrProjectPermission(projectId,
|
||||||
|
List.of(3001L, 3002L), ProjectExecutionConstants.PERMISSION_DELETE);
|
||||||
|
verify(projectExecutionMapper).deleteById(executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void precheckDeleteExecution_shouldAuthorizeProjectManagerOrExecutionOwnerBeforeCountingTasks() {
|
||||||
|
Long projectId = 2001L;
|
||||||
|
Long executionId = 5001L;
|
||||||
|
ProjectDO project = createEditableProject(projectId);
|
||||||
|
project.setManagerUserId(3001L);
|
||||||
|
ProjectExecutionDO execution = createExecution(projectId, executionId, 3002L);
|
||||||
|
|
||||||
|
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||||
|
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||||
|
when(projectTaskMapper.countByExecutionId(executionId)).thenReturn(3);
|
||||||
|
|
||||||
|
var result = projectExecutionService.precheckDeleteExecution(projectId, executionId);
|
||||||
|
|
||||||
|
verify(projectObjectAuthorizationService).checkAnyOwnerOrProjectPermission(projectId,
|
||||||
|
List.of(3001L, 3002L), ProjectExecutionConstants.PERMISSION_DELETE);
|
||||||
|
assertEquals(3, result.getTaskCount());
|
||||||
|
assertEquals(true, result.getHasDependentData());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void changeExecutionStatus_shouldUpdateByTransitionAndWriteStatusAndAuditLogs() {
|
void changeExecutionStatus_shouldUpdateByTransitionAndWriteStatusAndAuditLogs() {
|
||||||
Long projectId = 2001L;
|
Long projectId = 2001L;
|
||||||
@@ -771,8 +851,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||||
.thenReturn(createProjectStatus("pending", true));
|
.thenReturn(createProjectStatus("pending", true));
|
||||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调")).thenReturn(null);
|
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调")).thenReturn(null);
|
||||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
|
||||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
|
||||||
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.EXECUTION_TYPE), any()))
|
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.EXECUTION_TYPE), any()))
|
||||||
.thenReturn(success(true));
|
.thenReturn(success(true));
|
||||||
|
|
||||||
@@ -877,8 +955,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
.thenReturn(createExecutionStatus("pending", true));
|
.thenReturn(createExecutionStatus("pending", true));
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调")).thenReturn(null);
|
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调")).thenReturn(null);
|
||||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
|
||||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
|
||||||
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.EXECUTION_TYPE), any()))
|
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.EXECUTION_TYPE), any()))
|
||||||
.thenReturn(success(true));
|
.thenReturn(success(true));
|
||||||
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
||||||
|
|||||||
Reference in New Issue
Block a user