diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java b/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java index 215c7eeb70..8fa75ab260 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java @@ -2,6 +2,7 @@ package io.metersphere.api.controller.definition; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; +import io.metersphere.api.constants.ApiScenarioStepType; import io.metersphere.api.domain.ApiDefinition; import io.metersphere.api.dto.ReferenceDTO; import io.metersphere.api.dto.ReferenceRequest; @@ -10,8 +11,12 @@ import io.metersphere.api.dto.request.ApiEditPosRequest; import io.metersphere.api.dto.request.ApiTransferRequest; import io.metersphere.api.dto.request.ImportRequest; import io.metersphere.api.dto.schema.JsonSchemaItem; +import io.metersphere.api.mapper.ExtApiDefinitionMapper; +import io.metersphere.api.mapper.ExtApiScenarioStepMapper; +import io.metersphere.api.mapper.ExtApiTestCaseMapper; import io.metersphere.api.service.ApiFileResourceService; import io.metersphere.api.service.definition.*; +import io.metersphere.api.service.scenario.ApiScenarioService; import io.metersphere.project.service.FileModuleService; import io.metersphere.sdk.constants.DefaultRepositoryDir; import io.metersphere.sdk.constants.PermissionConstants; @@ -34,6 +39,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotBlank; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; @@ -41,6 +47,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.util.ArrayList; import java.util.List; @@ -51,6 +58,14 @@ import java.util.List; @RequestMapping(value = "/api/definition") @Tag(name = "接口测试-接口管理-接口定义") public class ApiDefinitionController { + + @Resource + private ExtApiDefinitionMapper extApiDefinitionMapper; + @Resource + private ExtApiTestCaseMapper extApiTestCaseMapper; + @Resource + private ExtApiScenarioStepMapper extApiScenarioStepMapper; + @Resource private ApiDefinitionService apiDefinitionService; @Resource @@ -61,12 +76,55 @@ public class ApiDefinitionController { private ApiDefinitionImportService apiDefinitionImportService; @Resource private ApiDefinitionExportService apiDefinitionExportService; + @Resource + private ApiScenarioService apiScenarioService; + + /* + 接口覆盖率 + 业务注释,误删 + * 一个接口如果被跨项目的场景给关联了,算不算覆盖? 不算 + * 自定义请求, 不管它有多少个“/"有多少子域 , 跟接口定义匹配的时候就用末端匹配法。 + · 例如:https://www.tapd.cn/tapd_fe/my/work?dialog_preview_id=abcdefg + ·/work能匹配的上 + ·/my/work能匹配的上 + ·/my 不可以 + ·/my/{something}可以匹配的上 + ·/my/{something}/{other-thing}不可以 + * 剩下的基本上就跟V2一样了. 有用例 or 被场景引用/复制 or 被自定义给命中了 就算覆盖。 且自定义请求可以命中多个接口定义,比如上一点 + */ + @GetMapping("/rage/{projectId}") + @Operation(summary = "接口测试-接口管理-接口列表(deleted 状态为 1 时为回收站数据)") + @RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_READ) + @CheckOwner(resourceId = "#projectId", resourceType = "project") + public ApiCoverageDTO rage(@PathVariable String projectId) { + List apiAllIds = new ArrayList<>(); + List httpApiList = new ArrayList<>(); + extApiDefinitionMapper.selectBaseInfoByProjectId(projectId).forEach(apiDefinition -> { + if (StringUtils.equalsIgnoreCase(apiDefinition.getProtocol(), "http")) { + httpApiList.add(apiDefinition); + } + apiAllIds.add(apiDefinition.getId()); + }); + + List apiDefinitionIdFromCase = extApiTestCaseMapper.selectApiId(projectId); + List apiInScenarioStep = extApiScenarioStepMapper.selectResourceId(projectId, ApiScenarioStepType.API.name()); + List apiCaseIdInStep = extApiScenarioStepMapper.selectResourceId(projectId, ApiScenarioStepType.API_CASE.name()); + if (CollectionUtils.isNotEmpty(apiCaseIdInStep)) { + List apiCaseIdInScenarioStep = extApiTestCaseMapper.selectApiIdByCaseId(apiCaseIdInStep); + apiInScenarioStep.addAll(apiCaseIdInScenarioStep); + } + + List apiInStepList = apiScenarioService.selectApiIdInCustomRequest(projectId, httpApiList); + apiInStepList.addAll(apiInScenarioStep); + + return new ApiCoverageDTO(apiAllIds, apiDefinitionIdFromCase, apiInStepList); + } @PostMapping(value = "/add") @Operation(summary = "接口测试-接口管理-添加接口定义") @RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_ADD) @Log(type = OperationLogType.ADD, expression = "#msClass.addLog(#request)", msClass = ApiDefinitionLogService.class) - @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + @CheckOwner(resourceId = "#request.getProjectId()s", resourceType = "project") @SendNotice(taskType = NoticeConstants.TaskType.API_DEFINITION_TASK, event = NoticeConstants.Event.CREATE, target = "#targetClass.getApiDTO(#request)", targetClass = ApiDefinitionNoticeService.class) public ApiDefinition add(@Validated @RequestBody ApiDefinitionAddRequest request) { return apiDefinitionService.create(request, SessionUtils.getUserId()); diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioBatchOperationController.java b/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioBatchOperationController.java index d3f41c15e7..1df9c5cefc 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioBatchOperationController.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioBatchOperationController.java @@ -3,10 +3,6 @@ package io.metersphere.api.controller.scenario; import io.metersphere.api.constants.ApiResource; import io.metersphere.api.dto.response.ApiScenarioBatchOperationResponse; import io.metersphere.api.dto.scenario.*; -import io.metersphere.api.dto.scenario.ApiScenarioBatchCopyMoveRequest; -import io.metersphere.api.dto.scenario.ApiScenarioBatchEditRequest; -import io.metersphere.api.dto.scenario.ApiScenarioBatchRequest; -import io.metersphere.api.dto.scenario.ApiScenarioBatchRunRequest; import io.metersphere.api.service.ApiValidateService; import io.metersphere.api.service.scenario.ApiScenarioBatchRunService; import io.metersphere.api.service.scenario.ApiScenarioNoticeService; diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/definition/ApiCoverageDTO.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/definition/ApiCoverageDTO.java new file mode 100644 index 0000000000..39559d73cd --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/definition/ApiCoverageDTO.java @@ -0,0 +1,82 @@ +package io.metersphere.api.dto.definition; + +import io.metersphere.sdk.util.CalculateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author lan + */ +@Data +@NoArgsConstructor +public class ApiCoverageDTO { + + @Schema(description = "接口定义总量") + private int allApiCount; + + @Schema(description = "接口定义未覆盖") + private int unCoverWithApiDefinition; + + @Schema(description = "接口定义已覆盖") + private int coverWithApiDefinition; + + @Schema(description = "接口覆盖率 接口(URL)有(用例或场景步骤)数/接口总数*100%") + private String apiCoverage; + + @Schema(description = "接口用例未覆盖") + private int unCoverWithApiCase; + + @Schema(description = "接口用例已覆盖") + private int coverWithApiCase; + + @Schema(description = "用例覆盖率 有用例的接口/接口总数*100%") + private String apiCaseCoverage; + + @Schema(description = "接口场景未覆盖") + private int unCoverWithApiScenario; + + @Schema(description = "接口场景已覆盖") + private int coverWithApiScenario; + + @Schema(description = "场景覆盖率 被场景步骤包含的接口(URL)数/接口总数*100%") + private String scenarioCoverage; + + public ApiCoverageDTO(List allApiId, List haveCaseIdList, List apiIdOrUrlInStepList) { + // 去重、过滤(只留下apiId中存在的数据,避免已删除的apiId导致统计错误) + allApiId = allApiId.stream().distinct().toList(); + haveCaseIdList = this.elementInList(allApiId, haveCaseIdList.stream().distinct().toList()); + apiIdOrUrlInStepList = this.elementInList(allApiId, apiIdOrUrlInStepList.stream().distinct().toList()); + + this.allApiCount = allApiId.size(); + // 用例覆盖率: 有用例的接口/接口总数*100% + this.coverWithApiCase = haveCaseIdList.size(); + this.unCoverWithApiCase = this.allApiCount - this.coverWithApiCase; + this.apiCaseCoverage = CalculateUtils.reportPercentage(coverWithApiCase, allApiCount); + + // 场景覆盖率: 被场景步骤包含的接口(URL)数/接口总数*100% + this.coverWithApiScenario = apiIdOrUrlInStepList.size(); + this.unCoverWithApiScenario = this.allApiCount - this.coverWithApiScenario; + this.scenarioCoverage = CalculateUtils.reportPercentage(coverWithApiScenario, allApiCount); + + // 接口覆盖率 + apiIdOrUrlInStepList.addAll(haveCaseIdList); + List allCoverList = apiIdOrUrlInStepList.stream().distinct().toList(); + this.coverWithApiDefinition = allCoverList.size(); + this.unCoverWithApiDefinition = this.allApiCount - this.coverWithApiDefinition; + this.apiCoverage = CalculateUtils.reportPercentage(coverWithApiDefinition, allApiCount); + } + + private List elementInList(List allList, List compareList) { + List returnList = new ArrayList<>(); + compareList.forEach(item -> { + if (allList.contains(item)) { + returnList.add(item); + } + }); + return returnList; + } +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchEditRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchEditRequest.java index c32e0d8e98..5c27c3488f 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchEditRequest.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchEditRequest.java @@ -19,7 +19,7 @@ public class ApiScenarioBatchEditRequest extends ApiScenarioBatchRequest impleme @Schema(description = "标签") private LinkedHashSet tags; - @Schema(description = "批量编辑的类型 用例等级: Priority,状态 :Status,标签: Tags,用例环境: Environment, 定时任务:Schedule") + @Schema(description = "批量编辑的类型 用例等级: Priority,状态 :Status,标签: Tags,用例环境: Environment") @NotBlank private String type; @Schema(description = "默认覆盖原标签") @@ -40,8 +40,6 @@ public class ApiScenarioBatchEditRequest extends ApiScenarioBatchRequest impleme @Schema(description = "用例等级") @Size(max = 50, message = "{api_test_case.priority.length_range}") private String priority; - @Schema(description = "定时任务是否开启") - private boolean scheduleOpen; public List getTags() { if (tags == null) { return new ArrayList<>(0); diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchScheduleConfigRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchScheduleConfigRequest.java index c36e3fb3db..3a649a6593 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchScheduleConfigRequest.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioBatchScheduleConfigRequest.java @@ -2,7 +2,6 @@ package io.metersphere.api.dto.scenario; import io.metersphere.sdk.dto.api.task.ApiRunModeConfigDTO; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.EqualsAndHashCode; @@ -18,7 +17,6 @@ public class ApiScenarioBatchScheduleConfigRequest extends ApiScenarioBatchReque private boolean enable; @Schema(description = "Cron表达式") - @NotBlank private String cron; @Schema(description = "定时任务配置") diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java index 6e535aeaf4..fb6ec96425 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java @@ -129,4 +129,5 @@ public interface ExtApiDefinitionMapper { List getCreateApiList(@Param("projectId") String projectId, @Param("startTime") Long startTime, @Param("endTime") Long endTime); + List selectBaseInfoByProjectId(String projectId); } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml index f2d4df7fa9..29b655fe5e 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml @@ -838,4 +838,10 @@ AND api_definition.create_time BETWEEN #{startTime} AND #{endTime} + diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.java b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.java index ef0129dc55..0b475b337a 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.java @@ -1,6 +1,7 @@ package io.metersphere.api.mapper; import io.metersphere.api.domain.ApiScenarioCsvStep; +import io.metersphere.api.domain.ApiScenarioStep; import io.metersphere.api.dto.scenario.ApiScenarioStepDTO; import org.apache.ibatis.annotations.Param; @@ -25,4 +26,8 @@ public interface ExtApiScenarioStepMapper { * @return */ List getHasBlobRequestStepIds(@Param("scenarioId") String scenarioId); + + List selectResourceId(@Param("projectId") String projectId, @Param("stepType") String stepType); + + List selectCustomRequestConfigByProjectId(String projectId); } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.xml b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.xml index 4e0edf75e9..75eb0960cf 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.xml +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioStepMapper.xml @@ -28,4 +28,21 @@ and step_type in ('API', 'API_CASE', 'CUSTOM_REQUEST') and ref_type in ('COPY', 'DIRECT') + + \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.java b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.java index b9e6aac380..3822eff39b 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.java @@ -139,4 +139,8 @@ public interface ExtApiTestCaseMapper { List getSimpleApiCaseList(@Param("projectId") String projectId, @Param("startTime") Long startTime, @Param("endTime") Long endTime); + + List selectApiId(String projectId); + + List selectApiIdByCaseId(@Param("ids") List apiCaseIdInStep); } \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.xml b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.xml index 6ff8b83237..24b031099d 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.xml +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiTestCaseMapper.xml @@ -1031,4 +1031,18 @@ + + \ No newline at end of file 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 ddd20d9549..68b8b1ac1d 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 @@ -25,10 +25,12 @@ import io.metersphere.api.service.ApiFileResourceService; import io.metersphere.api.service.definition.ApiDefinitionService; import io.metersphere.api.service.definition.ApiTestCaseService; import io.metersphere.api.utils.ApiDataUtils; +import io.metersphere.api.utils.ApiDefinitionUtils; import io.metersphere.api.utils.ApiScenarioBatchOperationUtils; import io.metersphere.api.utils.ApiScenarioUtils; import io.metersphere.functional.domain.FunctionalCaseTestExample; import io.metersphere.functional.mapper.FunctionalCaseTestMapper; +import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement; import io.metersphere.plugin.api.spi.AbstractMsTestElement; import io.metersphere.project.domain.*; import io.metersphere.project.dto.MoveNodeSortDTO; @@ -278,7 +280,6 @@ public class ApiScenarioService extends MoveNodeService { case STATUS -> batchUpdateStatus(example, updateScenario, request.getStatus(), mapper); case TAGS -> batchUpdateTags(example, updateScenario, request, ids, mapper); case ENVIRONMENT -> batchUpdateEnvironment(example, updateScenario, request, mapper); - case SCHEDULE -> batchUpdateSchedule(example, request, mapper, userId); default -> throw new MSException(Translator.get("batch_edit_type_error")); } sqlSession.flushStatements(); @@ -288,15 +289,6 @@ public class ApiScenarioService extends MoveNodeService { apiScenarioNoticeService.batchSendNotice(ids, userId, projectId, NoticeConstants.Event.UPDATE); } - private void batchUpdateSchedule(ApiScenarioExample example, ApiScenarioBatchEditRequest request, ApiScenarioMapper mapper, String userId) { - List apiScenarioList = mapper.selectByExample(example); - //批量编辑定时任务 - for (ApiScenario apiScenario : apiScenarioList) { - scheduleService.updateIfExist(apiScenario.getId(), request.isScheduleOpen(), ApiScenarioScheduleJob.getJobKey(apiScenario.getId()), - ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()), ApiScenarioScheduleJob.class, userId); - } - } - private void batchUpdateEnvironment(ApiScenarioExample example, ApiScenario updateScenario, ApiScenarioBatchEditRequest request, ApiScenarioMapper mapper) { if (BooleanUtils.isFalse(request.isGrouped())) { @@ -2566,30 +2558,42 @@ public class ApiScenarioService extends MoveNodeService { List apiScenarios = apiScenarioMapper.selectByExample(example); if (CollectionUtils.isNotEmpty(apiScenarios)) { - apiScenarios.forEach(apiScenario -> { - ScheduleConfig scheduleConfig = ScheduleConfig.builder() - .resourceId(apiScenario.getId()) - .key(apiScenario.getId()) - .projectId(apiScenario.getProjectId()) - .name(apiScenario.getName()) - .enable(request.isEnable()) - .cron(request.getCron()) - .resourceType(ScheduleResourceType.API_SCENARIO.name()) - .config(JSON.toJSONString(request.getConfig())) - .build(); + if (StringUtils.isBlank(request.getCron()) && request.getConfig() == null) { + this.batchUpdateSchedule(apiScenarios, request.isEnable(), operator); + } else { + apiScenarios.forEach(apiScenario -> { + ScheduleConfig scheduleConfig = ScheduleConfig.builder() + .resourceId(apiScenario.getId()) + .key(apiScenario.getId()) + .projectId(apiScenario.getProjectId()) + .name(apiScenario.getName()) + .enable(request.isEnable()) + .cron(request.getCron()) + .resourceType(ScheduleResourceType.API_SCENARIO.name()) + .config(JSON.toJSONString(request.getConfig())) + .build(); - scheduleService.scheduleConfig( - scheduleConfig, - ApiScenarioScheduleJob.getJobKey(apiScenario.getId()), - ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()), - ApiScenarioScheduleJob.class, - operator); - }); - apiScenarioLogService.batchScheduleConfigLog(request.getProjectId(), apiScenarios, operator); + scheduleService.scheduleConfig( + scheduleConfig, + ApiScenarioScheduleJob.getJobKey(apiScenario.getId()), + ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()), + ApiScenarioScheduleJob.class, + operator); + }); + apiScenarioLogService.batchScheduleConfigLog(request.getProjectId(), apiScenarios, operator); + } } } } + private void batchUpdateSchedule(List apiScenarioList, boolean isScheudleOpen, String userId) { + //批量编辑定时任务 + for (ApiScenario apiScenario : apiScenarioList) { + scheduleService.updateIfExist(apiScenario.getId(), isScheudleOpen, ApiScenarioScheduleJob.getJobKey(apiScenario.getId()), + ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()), ApiScenarioScheduleJob.class, userId); + } + } + // 场景统计相关 public List calculateRate(List ids) { List result = new ArrayList<>(); @@ -2619,4 +2623,47 @@ public class ApiScenarioService extends MoveNodeService { } return result; } + + public List selectApiIdInCustomRequest(String projectId, List apiDefinitions) { + List returnList = new ArrayList<>(); + List stepConfigList = extApiScenarioStepMapper.selectCustomRequestConfigByProjectId(projectId); + List requestIdList = new ArrayList<>(); + stepConfigList.forEach(step -> requestIdList.add(step.getId())); + if (requestIdList.isEmpty()) { + return returnList; + } + ApiScenarioStepBlobExample scenarioStepBlobExample = new ApiScenarioStepBlobExample(); + scenarioStepBlobExample.createCriteria().andIdIn(requestIdList); + List httpRequestStopBlobList = apiScenarioStepBlobMapper.selectByExampleWithBLOBs(scenarioStepBlobExample); + Map> methodPathMap = new HashMap<>(); + httpRequestStopBlobList.forEach(blob -> { + if (blob.getContent() != null) { + try { + AbstractMsProtocolTestElement protocolTestElement = ApiDataUtils.parseObject(new String(blob.getContent()), AbstractMsProtocolTestElement.class); + if (protocolTestElement instanceof MsHTTPElement msHTTPElement) { + String method = msHTTPElement.getMethod(); + if (methodPathMap.containsKey(method)) { + methodPathMap.get(method).add(msHTTPElement.getPath()); + } else { + List pathList = new ArrayList<>(); + pathList.add(msHTTPElement.getPath()); + methodPathMap.put(method, pathList); + } + } + } catch (Exception e) { + LogUtils.error(e); + } + } + }); + for (ApiDefinition apiDefinition : apiDefinitions) { + if (methodPathMap.containsKey(apiDefinition.getMethod())) { + String apiPath = apiDefinition.getPath(); + List customUrlList = methodPathMap.get(apiDefinition.getMethod()); + if (ApiDefinitionUtils.isUrlInList(apiPath, customUrlList)) { + returnList.add(apiDefinition.getId()); + } + } + } + return returnList; + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/utils/ApiDefinitionUtils.java b/backend/services/api-test/src/main/java/io/metersphere/api/utils/ApiDefinitionUtils.java new file mode 100644 index 0000000000..73089d6455 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/utils/ApiDefinitionUtils.java @@ -0,0 +1,61 @@ +package io.metersphere.api.utils; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; + +public class ApiDefinitionUtils { + + public static boolean isUrlInList(String apiUrl, Collection customRequestUrlList) { + if (CollectionUtils.isEmpty(customRequestUrlList)) { + return false; + } + + String urlSuffix = apiUrl.trim(); + if (urlSuffix.startsWith("/")) { + urlSuffix = urlSuffix.substring(1); + } + String[] urlParams = urlSuffix.split("/"); + + for (String customRequestUrl : customRequestUrlList) { + if (StringUtils.equalsAny(customRequestUrl, apiUrl, "/" + apiUrl)) { + return true; + } else { + if (StringUtils.isEmpty(customRequestUrl)) { + continue; + } + if (customRequestUrl.startsWith("/")) { + customRequestUrl = customRequestUrl.substring(1); + } + if (StringUtils.isNotEmpty(customRequestUrl)) { + String[] customUrlArr = customRequestUrl.split("/"); + + if (customUrlArr.length >= urlParams.length) { + + boolean isFetch = true; + for (int urlIndex = 0; urlIndex < urlParams.length; urlIndex++) { + String urlItem = urlParams[urlIndex]; + String customUrlItem = customUrlArr[customUrlArr.length - urlParams.length + urlIndex]; + // 不为rest参数的要进行全匹配。 而且忽略大小写 + if (isRestUrlParam(customUrlItem) && isRestUrlParam(urlItem)) { + if (!StringUtils.equalsIgnoreCase(customUrlItem, urlItem)) { + isFetch = false; + break; + } + } + } + if (isFetch) { + return true; + } + } + } + } + } + return false; + } + + private static boolean isRestUrlParam(String urlParam) { + return !StringUtils.startsWith(urlParam, "{") || !StringUtils.endsWith(urlParam, "}") || StringUtils.equals(urlParam, "{}"); + } +} diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiCalculateTest.java b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiCalculateTest.java new file mode 100644 index 0000000000..3abec57904 --- /dev/null +++ b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiCalculateTest.java @@ -0,0 +1,237 @@ +package io.metersphere.api.controller; + +import io.metersphere.api.constants.*; +import io.metersphere.api.domain.ApiDefinition; +import io.metersphere.api.domain.ApiDefinitionExample; +import io.metersphere.api.domain.ApiScenario; +import io.metersphere.api.domain.ApiTestCase; +import io.metersphere.api.dto.definition.ApiCoverageDTO; +import io.metersphere.api.dto.definition.ApiDefinitionAddRequest; +import io.metersphere.api.dto.definition.ApiTestCaseAddRequest; +import io.metersphere.api.dto.definition.HttpResponse; +import io.metersphere.api.dto.request.http.MsHTTPElement; +import io.metersphere.api.dto.scenario.ApiScenarioAddRequest; +import io.metersphere.api.dto.scenario.ApiScenarioStepRequest; +import io.metersphere.api.dto.scenario.ApiScenarioUpdateRequest; +import io.metersphere.api.mapper.ApiDefinitionMapper; +import io.metersphere.api.utils.ApiDataUtils; +import io.metersphere.project.domain.Project; +import io.metersphere.project.mapper.ExtBaseProjectVersionMapper; +import io.metersphere.project.mapper.ProjectMapper; +import io.metersphere.sdk.util.CalculateUtils; +import io.metersphere.sdk.util.JSON; +import io.metersphere.system.base.BaseTest; +import io.metersphere.system.dto.AddProjectRequest; +import io.metersphere.system.log.constants.OperationLogModule; +import io.metersphere.system.service.CommonProjectService; +import io.metersphere.system.uid.IDGenerator; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.*; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MvcResult; + +import java.util.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ApiCalculateTest extends BaseTest { + private static Project project; + + @Resource + private CommonProjectService commonProjectService; + @Resource + private ExtBaseProjectVersionMapper extBaseProjectVersionMapper; + @Resource + private ProjectMapper projectMapper; + + @Resource + private ApiDefinitionMapper apiDefinitionMapper; + + @BeforeEach + public void initTestData() throws Exception { + //测试计划专用项目 + if (project == null) { + AddProjectRequest initProject = new AddProjectRequest(); + initProject.setOrganizationId("100001"); + initProject.setName("文件管理专用项目"); + initProject.setDescription("建国创建的文件管理专用项目"); + initProject.setEnable(true); + initProject.setUserIds(List.of("admin")); + project = commonProjectService.add(initProject, "admin", "/organization-project/add", OperationLogModule.SETTING_ORGANIZATION_PROJECT); + + ArrayList moduleList = new ArrayList<>(List.of(new String[]{"workstation", "testPlan", "bugManagement", "caseManagement", "apiTest", "uiTest", "loadTest"})); + Project updateProject = new Project(); + updateProject.setId(project.getId()); + updateProject.setModuleSetting(JSON.toJSONString(moduleList)); + projectMapper.updateByPrimaryKeySelective(updateProject); + + + ApiScenarioAddRequest apiScenarioAddRequest = new ApiScenarioAddRequest(); + apiScenarioAddRequest.setProjectId(project.getId()); + apiScenarioAddRequest.setDescription("desc"); + apiScenarioAddRequest.setName("test name"); + apiScenarioAddRequest.setModuleId("default"); + apiScenarioAddRequest.setGrouped(false); + apiScenarioAddRequest.setTags(List.of("tag1", "tag2")); + apiScenarioAddRequest.setPriority("P0"); + apiScenarioAddRequest.setStatus(ApiScenarioStatus.COMPLETED.name()); + apiScenarioAddRequest.setSteps(new ArrayList<>()); + MvcResult scenarioMvcResult = this.requestPostWithOkAndReturn("/api/scenario/add", apiScenarioAddRequest); + ApiScenario scenario = getResultData(scenarioMvcResult, ApiScenario.class); + + + // 创建接口定义 + Map> methodAndPath = Map.of( + "GET", List.of( + "/api/get-test/1", + "/api/get-test/2", + "/api/{/get-test}/3/withCase", + "/api/get-test/4/withCase",// 场景关联它的用例 + "/api/get-test/5/never-compare", + "/{api}/{/get-test}/{6}",//这个一定会被匹配到 + "/api/get-test/{7}",// 这个一定会被匹配到 + "/api/get-test/8",// 场景关联它的自定义请求 + "/api/get-test/9",// 场景关联这个接口 + "/api/get-test/10"), + "POST", List.of( + "/post/api/test/1", + "/post/api/test/2", + "/post/api/{test}/3/withCase", + "/post/api/test/4/withCase",// 场景关联它的用例 + "/post/api/test/5/never-compare", + "/{post}/{api}/{/get-test}/{6}", //这个一定会被匹配到 + "/post/api/test/{7}", // 这个一定会被匹配到 + "/post/api/test/8", // 场景关联它的自定义请求 + "/post/api/test/9",// 场景关联这个接口 + "/post/api/test/10" + ) + ); + + List steps = new ArrayList<>(); + Map steptDetailMap = new HashMap<>(); + // 创建接口用例 + for (Map.Entry> entry : methodAndPath.entrySet()) { + String method = entry.getKey(); + for (String path : entry.getValue()) { + // 创建接口定义 + String defaultVersion = extBaseProjectVersionMapper.getDefaultVersion(project.getId()); + ApiDefinitionAddRequest apiDefinitionAddRequest = new ApiDefinitionAddRequest(); + apiDefinitionAddRequest.setName(method + "_" + path); + apiDefinitionAddRequest.setProtocol(ApiConstants.HTTP_PROTOCOL); + apiDefinitionAddRequest.setProjectId(project.getId()); + apiDefinitionAddRequest.setMethod(method); + apiDefinitionAddRequest.setPath(path); + apiDefinitionAddRequest.setStatus(ApiDefinitionStatus.PROCESSING.name()); + apiDefinitionAddRequest.setModuleId("default"); + apiDefinitionAddRequest.setVersionId(defaultVersion); + apiDefinitionAddRequest.setDescription("描述内容"); + apiDefinitionAddRequest.setTags(new LinkedHashSet<>(List.of("tag1", "tag2"))); + + MsHTTPElement msHttpElement = MsHTTPElementTest.getMsHttpElement(); + msHttpElement.setBody(MsHTTPElementTest.getGeneralBody()); + msHttpElement.setMethod(method); + msHttpElement.setPath(path); + + apiDefinitionAddRequest.setRequest(JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement))); + List msHttpResponse = MsHTTPElementTest.getMsHttpResponse(); + apiDefinitionAddRequest.setResponse(msHttpResponse); + + MvcResult mvcResult = this.requestPostWithOkAndReturn("/api/definition/add", apiDefinitionAddRequest); + ApiDefinition resultData = getResultData(mvcResult, ApiDefinition.class); + + if (path.endsWith("/withCase")) { + ApiTestCaseAddRequest request = new ApiTestCaseAddRequest(); + request.setApiDefinitionId(resultData.getId()); + request.setName(resultData.getName() + "_case"); + request.setProjectId(project.getId()); + request.setPriority("P0"); + request.setStatus(ApiDefinitionStatus.PROCESSING.name()); + request.setTags(new LinkedHashSet<>(List.of("tag1", "tag2"))); + request.setRequest(JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement))); + MvcResult testCaseResult = this.requestPostWithOkAndReturn("/api/case/add", request); + ApiTestCase apiTestCase = getResultData(testCaseResult, ApiTestCase.class); + + if (path.endsWith("/4/withCase")) { + ApiScenarioStepRequest stepRequest = new ApiScenarioStepRequest(); + stepRequest.setId(IDGenerator.nextStr()); + stepRequest.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(project.getId())); + stepRequest.setConfig(new HashMap<>()); + stepRequest.setEnable(true); + stepRequest.setStepType(ApiScenarioStepType.API_CASE.name()); + stepRequest.setResourceId(apiTestCase.getId()); + stepRequest.setName(apiTestCase.getName() + "_step"); + stepRequest.setRefType(ApiScenarioStepRefType.REF.name()); + stepRequest.setProjectId(project.getId()); + steps.add(stepRequest); + steptDetailMap.put(stepRequest.getId(), JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement))); + } + } else if (path.endsWith("/8")) { + + ApiScenarioStepRequest stepRequest = new ApiScenarioStepRequest(); + stepRequest.setId(IDGenerator.nextStr()); + stepRequest.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(project.getId())); + stepRequest.setConfig(new HashMap<>()); + stepRequest.setEnable(true); + stepRequest.setStepType(ApiScenarioStepType.CUSTOM_REQUEST.name()); + stepRequest.setName("custom_step"); + stepRequest.setRefType(ApiScenarioStepRefType.DIRECT.name()); + stepRequest.setProjectId(project.getId()); + steps.add(stepRequest); + + MsHTTPElement customElement = MsHTTPElementTest.getMsHttpElement(); + customElement.setBody(MsHTTPElementTest.getGeneralBody()); + customElement.setMethod(method); + customElement.setPath(path); + steptDetailMap.put(stepRequest.getId(), JSON.parseObject(ApiDataUtils.toJSONString(customElement))); + } else if (path.endsWith("/9")) { + ApiScenarioStepRequest stepRequest = new ApiScenarioStepRequest(); + stepRequest.setId(IDGenerator.nextStr()); + stepRequest.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(project.getId())); + stepRequest.setConfig(new HashMap<>()); + stepRequest.setEnable(true); + stepRequest.setStepType(ApiScenarioStepType.API.name()); + stepRequest.setResourceId(resultData.getId()); + stepRequest.setName(resultData.getName() + "_step"); + stepRequest.setRefType(ApiScenarioStepRefType.REF.name()); + stepRequest.setProjectId(project.getId()); + steps.add(stepRequest); + steptDetailMap.put(stepRequest.getId(), JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement))); + } + } + } + + ApiScenarioUpdateRequest apiScenarioUpdateRequest = new ApiScenarioUpdateRequest(); + apiScenarioUpdateRequest.setId(scenario.getId()); + apiScenarioUpdateRequest.setProjectId(project.getId()); + apiScenarioUpdateRequest.setSteps(steps); + apiScenarioUpdateRequest.setStepDetails(steptDetailMap); + this.requestPostWithOkAndReturn("/api/scenario/update", apiScenarioUpdateRequest); + + } + } + + @Test + public void calculateTest() throws Exception { + ApiDefinitionExample apiDefinitionExample = new ApiDefinitionExample(); + apiDefinitionExample.createCriteria().andProjectIdEqualTo(project.getId()); + Assertions.assertEquals(apiDefinitionMapper.countByExample(apiDefinitionExample), 20); + + MvcResult mvcResult = this.requestGetWithOkAndReturn("/api/definition/rage/" + project.getId()); + ApiCoverageDTO apiCoverageDTO = getResultData(mvcResult, ApiCoverageDTO.class); + Assertions.assertEquals(apiCoverageDTO.getAllApiCount(), 20); + Assertions.assertEquals(apiCoverageDTO.getCoverWithApiCase(), 4); + Assertions.assertEquals(apiCoverageDTO.getUnCoverWithApiCase(), 16); + Assertions.assertEquals(apiCoverageDTO.getApiCaseCoverage(), CalculateUtils.reportPercentage(apiCoverageDTO.getCoverWithApiCase(), apiCoverageDTO.getAllApiCount())); + + Assertions.assertEquals(apiCoverageDTO.getCoverWithApiScenario(), 8); + Assertions.assertEquals(apiCoverageDTO.getUnCoverWithApiScenario(), 12); + Assertions.assertEquals(apiCoverageDTO.getScenarioCoverage(), CalculateUtils.reportPercentage(apiCoverageDTO.getCoverWithApiScenario(), apiCoverageDTO.getAllApiCount())); + + Assertions.assertEquals(apiCoverageDTO.getCoverWithApiDefinition(), 10); + Assertions.assertEquals(apiCoverageDTO.getUnCoverWithApiDefinition(), 10); + Assertions.assertEquals(apiCoverageDTO.getApiCoverage(), CalculateUtils.reportPercentage(apiCoverageDTO.getCoverWithApiDefinition(), apiCoverageDTO.getAllApiCount())); + } + +} diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiReportControllerTests.java b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiReportControllerTests.java index d0ba04c276..6c5f20dc48 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiReportControllerTests.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiReportControllerTests.java @@ -173,7 +173,7 @@ public class ApiReportControllerTests extends BaseTest { Assertions.assertNotNull(returnPager); //返回值的页码和当前页码相同 Assertions.assertEquals(returnPager.getCurrent(), request.getCurrent()); - ; + //返回的数据量不超过规定要返回的数据量相同 Assertions.assertTrue(((List) returnPager.getList()).size() <= request.getPageSize()); //过滤 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 d1bba1a4e4..66849375a9 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 @@ -2144,6 +2144,18 @@ public class ApiScenarioControllerTests extends BaseTest { batchRequest.setEnable(false); this.requestPostWithOk(batchUrl, batchRequest); apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchRequest.isEnable()); + + // 仅仅是开启/关闭 + batchRequest = new ApiScenarioBatchScheduleConfigRequest(); + batchRequest.setProjectId(DEFAULT_PROJECT_ID); + batchRequest.setSelectIds(List.of(BATCH_OPERATION_SCENARIO_ID.getFirst())); + batchRequest.setEnable(true); + this.requestPostWithOk(batchUrl, batchRequest); + apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchRequest.isEnable()); + batchRequest.setEnable(false); + this.requestPostWithOk(batchUrl, batchRequest); + apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchRequest.isEnable()); + //增加日志检查 LOG_CHECK_LIST.add( new CheckLogModel(scenarioId, OperationLogType.UPDATE, "/api/scenario/schedule-config") @@ -2151,17 +2163,6 @@ public class ApiScenarioControllerTests extends BaseTest { LOG_CHECK_LIST.add( new CheckLogModel(BATCH_OPERATION_SCENARIO_ID.getFirst(), OperationLogType.UPDATE, "/api/scenario/batch-operation/schedule-config") ); - // 批量定时任务的开关 - ApiScenarioBatchEditRequest batchEditRequest = new ApiScenarioBatchEditRequest(); - batchEditRequest.setProjectId(DEFAULT_PROJECT_ID); - batchEditRequest.setType("Schedule"); - batchEditRequest.setScheduleOpen(true); - batchEditRequest.setSelectIds(List.of(BATCH_OPERATION_SCENARIO_ID.getFirst())); - requestPostAndReturn(BATCH_EDIT, batchEditRequest); - apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchEditRequest.isScheduleOpen()); - batchEditRequest.setScheduleOpen(false); - requestPostAndReturn(BATCH_EDIT, batchEditRequest); - apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchEditRequest.isScheduleOpen()); //关闭 request.setEnable(false); result = this.requestPostAndReturn(testUrl, request); @@ -2240,11 +2241,6 @@ public class ApiScenarioControllerTests extends BaseTest { 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); diff --git a/frontend/src/views/api-test/scenario/components/common/exportScenario/scenarioExportModal.vue b/frontend/src/views/api-test/scenario/components/common/exportScenario/scenarioExportModal.vue index 983147cc0c..2ed5c98a58 100644 --- a/frontend/src/views/api-test/scenario/components/common/exportScenario/scenarioExportModal.vue +++ b/frontend/src/views/api-test/scenario/components/common/exportScenario/scenarioExportModal.vue @@ -6,18 +6,7 @@ class="ms-modal-upload ms-modal-medium" :width="400" > -
-
-
{{ item.name }}
-
-
- -
+
{{ t('apiScenario.export.type.all') }} @@ -58,18 +47,6 @@ import useAppStore from '@/store/modules/app'; import { downloadByteFile, getGenerateId } from '@/utils'; - import { RequestImportFormat } from '@/enums/apiEnum'; - - const platformList: { name: string; value: RequestImportFormat.MeterSphere | RequestImportFormat.Jmeter }[] = [ - { - name: 'MeterSphere', - value: RequestImportFormat.MeterSphere, - }, - { - name: 'Jmeter', - value: RequestImportFormat.Jmeter, - }, - ]; const appStore = useAppStore(); const { t } = useI18n(); @@ -84,13 +61,9 @@ const exportLoading = ref(false); const exportTypeRadio = ref(false); - const exportPlatform = ref(RequestImportFormat.MeterSphere); function cancelExport() { visible.value = false; } - function setActiveImportFormat(format: RequestImportFormat.MeterSphere | RequestImportFormat.Jmeter) { - exportPlatform.value = format; - } const websocket = ref(); const reportId = ref(''); const isShowExportingMessage = ref(false); // 正在导出提示显示中 @@ -217,7 +190,7 @@ sort: props.sorter || {}, fileId: reportId.value, }, - exportPlatform.value + 'METERSPHERE' ); showExportingMessage(res); visible.value = false;