feat(接口测试): 接口场景的定时任务功能开发

This commit is contained in:
song-tianyang 2024-01-29 16:00:13 +08:00 committed by 刘瑞斌
parent 0d85df85c7
commit f546b29ade
23 changed files with 583 additions and 18 deletions

View File

@ -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=用户不存在

View File

@ -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

View File

@ -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=用户不存在

View File

@ -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=用戶不存在

View File

@ -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());
}
}

View File

@ -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<String, Object> configMap;
}

View File

@ -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());
}
}

View File

@ -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<Schedule> getScheduleNotice(ApiScenarioScheduleConfigRequest request) {
ScheduleExample example = new ScheduleExample();
example.createCriteria().andResourceIdEqualTo(request.getScenarioId()).andJobEqualTo(ApiScenarioScheduleJob.class.getName());
return scheduleMapper.selectByExample(example);
}
public List<ApiScenario> getBatchOptionScenarios(ApiScenarioBatchRequest request) {
List<String> ids = apiScenarioService.doSelectIds(request, false);

View File

@ -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 记录完整的场景信息

View File

@ -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);
}
}

View File

@ -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<String> 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());

View File

@ -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);
}
}

View File

@ -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<OptionDTO> getDriverOptions(String organizationId) {
return jdbcDriverPluginService.getJdbcDriverOption(organizationId);

View File

@ -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;

View File

@ -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<String, Object> 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;
}
}

View File

@ -22,4 +22,11 @@ public interface ExtScheduleMapper {
List<ApiScenario> getApiScenarioListByIds(@Param("ids") List<String> ids);
long countByResourceId(String resourceId);
long countByIdAndEnable(@Param("id") String scheduleId, @Param("enable") boolean isEnable);
long countQuartzTriggersByResourceId(String scheduleId);
long countQuartzCronTriggersByResourceId(String scheduleId);
}

View File

@ -70,6 +70,28 @@
#{id}
</foreach>
</select>
<select id="countByIdAndEnable" resultType="java.lang.Long">
select count(*)
from schedule
where id = #{id}
and enable = #{enable}
</select>
<select id="countQuartzTriggersByResourceId" resultType="java.lang.Long">
SELECT *
FROM QRTZ_TRIGGERS
WHERE TRIGGER_NAME = #{0}
</select>
<select id="countQuartzCronTriggersByResourceId" resultType="java.lang.Long">
SELECT *
FROM QRTZ_CRON_TRIGGERS
WHERE TRIGGER_NAME = #{0}
</select>
<select id="countByResourceId" resultType="java.lang.Long">
SELECT count(*)
FROM schedule
WHERE resource_id = #{0}
</select>
</mapper>

View File

@ -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<Schedule> 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<String, Object> 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();
}
}

View File

@ -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"

View File

@ -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");
}
}

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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);