From f546b29ade7f44a1f6ffa5792db5c17796780ff2 Mon Sep 17 00:00:00 2001 From: song-tianyang Date: Mon, 29 Jan 2024 16:00:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A3=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=9C=BA=E6=99=AF=E7=9A=84=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/i18n/system.properties | 2 + .../resources/i18n/system_en_US.properties | 2 + .../resources/i18n/system_zh_CN.properties | 2 + .../resources/i18n/system_zh_TW.properties | 2 + .../scenario/ApiScenarioController.java | 18 ++ .../ApiScenarioScheduleConfigRequest.java | 26 +++ .../api/job/ApiScenarioScheduleJob.java | 25 +++ .../definition/ApiScenarioNoticeService.java | 13 ++ .../scenario/ApiScenarioLogService.java | 14 ++ .../service/scenario/ApiScenarioService.java | 45 ++++- .../ApiScenarioControllerTests.java | 160 +++++++++++++++++- .../ApiScenarioBatchOperationTestService.java | 24 +++ .../project/service/EnvironmentService.java | 2 +- .../controller/PersonalCenterController.java | 3 +- .../system/dto/request/ScheduleConfig.java | 49 ++++++ .../system/mapper/ExtScheduleMapper.java | 7 + .../system/mapper/ExtScheduleMapper.xml | 22 +++ .../system/schedule/ScheduleService.java | 45 +++++ .../src/main/resources/permission.json | 3 +- .../controller/ScheduleControllerTests.java | 106 ++++++++++++ .../system/job/ApiScenarioScheduleJob.java | 25 +++ .../dto/request/TestPlanCreateRequest.java | 2 + .../plan/controller/TestPlanTests.java | 4 +- 23 files changed, 583 insertions(+), 18 deletions(-) create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioScheduleConfigRequest.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/job/ApiScenarioScheduleJob.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/ScheduleConfig.java create mode 100644 backend/services/system-setting/src/test/java/io/metersphere/system/controller/ScheduleControllerTests.java create mode 100644 backend/services/system-setting/src/test/java/io/metersphere/system/job/ApiScenarioScheduleJob.java diff --git a/backend/framework/sdk/src/main/resources/i18n/system.properties b/backend/framework/sdk/src/main/resources/i18n/system.properties index 9bedfad6bf..20a20a01e1 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system.properties @@ -1,7 +1,9 @@ excel.parse.error=Excel解析失败 id.not_blank=ID不能为空 +permission.system_user.invite=邀请用户 role.not.global.system=角色不是全局系统角色 role.not.contains.member=角色不包含系统成员角色 +schedule.cron.error=Cron表达式错误 user.not.login=未获取到登录用户 user.not.empty=用户不呢为空 user.not.exist=用户不存在 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties index e99de68178..5d84b957e4 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties @@ -1,7 +1,9 @@ excel.parse.error=Excel parse error id.not_blank=Id must not be blank +permission.system_user.invite=Invite user role.not.global.system=Role is not global system role role.not.contains.member=Role not contains member +schedule.cron.error=Cron is error user.not.login=User not login user.not.exist=User not exist personal.no.permission=No permission diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties index 7a4b72b91a..b55a48fd55 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties @@ -1,7 +1,9 @@ excel.parse.error=Excel解析失败 id.not_blank=ID不能为空 +permission.system_user.invite=邀请用户 role.not.global.system=角色不是全局系统角色 role.not.contains.member=角色不包含系统成员角色 +schedule.cron.error=Cron表达式错误 user.not.login=未获取到登录用户 user.not.empty=用户不呢为空 user.not.exist=用户不存在 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties index d31bfd195f..4d235768f3 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties @@ -1,7 +1,9 @@ excel.parse.error=Excel解析失敗 id.not_blank=ID不能為空 +permission.system_user.invite=邀請用戶 role.not.global.system=角色不是為全局系統角色 role.not.contains.member=角色不包含系統成員角色 +schedule.cron.error=Cron表達式錯誤 user.not.login=未獲取到登錄用戶 user.not.empty=用戶不呢為空 user.not.exist=用戶不存在 diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java b/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java index e3af699d24..1039bd82aa 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java @@ -6,11 +6,14 @@ import io.metersphere.api.constants.ApiResource; import io.metersphere.api.domain.ApiScenario; import io.metersphere.api.dto.scenario.*; import io.metersphere.api.service.ApiValidateService; +import io.metersphere.api.service.definition.ApiScenarioNoticeService; import io.metersphere.api.service.scenario.ApiScenarioLogService; import io.metersphere.api.service.scenario.ApiScenarioService; import io.metersphere.sdk.constants.PermissionConstants; import io.metersphere.system.log.annotation.Log; import io.metersphere.system.log.constants.OperationLogType; +import io.metersphere.system.notice.annotation.SendNotice; +import io.metersphere.system.notice.constants.NoticeConstants; import io.metersphere.system.security.CheckOwner; import io.metersphere.system.utils.PageUtils; import io.metersphere.system.utils.Pager; @@ -159,4 +162,19 @@ public class ApiScenarioController { apiScenarioService.updatePriority(id, priority, SessionUtils.getUserId()); } + @PostMapping(value = "/schedule-config") + @Operation(summary = "接口测试-接口场景管理-定时任务配置") + @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_EXECUTE) + @Log(type = OperationLogType.UPDATE, expression = "#msClass.scheduleLog(#request.getScenarioId())", msClass = ApiScenarioLogService.class) + @CheckOwner(resourceId = "#request.getScenarioId()", resourceType = "api_scenario") + @SendNotice(taskType = NoticeConstants.TaskType.SCHEDULE_TASK, event = NoticeConstants.Event.UPDATE, target = "#targetClass.getScheduleNotice(#request)", targetClass = ApiScenarioNoticeService.class) + public String scheduleConfig(@Validated @RequestBody ApiScenarioScheduleConfigRequest request) { + /* + TODO to Chen Jianxing: + request.configMap 中需要补充场景的执行信息,比如环境、资源池、是否失败停止等配置。 + 在触发定时任务的APIScenarioScheduleJob中会用到 + */ + apiValidateService.validateApiMenuInProject(request.getScenarioId(), ApiResource.API_SCENARIO.name()); + return apiScenarioService.scheduleConfig(request, SessionUtils.getUserId()); + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioScheduleConfigRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioScheduleConfigRequest.java new file mode 100644 index 0000000000..4f72e6e985 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioScheduleConfigRequest.java @@ -0,0 +1,26 @@ +package io.metersphere.api.dto.scenario; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.util.Map; + +@Data +public class ApiScenarioScheduleConfigRequest { + + @NotBlank(message = "{api_scenario.id.not_blank}") + @Schema(description = "场景ID") + private String scenarioId; + + @Schema(description = "启用/禁用") + private boolean enable; + + @Schema(description = "Cron表达式") + @NotBlank + private String cron; + + @Schema(description = "定时任务配置 (如果配置不更改,不需要传入这个参数。 如果要清空配置,传入一个空数据{} )") + Map configMap; + +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/job/ApiScenarioScheduleJob.java b/backend/services/api-test/src/main/java/io/metersphere/api/job/ApiScenarioScheduleJob.java new file mode 100644 index 0000000000..78c26b15cb --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/job/ApiScenarioScheduleJob.java @@ -0,0 +1,25 @@ +package io.metersphere.api.job; + +import io.metersphere.system.schedule.BaseScheduleJob; +import org.quartz.JobExecutionContext; +import org.quartz.JobKey; +import org.quartz.TriggerKey; + +public class ApiScenarioScheduleJob extends BaseScheduleJob { + @Override + protected void businessExecute(JobExecutionContext context) { + /* + TODO to Chen Jianxing: + 这里需要补充执行逻辑 + 记得执行信息(环境、资源池、是否失败停止等配置)在jobDataMap里面 + */ + } + + public static JobKey getJobKey(String scenarioId) { + return new JobKey(scenarioId, ApiScenarioScheduleJob.class.getName()); + } + + public static TriggerKey getTriggerKey(String scenarioId) { + return new TriggerKey(scenarioId, ApiScenarioScheduleJob.class.getName()); + } +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiScenarioNoticeService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiScenarioNoticeService.java index c15f4248f0..ac0bf78335 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiScenarioNoticeService.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiScenarioNoticeService.java @@ -3,9 +3,14 @@ package io.metersphere.api.service.definition; import io.metersphere.api.domain.ApiScenario; import io.metersphere.api.domain.ApiScenarioExample; import io.metersphere.api.dto.scenario.ApiScenarioBatchRequest; +import io.metersphere.api.dto.scenario.ApiScenarioScheduleConfigRequest; +import io.metersphere.api.job.ApiScenarioScheduleJob; import io.metersphere.api.mapper.ApiScenarioMapper; import io.metersphere.api.service.scenario.ApiScenarioService; import io.metersphere.sdk.util.SubListUtils; +import io.metersphere.system.domain.Schedule; +import io.metersphere.system.domain.ScheduleExample; +import io.metersphere.system.mapper.ScheduleMapper; import jakarta.annotation.Resource; import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Service; @@ -21,6 +26,14 @@ public class ApiScenarioNoticeService { @Resource private ApiScenarioMapper apiScenarioMapper; + @Resource + private ScheduleMapper scheduleMapper; + + public List getScheduleNotice(ApiScenarioScheduleConfigRequest request) { + ScheduleExample example = new ScheduleExample(); + example.createCriteria().andResourceIdEqualTo(request.getScenarioId()).andJobEqualTo(ApiScenarioScheduleJob.class.getName()); + return scheduleMapper.selectByExample(example); + } public List getBatchOptionScenarios(ApiScenarioBatchRequest request) { List ids = apiScenarioService.doSelectIds(request, false); diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java index baf9ef5fe0..40fd7bf8aa 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java @@ -133,6 +133,20 @@ public class ApiScenarioLogService { return dto; } + public LogDTO scheduleLog(String id) { + ApiScenario apiScenario = apiScenarioMapper.selectByPrimaryKey(id); + Project project = projectMapper.selectByPrimaryKey(apiScenario.getProjectId()); + LogDTO dto = LogDTOBuilder.builder() + .projectId(project.getId()) + .organizationId(project.getOrganizationId()) + .type(OperationLogType.UPDATE.name()) + .module(OperationLogModule.API_SCENARIO) + .sourceId(apiScenario.getId()) + .content(Translator.get("api_automation_schedule") + ":" + apiScenario.getName()) + .build().getLogDTO(); + return dto; + } + public LogDTO updateLog(String id) { ApiScenario apiScenario = apiScenarioMapper.selectByPrimaryKey(id); // todo 记录完整的场景信息 diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java index 572e3d578d..4b7a9f1b34 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java @@ -9,6 +9,7 @@ import io.metersphere.api.dto.debug.ApiResourceRunRequest; import io.metersphere.api.dto.request.MsScenario; import io.metersphere.api.dto.response.ApiScenarioBatchOperationResponse; import io.metersphere.api.dto.scenario.*; +import io.metersphere.api.job.ApiScenarioScheduleJob; import io.metersphere.api.mapper.*; import io.metersphere.api.parser.step.StepParser; import io.metersphere.api.parser.step.StepParserFactory; @@ -23,10 +24,7 @@ import io.metersphere.project.mapper.ExtBaseProjectVersionMapper; import io.metersphere.project.service.FileAssociationService; import io.metersphere.project.service.FileMetadataService; import io.metersphere.project.service.ProjectService; -import io.metersphere.sdk.constants.ApiExecuteRunMode; -import io.metersphere.sdk.constants.ApplicationNumScope; -import io.metersphere.sdk.constants.DefaultRepositoryDir; -import io.metersphere.sdk.constants.ModuleConstants; +import io.metersphere.sdk.constants.*; import io.metersphere.sdk.domain.Environment; import io.metersphere.sdk.domain.EnvironmentExample; import io.metersphere.sdk.domain.EnvironmentGroup; @@ -40,8 +38,10 @@ import io.metersphere.sdk.mapper.EnvironmentGroupMapper; import io.metersphere.sdk.mapper.EnvironmentMapper; import io.metersphere.sdk.util.*; import io.metersphere.system.dto.LogInsertModule; +import io.metersphere.system.dto.request.ScheduleConfig; import io.metersphere.system.log.constants.OperationLogModule; import io.metersphere.system.log.constants.OperationLogType; +import io.metersphere.system.schedule.ScheduleService; import io.metersphere.system.service.UserLoginService; import io.metersphere.system.uid.IDGenerator; import io.metersphere.system.uid.NumGenerator; @@ -121,6 +121,9 @@ public class ApiScenarioService { private ApiScenarioCsvMapper apiScenarioCsvMapper; @Resource private ApiScenarioCsvStepMapper apiScenarioCsvStepMapper; + @Resource + private ScheduleService scheduleService; + public static final String PRIORITY = "Priority"; public static final String STATUS = "Status"; public static final String TAGS = "Tags"; @@ -836,6 +839,9 @@ public class ApiScenarioService { apiFileResourceService.deleteByResourceId(scenarioDir, scenario.getId(), scenario.getProjectId(), operator, OperationLogModule.API_DEBUG); }catch (Exception ignore){} + //删除定时任务 + scheduleService.deleteByResourceId(scenario.getId(), ApiScenarioScheduleJob.class.getName()); + //todo wang xiao gang: 删除csv相关东西 } @@ -858,13 +864,14 @@ public class ApiScenarioService { blobExample.createCriteria().andScenarioIdIn(scenarioIdList); apiScenarioStepBlobMapper.deleteByExample(blobExample); - //删除文件 scenarioList.forEach(scenario -> { + //删除文件 String scenarioDir = DefaultRepositoryDir.getApiDebugDir(scenario.getProjectId(), scenario.getId()); try { apiFileResourceService.deleteByResourceId(scenarioDir, scenario.getId(), scenario.getProjectId(), operator, OperationLogModule.API_DEBUG); }catch (Exception ignore){} - + //删除定时任务 + scheduleService.deleteByResourceId(scenario.getId(), ApiScenarioScheduleJob.class.getName()); }); //todo wang xiao gang: 删除csv相关东西 @@ -884,6 +891,9 @@ public class ApiScenarioService { apiScenario.setId(id); apiScenario.setDeleted(true); apiScenarioMapper.updateByPrimaryKeySelective(apiScenario); + + //删除定时任务 + scheduleService.deleteByResourceId(id, ApiScenarioScheduleJob.class.getName()); } private void checkAddExist(ApiScenarioAddRequest apiScenario) { @@ -1574,6 +1584,9 @@ public class ApiScenarioService { for (ApiScenario scenario : apiScenarioList) { response.addSuccessData(scenario.getId(), scenario.getNum(), scenario.getName()); + + //删除定时任务 + scheduleService.deleteByResourceId(scenario.getId(), ApiScenarioScheduleJob.class.getName()); } return response; } @@ -1592,4 +1605,24 @@ public class ApiScenarioService { } + public String scheduleConfig(ApiScenarioScheduleConfigRequest scheduleRequest, String operator) { + ApiScenario apiScenario = apiScenarioMapper.selectByPrimaryKey(scheduleRequest.getScenarioId()); + ScheduleConfig scheduleConfig = ScheduleConfig.builder() + .resourceId(apiScenario.getId()) + .key(apiScenario.getId()) + .projectId(apiScenario.getProjectId()) + .name(apiScenario.getName()) + .enable(scheduleRequest.isEnable()) + .cron(scheduleRequest.getCron()) + .resourceType(ScheduleResourceType.API_SCENARIO.name()) + .configMap(scheduleRequest.getConfigMap()) + .build(); + + return scheduleService.scheduleConfig( + scheduleConfig, + ApiScenarioScheduleJob.getJobKey(apiScenario.getId()), + ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()), + ApiScenarioScheduleJob.class, + operator); + } } \ No newline at end of file diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerTests.java b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerTests.java index 8fb9cb618c..048adb07c4 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerTests.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerTests.java @@ -98,6 +98,8 @@ public class ApiScenarioControllerTests extends BaseTest { @Resource private ApiScenarioStepBlobMapper apiScenarioStepBlobMapper; @Resource + private ApiFileResourceMapper apiFileResourceMapper; + @Resource private ApiScenarioBlobMapper apiScenarioBlobMapper; @Resource private ExtBaseProjectVersionMapper extBaseProjectVersionMapper; @@ -1122,7 +1124,7 @@ public class ApiScenarioControllerTests extends BaseTest { } @Test - @Order(21) + @Order(22) void batchMove() throws Exception { String testUrl = "/batch-operation/move"; if (CollectionUtils.isEmpty(BATCH_OPERATION_SCENARIO_ID)) { @@ -1252,14 +1254,155 @@ public class ApiScenarioControllerTests extends BaseTest { } @Test - @Order(22) + @Order(23) + void scheduleTest() throws Exception { + String testUrl = "/schedule-config"; + + if (CollectionUtils.isEmpty(BATCH_OPERATION_SCENARIO_ID)) { + this.batchCreateScenarios(); + } + + //使用最后一个场景ID用于做定时任务的测试 + String scenarioId = BATCH_OPERATION_SCENARIO_ID.getLast(); + ApiScenarioScheduleConfigRequest request = new ApiScenarioScheduleConfigRequest(); + + request.setScenarioId(scenarioId); + request.setEnable(true); + request.setCron("0 0 0 * * ?"); + + //先测试一下没有开启模块时接口能否使用 + apiScenarioBatchOperationTestService.removeApiModule(DEFAULT_PROJECT_ID); + this.requestPost(testUrl, request).andExpect(status().is5xxServerError()); + //恢复 + apiScenarioBatchOperationTestService.resetProjectModule(DEFAULT_PROJECT_ID); + MvcResult result = this.requestPostAndReturn(testUrl, request); + ResultHolder resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class); + String scheduleId = resultHolder.getData().toString(); + apiScenarioBatchOperationTestService.checkSchedule(scheduleId, scenarioId, request.isEnable()); + + //增加日志检查 + LOG_CHECK_LIST.add( + new CheckLogModel(scenarioId, OperationLogType.UPDATE, "/api/scenario/schedule-config") + ); + + //关闭 + request.setEnable(false); + result = this.requestPostAndReturn(testUrl, request); + resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class); + String newScheduleId = resultHolder.getData().toString(); + //检查两个scheduleId是否相同 + Assertions.assertEquals(scheduleId, newScheduleId); + apiScenarioBatchOperationTestService.checkSchedule(newScheduleId, scenarioId, request.isEnable()); + + //配置configMap + request.setEnable(true); + request.setConfigMap(new HashMap<>() {{ + this.put("envId", "testEnv"); + this.put("resourcePoolId", "testResourcePool"); + }}); + result = this.requestPostAndReturn(testUrl, request); + resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class); + newScheduleId = resultHolder.getData().toString(); + apiScenarioBatchOperationTestService.checkSchedule(newScheduleId, scenarioId, request.isEnable()); + + //清空configMap + request.setConfigMap(new HashMap<>()); + request.setEnable(false); + result = this.requestPostAndReturn(testUrl, request); + resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class); + newScheduleId = resultHolder.getData().toString(); + apiScenarioBatchOperationTestService.checkSchedule(newScheduleId, scenarioId, request.isEnable()); + + //测试各种corn表达式用于校验正则的准确性 + String[] cornStrArr = new String[]{ + "0 0 12 * * ?", //每天中午12点触发 + "0 15 10 ? * *", //每天上午10:15触发 + "0 15 10 * * ?", //每天上午10:15触发 + "0 15 10 * * ? *",//每天上午10:15触发 + "0 15 10 * * ? 2048",//2008年的每天上午10:15触发 + "0 * 10 * * ?",//每天上午10:00至10:59期间的每1分钟触发 + "0 0/5 10 * * ?",//每天上午10:00至10:55期间的每5分钟触发 + "0 0/5 10,16 * * ?",//每天上午10:00至10:55期间和下午4:00至4:55期间的每5分钟触发 + "0 0-5 10 * * ?",//每天上午10:00至10:05期间的每1分钟触发 + "0 10,14,18 15 ? 3 WED",//每年三月的星期三的下午2:10和2:18触发 + "0 10 15 ? * MON-FRI",//每个周一、周二、周三、周四、周五的下午3:10触发 + "0 15 10 15 * ?",//每月15日上午10:15触发 + "0 15 10 L * ?", //每月最后一日的上午10:15触发 + "0 15 10 ? * 6L", //每月的最后一个星期五上午10:15触发 + "0 15 10 ? * 6L 2024-2026", //从2024年至2026年每月的最后一个星期五上午10:15触发 + "0 15 10 ? * 6#3", //每月的第三个星期五上午10:15触发 + }; + + //每种corn表达式开启、关闭都测试一遍,检查是否能正常开关定时任务 + for (String corn : cornStrArr) { + request = new ApiScenarioScheduleConfigRequest(); + request.setScenarioId(scenarioId); + request.setEnable(true); + request.setCron(corn); + result = this.requestPostAndReturn(testUrl, request); + resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class); + scheduleId = resultHolder.getData().toString(); + apiScenarioBatchOperationTestService.checkSchedule(scheduleId, scenarioId, request.isEnable()); + + request = new ApiScenarioScheduleConfigRequest(); + request.setScenarioId(scenarioId); + request.setEnable(false); + request.setCron(corn); + result = this.requestPostAndReturn(testUrl, request); + resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class); + scheduleId = resultHolder.getData().toString(); + apiScenarioBatchOperationTestService.checkSchedule(scheduleId, scenarioId, request.isEnable()); + } + + + //校验权限 + this.requestPostPermissionTest(PermissionConstants.PROJECT_API_SCENARIO_EXECUTE, testUrl, request); + + //反例:scenarioId不存在 + request = new ApiScenarioScheduleConfigRequest(); + request.setCron("0 0 0 * * ?"); + this.requestPost(testUrl, request).andExpect(status().isBadRequest()); + request.setScenarioId(IDGenerator.nextStr()); + this.requestPost(testUrl, request).andExpect(status().is5xxServerError()); + + //反例:不配置cron表达式 + request = new ApiScenarioScheduleConfigRequest(); + request.setScenarioId(scenarioId); + this.requestPost(testUrl, request).andExpect(status().isBadRequest()); + + //反例:配置错误的cron表达式,测试是否会关闭定时任务 + request = new ApiScenarioScheduleConfigRequest(); + request.setScenarioId(scenarioId); + request.setEnable(true); + request.setCron(IDGenerator.nextStr()); + this.requestPost(testUrl, request).andExpect(status().is5xxServerError()); + + } + + //30开始是关于删除和恢复的 + @Test + @Order(31) void batchRemoveToGc() throws Exception { String testUrl = "/batch-operation/delete-gc"; if (CollectionUtils.isEmpty(BATCH_OPERATION_SCENARIO_ID)) { - this.batchCopy(); + this.scheduleTest(); } + + //使用最后一个场景ID用于做定时任务的测试 + String scenarioId = BATCH_OPERATION_SCENARIO_ID.getLast(); + //本次测试涉及到的场景ID List operationScenarioIds = new ArrayList<>(BATCH_OPERATION_SCENARIO_ID.subList(200, 500)); + //给最后一个场景创建定时任务,测试batchToGc时,是否会删除定时任务 + ApiScenarioScheduleConfigRequest scheduleRequest = new ApiScenarioScheduleConfigRequest(); + scheduleRequest.setScenarioId(scenarioId); + scheduleRequest.setEnable(true); + scheduleRequest.setCron("0 0 0 * * ?"); + MvcResult scheduleResult = this.requestPostAndReturn("/schedule-config", scheduleRequest); + ResultHolder scheduleResultHolder = JSON.parseObject(scheduleResult.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class); + String scheduleId = scheduleResultHolder.getData().toString(); + apiScenarioBatchOperationTestService.checkSchedule(scheduleId, scenarioId, scheduleRequest.isEnable()); + /* 正例测试 @@ -1283,7 +1426,8 @@ public class ApiScenarioControllerTests extends BaseTest { ApiScenarioBatchOperationResponse resultResponse = JSON.parseObject(JSON.toJSONString(resultHolder.getData()), ApiScenarioBatchOperationResponse.class); //检查返回值 Assertions.assertEquals(resultResponse.getSuccess(), 300); - + //检查定时任务是否删除 + apiScenarioBatchOperationTestService.checkScheduleIsRemove(scenarioId); //数据库级别的检查 apiScenarioBatchOperationTestService.checkBatchGCOperation (BATCH_OPERATION_SCENARIO_ID.subList(200, 500), true); @@ -1320,7 +1464,7 @@ public class ApiScenarioControllerTests extends BaseTest { } @Test - @Order(23) + @Order(32) //todo void batchRecoverToGc() throws Exception { String testUrl = "/batch-operation/recover-gc"; @@ -1393,7 +1537,7 @@ public class ApiScenarioControllerTests extends BaseTest { } @Test - @Order(24) + @Order(33) //todo void batchDelete() throws Exception { String testUrl = "/batch-operation/delete"; @@ -1467,9 +1611,8 @@ public class ApiScenarioControllerTests extends BaseTest { } - //30开始是关于删除和恢复的 @Test - @Order(30) + @Order(34) void recover() throws Exception { if (CollectionUtils.isEmpty(BATCH_OPERATION_SCENARIO_ID)) { this.batchCreateScenarios(); @@ -1651,6 +1794,7 @@ public class ApiScenarioControllerTests extends BaseTest { apiFileResource.setResourceType("API_SCENARIO"); apiFileResource.setCreateTime(System.currentTimeMillis()); apiFileResource.setProjectId(apiScenario.getProjectId()); + apiFileResourceMapper.insertSelective(apiFileResource); } apiScenarioMapper.insertSelective(apiScenario); BATCH_OPERATION_SCENARIO_ID.add(apiScenario.getId()); diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiScenarioBatchOperationTestService.java b/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiScenarioBatchOperationTestService.java index 9ef76bc809..e47d9ade04 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiScenarioBatchOperationTestService.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiScenarioBatchOperationTestService.java @@ -2,13 +2,16 @@ package io.metersphere.api.service; import io.metersphere.api.domain.*; import io.metersphere.api.dto.scenario.ApiScenarioBatchCopyMoveRequest; +import io.metersphere.api.job.ApiScenarioScheduleJob; import io.metersphere.api.mapper.*; import io.metersphere.project.domain.Project; import io.metersphere.project.mapper.ProjectMapper; import io.metersphere.sdk.util.JSON; +import io.metersphere.system.mapper.ExtScheduleMapper; import jakarta.annotation.Resource; import org.apache.commons.collections.MapUtils; import org.junit.jupiter.api.Assertions; +import org.quartz.Scheduler; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +33,11 @@ public class ApiScenarioBatchOperationTestService { private ApiScenarioStepBlobMapper apiScenarioStepBlobMapper; @Resource private ApiFileResourceMapper apiFileResourceMapper; + @Resource + private ExtScheduleMapper extScheduleMapper; + + @Resource + private Scheduler scheduler; @Resource private ProjectMapper projectMapper; @@ -189,4 +197,20 @@ public class ApiScenarioBatchOperationTestService { sourceFileExample.createCriteria().andResourceIdIn(deleteScenarioIds); Assertions.assertEquals(apiFileResourceMapper.countByExample(sourceFileExample), 0); } + + /* + 校验定时任务是否成功开启: + 1.schedule表中存在数据,且开启状态符合isEnable + 2.开启状态下: qrtz_triggers 和 qrtz_cron_triggers 表存在对应的数据 + 3.关闭状态下: qrtz_triggers 和 qrtz_cron_triggers 表不存在对应的数据 + */ + public void checkSchedule(String scheduleId, String resourceId, boolean isEnable) throws Exception { + Assertions.assertEquals(extScheduleMapper.countByIdAndEnable(scheduleId, isEnable), 1L); + Assertions.assertEquals(scheduler.checkExists(ApiScenarioScheduleJob.getJobKey(resourceId)), isEnable); + } + + public void checkScheduleIsRemove(String resourceId) throws Exception { + Assertions.assertEquals(extScheduleMapper.countByResourceId(resourceId), 0L); + Assertions.assertEquals(scheduler.checkExists(ApiScenarioScheduleJob.getJobKey(resourceId)), false); + } } diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/service/EnvironmentService.java b/backend/services/project-management/src/main/java/io/metersphere/project/service/EnvironmentService.java index 12b8ddf308..b8b4fef0a7 100644 --- a/backend/services/project-management/src/main/java/io/metersphere/project/service/EnvironmentService.java +++ b/backend/services/project-management/src/main/java/io/metersphere/project/service/EnvironmentService.java @@ -68,7 +68,7 @@ public class EnvironmentService { private static final String USERNAME = "user"; private static final String PASSWORD = "password"; private static final String PATH = "/project/environment/import"; - private static final String MOCK_EVN_SOCKET = "/api/mock/"; + private static final String MOCK_EVN_SOCKET = "/mock-server/"; public List getDriverOptions(String organizationId) { return jdbcDriverPluginService.getJdbcDriverOption(organizationId); diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PersonalCenterController.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PersonalCenterController.java index c394474701..38bfa4bd1f 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PersonalCenterController.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PersonalCenterController.java @@ -19,7 +19,8 @@ import org.springframework.web.bind.annotation.*; @RestController @Tag(name = "个人中心") @RequestMapping("/personal") -public class PersonalCenterController { +public class +PersonalCenterController { @Resource private UserService userService; diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/ScheduleConfig.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/ScheduleConfig.java new file mode 100644 index 0000000000..4d0bc45a69 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/ScheduleConfig.java @@ -0,0 +1,49 @@ +package io.metersphere.system.dto.request; + +import io.metersphere.sdk.constants.ScheduleType; +import io.metersphere.sdk.util.JSON; +import io.metersphere.system.domain.Schedule; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +@Data +@Builder +public class ScheduleConfig { + + private String resourceId; + + private String key; + + private String projectId; + + private String name; + + private Boolean enable; + + private String cron; + + private String resourceType; + + Map configMap; + + public Schedule genCronSchedule(Schedule schedule) { + if (schedule == null) { + schedule = new Schedule(); + } + schedule.setName(this.getName()); + schedule.setResourceId(this.getResourceId()); + schedule.setType(ScheduleType.CRON.name()); + schedule.setKey(this.getKey()); + schedule.setEnable(this.getEnable()); + schedule.setProjectId(this.getProjectId()); + schedule.setValue(this.getCron()); + schedule.setResourceType(this.getResourceType()); + //配置数据为null,意味着不更改; 如果要清空配置,需要传入空对象 + if (configMap != null) { + schedule.setConfig(JSON.toJSONString(configMap)); + } + return schedule; + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.java index bd9186e766..e12ec71723 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.java @@ -22,4 +22,11 @@ public interface ExtScheduleMapper { List getApiScenarioListByIds(@Param("ids") List ids); + long countByResourceId(String resourceId); + + long countByIdAndEnable(@Param("id") String scheduleId, @Param("enable") boolean isEnable); + + long countQuartzTriggersByResourceId(String scheduleId); + + long countQuartzCronTriggersByResourceId(String scheduleId); } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.xml index 6a6b004da3..09a80e7c72 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.xml +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtScheduleMapper.xml @@ -70,6 +70,28 @@ #{id} + + + + \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/schedule/ScheduleService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/schedule/ScheduleService.java index 28fb759c42..78413a0370 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/schedule/ScheduleService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/schedule/ScheduleService.java @@ -1,20 +1,25 @@ package io.metersphere.system.schedule; import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.JSON; import io.metersphere.system.domain.Schedule; import io.metersphere.system.domain.ScheduleExample; +import io.metersphere.system.dto.request.ScheduleConfig; import io.metersphere.system.mapper.ScheduleMapper; import io.metersphere.system.uid.IDGenerator; import jakarta.annotation.Resource; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import org.quartz.JobDataMap; import org.quartz.JobKey; import org.quartz.SchedulerException; import org.quartz.TriggerKey; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; +import java.util.Optional; @Transactional(rollbackFor = Exception.class) public class ScheduleService { @@ -100,4 +105,44 @@ public class ScheduleService { } } } + + public String scheduleConfig(ScheduleConfig scheduleConfig, JobKey jobKey, TriggerKey triggerKey, Class clazz, String operator) { + Schedule schedule; + ScheduleExample example = new ScheduleExample(); + example.createCriteria().andResourceIdEqualTo(scheduleConfig.getResourceId()).andJobEqualTo(clazz.getName()); + List scheduleList = scheduleMapper.selectByExample(example); + if (CollectionUtils.isNotEmpty(scheduleList)) { + schedule = scheduleConfig.genCronSchedule(scheduleList.getFirst()); + schedule.setUpdateTime(System.currentTimeMillis()); + schedule.setJob(clazz.getName()); + scheduleMapper.updateByExampleSelective(schedule, example); + } else { + schedule = scheduleConfig.genCronSchedule(null); + schedule.setJob(clazz.getName()); + schedule.setId(IDGenerator.nextStr()); + schedule.setCreateUser(operator); + schedule.setCreateTime(System.currentTimeMillis()); + schedule.setUpdateTime(System.currentTimeMillis()); + scheduleMapper.insert(schedule); + } + + JobDataMap jobDataMap = scheduleManager.getDefaultJobDataMap(schedule, scheduleConfig.getCron(), operator); + if (StringUtils.isNotEmpty(schedule.getConfig())) { + Map configMap = JSON.parseObject(schedule.getConfig(), Map.class); + jobDataMap.putAll(configMap); + } + + /* + scheduleManager.modifyCronJobTime方法如同它的方法名所说,只能修改定时任务的触发时间。 + 如果定时任务的配置数据jobData发生了变化,上面方法是无法更新配置数据的。 + 所以,如果配置数据发生了变化,做法就是先删除运行中的定时任务,再重新添加定时任务。 + + 以上的更新逻辑配合 enable 开关,可以简化为下列写法: + */ + scheduleManager.removeJob(jobKey, triggerKey); + if (BooleanUtils.isTrue(schedule.getEnable())) { + scheduleManager.addCronJob(jobKey, triggerKey, clazz, scheduleConfig.getCron(), jobDataMap); + } + return schedule.getId(); + } } diff --git a/backend/services/system-setting/src/main/resources/permission.json b/backend/services/system-setting/src/main/resources/permission.json index 837bfc472e..6f8e769524 100644 --- a/backend/services/system-setting/src/main/resources/permission.json +++ b/backend/services/system-setting/src/main/resources/permission.json @@ -21,7 +21,8 @@ "id": "SYSTEM_USER:READ+UPDATE" }, { - "id": "SYSTEM_USER:READ+INVITE" + "id": "SYSTEM_USER:READ+INVITE", + "name": "permission.system_user.invite" }, { "id": "SYSTEM_USER:READ+DELETE" diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/ScheduleControllerTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/ScheduleControllerTests.java new file mode 100644 index 0000000000..e0467384e0 --- /dev/null +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/ScheduleControllerTests.java @@ -0,0 +1,106 @@ +package io.metersphere.system.controller; + +import io.metersphere.sdk.constants.ScheduleResourceType; +import io.metersphere.sdk.constants.ScheduleType; +import io.metersphere.system.base.BaseTest; +import io.metersphere.system.domain.Schedule; +import io.metersphere.system.dto.request.ScheduleConfig; +import io.metersphere.system.job.ApiScenarioScheduleJob; +import io.metersphere.system.schedule.ScheduleService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.quartz.JobKey; +import org.quartz.TriggerKey; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.HashMap; +import java.util.List; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ScheduleControllerTests extends BaseTest { + + @Resource + private ScheduleService scheduleService; + + @Test + public void test() { + Schedule schedule = new Schedule(); + schedule.setName("test-schedule"); + schedule.setResourceId("test-resource-id"); + schedule.setEnable(true); + schedule.setValue("0 0/1 * * * ?"); + schedule.setKey("test-resource-id"); + schedule.setCreateUser("admin"); + schedule.setProjectId(DEFAULT_PROJECT_ID); + schedule.setConfig("{}"); + schedule.setJob(ApiScenarioScheduleJob.class.getName()); + schedule.setType(ScheduleType.CRON.name()); + schedule.setResourceType(ScheduleResourceType.API_IMPORT.name()); + + scheduleService.addSchedule(schedule); + scheduleService.getSchedule(schedule.getId()); + scheduleService.editSchedule(schedule); + scheduleService.getScheduleByResource(schedule.getResourceId(), schedule.getJob()); + scheduleService.deleteByResourceId(schedule.getResourceId(), schedule.getJob()); + schedule = new Schedule(); + schedule.setName("test-schedule-1"); + schedule.setResourceId("test-resource-id-1"); + schedule.setEnable(true); + schedule.setValue("0 0/1 * * * ?"); + schedule.setKey("test-resource-id-1"); + schedule.setCreateUser("admin"); + schedule.setProjectId(DEFAULT_PROJECT_ID); + schedule.setConfig("{}"); + schedule.setJob(ApiScenarioScheduleJob.class.getName()); + schedule.setType(ScheduleType.CRON.name()); + schedule.setResourceType(ScheduleResourceType.API_SCENARIO.name()); + scheduleService.addSchedule(schedule); + scheduleService.deleteByResourceIds(List.of(schedule.getResourceId()), schedule.getJob()); + schedule = new Schedule(); + schedule.setName("test-schedule-2"); + schedule.setResourceId("test-resource-id-2"); + schedule.setEnable(true); + schedule.setValue("0 0/1 * * * ?"); + schedule.setKey("test-resource-id-2"); + schedule.setCreateUser("admin"); + schedule.setProjectId(DEFAULT_PROJECT_ID); + schedule.setConfig("{}"); + schedule.setJob("test-job"); + schedule.setType(ScheduleType.CRON.name()); + schedule.setResourceType(ScheduleResourceType.API_SCENARIO.name()); + scheduleService.addSchedule(schedule); + scheduleService.addOrUpdateCronJob(schedule, + new JobKey(schedule.getResourceId(), ApiScenarioScheduleJob.class.getName()), + new TriggerKey(schedule.getResourceId(), ApiScenarioScheduleJob.class.getName()), + ApiScenarioScheduleJob.class); + scheduleService.deleteByProjectId(schedule.getProjectId()); + + ScheduleConfig scheduleConfig = ScheduleConfig.builder() + .resourceId("test-resource-id-3") + .key("test-resource-id-3") + .projectId(DEFAULT_PROJECT_ID) + .name("test-schedule-3") + .enable(true) + .cron("0 0/1 * * * ?") + .resourceType(ScheduleResourceType.API_SCENARIO.name()) + .configMap(new HashMap<>() {{ + this.put("envId", "testEnv"); + this.put("resourcePoolId", "testResourcePool"); + }}) + .build(); + + scheduleService.scheduleConfig( + scheduleConfig, + new JobKey(scheduleConfig.getResourceId(), ApiScenarioScheduleJob.class.getName()), + new TriggerKey(scheduleConfig.getResourceId(), ApiScenarioScheduleJob.class.getName()), + ApiScenarioScheduleJob.class, + "admin"); + + + } +} diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/job/ApiScenarioScheduleJob.java b/backend/services/system-setting/src/test/java/io/metersphere/system/job/ApiScenarioScheduleJob.java new file mode 100644 index 0000000000..54b041d07b --- /dev/null +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/job/ApiScenarioScheduleJob.java @@ -0,0 +1,25 @@ +package io.metersphere.system.job; + +import io.metersphere.system.schedule.BaseScheduleJob; +import org.quartz.JobExecutionContext; +import org.quartz.JobKey; +import org.quartz.TriggerKey; + +public class ApiScenarioScheduleJob extends BaseScheduleJob { + @Override + protected void businessExecute(JobExecutionContext context) { + /* + TODO to Chen Jianxing: + 这里需要补充执行逻辑 + 记得执行信息(环境、资源池、是否失败停止等配置)在jobDataMap里面 + */ + } + + public static JobKey getJobKey(String scenarioId) { + return new JobKey(scenarioId, ApiScenarioScheduleJob.class.getName()); + } + + public static TriggerKey getTriggerKey(String scenarioId) { + return new TriggerKey(scenarioId, ApiScenarioScheduleJob.class.getName()); + } +} diff --git a/backend/services/test-plan/src/main/java/io/metersphere/plan/dto/request/TestPlanCreateRequest.java b/backend/services/test-plan/src/main/java/io/metersphere/plan/dto/request/TestPlanCreateRequest.java index d85c8be3a4..c27e2df773 100644 --- a/backend/services/test-plan/src/main/java/io/metersphere/plan/dto/request/TestPlanCreateRequest.java +++ b/backend/services/test-plan/src/main/java/io/metersphere/plan/dto/request/TestPlanCreateRequest.java @@ -4,6 +4,7 @@ import io.metersphere.sdk.constants.ModuleConstants; import io.metersphere.sdk.constants.TestPlanConstants; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; @@ -58,6 +59,7 @@ public class TestPlanCreateRequest { @Schema(description = "测试计划通过阈值;0-100", requiredMode = Schema.RequiredMode.REQUIRED) @Max(value = 100, message = "{test_plan.pass_threshold.max}") + @Min(value = 0) private double passThreshold = 100; @Schema(description = "测试计划类型") private String type = TestPlanConstants.TEST_PLAN_TYPE_PLAN; diff --git a/backend/services/test-plan/src/test/java/io/metersphere/plan/controller/TestPlanTests.java b/backend/services/test-plan/src/test/java/io/metersphere/plan/controller/TestPlanTests.java index 9c9cf83e99..f6f1de5768 100644 --- a/backend/services/test-plan/src/test/java/io/metersphere/plan/controller/TestPlanTests.java +++ b/backend/services/test-plan/src/test/java/io/metersphere/plan/controller/TestPlanTests.java @@ -619,7 +619,7 @@ public class TestPlanTests extends BaseTest { 3.group_id 3.1 group_id不存在 3.2 group_id对应的测试计划type不是group - 4.参数校验:passThreshold大于100 + 4.参数校验:passThreshold大于100 、 小于0 5.重名校验 */ request.setName(null); @@ -633,6 +633,8 @@ public class TestPlanTests extends BaseTest { request.setGroupId(TestPlanConstants.TEST_PLAN_DEFAULT_GROUP_ID); request.setPassThreshold(100.111); this.requestPost(URL_POST_TEST_PLAN_ADD, request).andExpect(status().isBadRequest()); + request.setPassThreshold(-0.12); + this.requestPost(URL_POST_TEST_PLAN_ADD, request).andExpect(status().isBadRequest()); //测试权限 request.setProjectId(DEFAULT_PROJECT_ID);