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 06a1e15793..4316c1055a 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 @@ -111,10 +111,17 @@ public class ApiScenarioController { apiScenarioService.deleteToGc(id); } + @GetMapping("/get/{scenarioId}") + @Operation(summary = "接口测试-接口场景管理-获取场景详情") + @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_READ) + public ApiScenarioDetail get(@PathVariable String scenarioId) { + return apiScenarioService.get(scenarioId); + } + @PostMapping("/debug") @Operation(summary = "接口测试-接口场景管理-场景调试") @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_EXECUTE) - public String debug(@RequestBody ApiScenarioDebugRequest request) { + public String debug(@Validated @RequestBody ApiScenarioDebugRequest request) { return apiScenarioService.debug(request); } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioAddRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioAddRequest.java index 6adbf71a49..931ba2123f 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioAddRequest.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioAddRequest.java @@ -3,6 +3,7 @@ package io.metersphere.api.dto.scenario; import io.metersphere.api.constants.ApiScenarioStatus; import io.metersphere.system.valid.EnumValue; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; @@ -57,6 +58,7 @@ public class ApiScenarioAddRequest { private ScenarioConfig scenarioConfig; @Schema(description = "步骤集合") + @Valid private List steps; /** diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDebugRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDebugRequest.java index a794dcd5df..ae1f262c6b 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDebugRequest.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDebugRequest.java @@ -1,6 +1,7 @@ package io.metersphere.api.dto.scenario; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; @@ -29,6 +30,7 @@ public class ApiScenarioDebugRequest { @Schema(description = "场景的通用配置") private ScenarioConfig scenarioConfig; + @Valid @Schema(description = "步骤集合") private List steps; diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDetail.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDetail.java index 4cab52611e..e11e64b927 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDetail.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioDetail.java @@ -5,9 +5,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + @Data @EqualsAndHashCode(callSuper = false) public class ApiScenarioDetail extends ApiScenario { @Schema(description = "场景的通用配置") private ScenarioConfig scenarioConfig; + @Schema(description = "步骤") + private List steps; } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepCommonDTO.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepCommonDTO.java new file mode 100644 index 0000000000..2684a4953b --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepCommonDTO.java @@ -0,0 +1,57 @@ +package io.metersphere.api.dto.scenario; + +import io.metersphere.api.constants.ApiScenarioStepRefType; +import io.metersphere.api.constants.ApiScenarioStepType; +import io.metersphere.system.valid.EnumValue; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +/** + * 步骤解析使用的核心数据 + * 用于步骤解析的统一处理 + * @Author: jianxing + * @CreateTime: 2024-01-10 11:24 + */ +@Data +public class ApiScenarioStepCommonDTO { + @Schema(description = "步骤id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{api_scenario_step.id.not_blank}") + @Size(max = 50, message = "{api_scenario_step.id.length_range}") + private String id; + + @Schema(description = "启用/禁用") + private Boolean enable = true; + + @Schema(description = "资源id") + private String resourceId; + + /** + * @see ApiScenarioStepType + */ + @Schema(description = "步骤类型/API/CASE等") + @NotBlank + @EnumValue(enumClass = ApiScenarioStepType.class) + private String stepType; + + /** + * 引用模式:默认完全引用 + * - 完全引用:步骤状态不可调整 + * - 部分引用:步骤状态可调整 + * @see ApiScenarioStepRefType + */ + @Schema(description = "引用/复制/自定义") + @EnumValue(enumClass = ApiScenarioStepRefType.class) + private String refType; + + @Schema(description = "循环等组件基础数据") + private Object config; + + @Valid + @Schema(description = "子步骤") + private List children; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepDTO.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepDTO.java new file mode 100644 index 0000000000..5ee1cb92b7 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepDTO.java @@ -0,0 +1,33 @@ +package io.metersphere.api.dto.scenario; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: jianxing + * @CreateTime: 2024-01-10 11:24 + */ +@Data +public class ApiScenarioStepDTO extends ApiScenarioStepCommonDTO { + + @Schema(description = "步骤名称") + private String name; + + @Schema(description = "资源编号") + private String resourceNum; + + @Schema(description = "项目fk") + private String projectId; + + @Schema(description = "版本号") + private String versionId; + + @Schema(description = "场景id") + private String scenarioId; + + @Schema(description = "序号") + private Long sort; + + @Schema(description = "父级fk") + private String parentId; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepRequest.java index 8a3464bfc4..c30e7e5d21 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepRequest.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioStepRequest.java @@ -1,64 +1,25 @@ package io.metersphere.api.dto.scenario; -import io.metersphere.api.constants.ApiScenarioStepType; -import io.metersphere.system.valid.EnumValue; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; import lombok.Data; -import java.util.List; -import java.util.Map; - /** * @Author: jianxing * @CreateTime: 2024-01-10 11:24 */ @Data -public class ApiScenarioStepRequest { - @Schema(description = "步骤id", requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank(message = "{api_scenario_step.id.not_blank}") - @Size(max = 50, message = "{api_scenario_step.id.length_range}") - private String id; - +public class ApiScenarioStepRequest extends ApiScenarioStepCommonDTO { @Schema(description = "步骤名称") + @NotBlank private String name; - @Schema(description = "启用/禁用") - private Boolean enable = true; - - @Schema(description = "资源id") - private String resourceId; - @Schema(description = "资源编号") private String resourceNum; - /** - * @see ApiScenarioStepType - */ - @Schema(description = "步骤类型/API/CASE等") - @NotBlank - @EnumValue(enumClass = ApiScenarioStepType.class) - private String stepType; - @Schema(description = "项目fk") private String projectId; @Schema(description = "版本号") private String versionId; - - /** - * 引用模式:默认完全引用 - * - 完全引用:步骤状态不可调整 - * - 部分引用:步骤状态可调整 - * @see io.metersphere.api.constants.ApiScenarioStepRefType - */ - @Schema(description = "引用/复制/自定义") - private String refType; - - @Schema(description = "循环等组件基础数据") - private Map config; - - @Schema(description = "子步骤") - private List children; } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioUpdateRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioUpdateRequest.java index 4a897f115f..6c8c071981 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioUpdateRequest.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioUpdateRequest.java @@ -5,6 +5,7 @@ import io.metersphere.system.valid.EnumValue; import io.metersphere.validation.groups.Created; import io.metersphere.validation.groups.Updated; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; @@ -54,6 +55,7 @@ public class ApiScenarioUpdateRequest { @Schema(description = "场景的通用配置") private ScenarioConfig scenarioConfig; + @Valid @Schema(description = "步骤集合") private List steps; 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 a5b9ec7af2..210521f51d 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,5 +1,8 @@ package io.metersphere.api.mapper; +import io.metersphere.api.dto.scenario.ApiScenarioStepDTO; +import org.apache.ibatis.annotations.Param; + import java.util.List; /** @@ -7,5 +10,7 @@ import java.util.List; * @CreateTime: 2024-01-16 19:57 */ public interface ExtApiScenarioStepMapper { - List getStepIdsByScenarioId(String scenarioId); + List getStepIdsByScenarioId(@Param("scenarioId") String scenarioId); + + List getStepDTOByScenarioIds(@Param("scenarioIds") List scenarioIds); } 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 fbe9d45ddc..cf364b11eb 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 @@ -5,4 +5,12 @@ + \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiCaseStepParser.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiCaseStepParser.java index 7b7c8ac6f0..69f81d98e1 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiCaseStepParser.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiCaseStepParser.java @@ -1,6 +1,6 @@ package io.metersphere.api.parser.step; -import io.metersphere.api.dto.scenario.ApiScenarioStepRequest; +import io.metersphere.api.dto.scenario.ApiScenarioStepCommonDTO; import io.metersphere.api.utils.ApiDataUtils; import io.metersphere.plugin.api.spi.AbstractMsTestElement; import org.apache.commons.lang3.StringUtils; @@ -11,7 +11,7 @@ import org.apache.commons.lang3.StringUtils; */ public class ApiCaseStepParser extends StepParser { @Override - public AbstractMsTestElement parse(ApiScenarioStepRequest step, String resourceBlob, String stepDetail) { + public AbstractMsTestElement parse(ApiScenarioStepCommonDTO step, String resourceBlob, String stepDetail) { if (isRef(step.getRefType())) { return StringUtils.isBlank(resourceBlob) ? null : parse2MsTestElement(resourceBlob); } else { diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiScenarioStepParser.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiScenarioStepParser.java index ccb68556ad..f89b25385d 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiScenarioStepParser.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiScenarioStepParser.java @@ -1,7 +1,7 @@ package io.metersphere.api.parser.step; import io.metersphere.api.dto.request.MsScenario; -import io.metersphere.api.dto.scenario.ApiScenarioStepRequest; +import io.metersphere.api.dto.scenario.ApiScenarioStepCommonDTO; import io.metersphere.api.dto.scenario.ScenarioConfig; import io.metersphere.plugin.api.spi.AbstractMsTestElement; import io.metersphere.sdk.util.JSON; @@ -13,7 +13,7 @@ import org.apache.commons.lang3.StringUtils; */ public class ApiScenarioStepParser extends StepParser { @Override - public AbstractMsTestElement parse(ApiScenarioStepRequest step, String resourceBlob, String stepDetail) { + public AbstractMsTestElement parse(ApiScenarioStepCommonDTO step, String resourceBlob, String stepDetail) { MsScenario msScenario = new MsScenario(); if (isRef(step.getRefType())) { if (StringUtils.isNotBlank(resourceBlob)) { diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiStepParser.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiStepParser.java index c9b46f825e..014f7d9f9f 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiStepParser.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/ApiStepParser.java @@ -3,7 +3,7 @@ package io.metersphere.api.parser.step; import io.metersphere.api.dto.request.http.KeyValueParam; import io.metersphere.api.dto.request.http.MsHTTPElement; import io.metersphere.api.dto.request.http.body.Body; -import io.metersphere.api.dto.scenario.ApiScenarioStepRequest; +import io.metersphere.api.dto.scenario.ApiScenarioStepCommonDTO; import io.metersphere.api.utils.ApiDataUtils; import io.metersphere.plugin.api.spi.AbstractMsTestElement; import org.apache.commons.collections.CollectionUtils; @@ -17,7 +17,7 @@ import java.util.List; */ public class ApiStepParser extends StepParser { @Override - public AbstractMsTestElement parse(ApiScenarioStepRequest step, String resourceBlob, String stepDetail) { + public AbstractMsTestElement parse(ApiScenarioStepCommonDTO step, String resourceBlob, String stepDetail) { if (isRef(step.getRefType())) { if (StringUtils.isBlank(resourceBlob)) { return null; diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/StepParser.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/StepParser.java index 89868e6967..04cb87c80f 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/StepParser.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/step/StepParser.java @@ -1,6 +1,7 @@ package io.metersphere.api.parser.step; import io.metersphere.api.constants.ApiScenarioStepRefType; +import io.metersphere.api.dto.scenario.ApiScenarioStepCommonDTO; import io.metersphere.api.dto.scenario.ApiScenarioStepRequest; import io.metersphere.api.utils.ApiDataUtils; import io.metersphere.plugin.api.spi.AbstractMsTestElement; @@ -12,7 +13,7 @@ import org.apache.commons.lang3.StringUtils; */ public abstract class StepParser { - public abstract AbstractMsTestElement parse(ApiScenarioStepRequest step, String resourceBlob, String stepDetail); + public abstract AbstractMsTestElement parse(ApiScenarioStepCommonDTO step, String resourceBlob, String stepDetail); protected boolean isRef(String refType) { return StringUtils.equalsAny(refType, ApiScenarioStepRefType.REF.name(), ApiScenarioStepRefType.PARTIAL_REF.name()); 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 82c65a7ad9..0cc7f95117 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 @@ -479,7 +479,7 @@ public class ApiScenarioService { if (step == null) { return; } - if (!isRef(step.getRefType()) || StringUtils.equals(step.getRefType(), ApiScenarioStepType.API.name())) { + if (!isRef(step.getRefType()) || isRefApi(step.getStepType(), step.getRefType())) { // 非引用的步骤,如果有编辑内容,保存到blob表 // 如果引用的是接口定义,也保存详情,因为应用接口定义允许修改参数值 ApiScenarioStepBlob apiScenarioStepBlob = new ApiScenarioStepBlob(); @@ -503,15 +503,14 @@ public class ApiScenarioService { * @param steps * @return */ - private List getApiScenarioSteps(ApiScenarioStepRequest parent, - List steps) { - + private List getApiScenarioSteps(ApiScenarioStepCommonDTO parent, + List steps) { if (CollectionUtils.isEmpty(steps)) { return Collections.emptyList(); } List apiScenarioSteps = new ArrayList<>(); long sort = 1; - for (ApiScenarioStepRequest step : steps) { + for (ApiScenarioStepCommonDTO step : steps) { ApiScenarioStep apiScenarioStep = new ApiScenarioStep(); BeanUtils.copyBean(apiScenarioStep, step); apiScenarioStep.setSort(sort++); @@ -540,13 +539,13 @@ public class ApiScenarioService { * @param steps * @return */ - private List getPartialRefStepDetails(List steps) { + private List getPartialRefStepDetails(List steps) { if (CollectionUtils.isEmpty(steps)) { return Collections.emptyList(); } List apiScenarioStepsDetails = new ArrayList<>(); - for (ApiScenarioStepRequest step : steps) { + for (ApiScenarioStepCommonDTO step : steps) { if (StringUtils.equals(step.getRefType(), ApiScenarioStepRefType.REF.name())) { // 引用的步骤不解析子步骤 continue; @@ -572,12 +571,12 @@ public class ApiScenarioService { * @param steps * @return */ - private Set getEnableStepSet(List steps) { + private Set getEnableStepSet(List steps) { Set enableSteps = new HashSet<>(); if (CollectionUtils.isEmpty(steps)) { return Collections.emptySet(); } - for (ApiScenarioStepRequest step : steps) { + for (ApiScenarioStepCommonDTO step : steps) { if (BooleanUtils.isTrue(step.getEnable())) { enableSteps.add(step.getId()); } @@ -708,20 +707,19 @@ public class ApiScenarioService { * @param stepDetailMap */ private void parseStep2MsElement(AbstractMsTestElement parentElement, - List steps, + List steps, Map resourceBlobMap, Map stepDetailMap) { if (CollectionUtils.isNotEmpty(steps)) { parentElement.setChildren(new LinkedList<>()); } - for (ApiScenarioStepRequest step : steps) { + for (ApiScenarioStepCommonDTO step : steps) { StepParser stepParser = StepParserFactory.getStepParser(step.getStepType()); if (stepParser == null || BooleanUtils.isFalse(step.getEnable())) { continue; } - if (StringUtils.equals(step.getStepType(), ApiScenarioStepRefType.PARTIAL_REF.name())) { - setPartialRefStepEnable(step, stepDetailMap); - } + setPartialRefStepEnable(step, stepDetailMap); + // 将步骤详情解析生成对应的MsTestElement AbstractMsTestElement msTestElement = stepParser.parse(step, resourceBlobMap.get(step.getResourceId()), stepDetailMap.get(step.getId())); if (msTestElement != null) { @@ -734,18 +732,18 @@ public class ApiScenarioService { } /** - * 设置部分引用的步骤的启用状态 + * 设置单个部分引用的步骤的启用状态 * * @param step * @param stepDetailMap */ - private void setPartialRefStepEnable(ApiScenarioStepRequest step, Map stepDetailMap) { + private void setPartialRefStepEnable(ApiScenarioStepCommonDTO step, Map stepDetailMap) { String stepDetail = stepDetailMap.get(step.getId()); - if (StringUtils.isBlank(stepDetail)) { - return; + if (!isPartialRef(step) || StringUtils.isBlank(stepDetail)) { + return; } PartialRefStepDetail partialRefStepDetail = JSON.parseObject(stepDetail, PartialRefStepDetail.class); - setChildPartialRefEnable(step.getChildren(), partialRefStepDetail.getEnableStepIds(), stepDetailMap); + setChildPartialRefEnable(step.getChildren(), partialRefStepDetail.getEnableStepIds()); } /** @@ -753,28 +751,31 @@ public class ApiScenarioService { * * @param steps * @param enableStepIds - * @param stepDetailMap */ - private void setChildPartialRefEnable(List steps, Set enableStepIds, Map stepDetailMap) { - for (ApiScenarioStepRequest step : steps) { + private void setChildPartialRefEnable(List steps, Set enableStepIds) { + for (ApiScenarioStepCommonDTO step : steps) { if (StringUtils.equals(step.getRefType(), ApiScenarioStepRefType.REF.name())) { // 引用的启用不修改 continue; } // 非完全引用的步骤,使用当前场景配置的启用状态 step.setEnable(enableStepIds.contains(step.getId())); - if (StringUtils.equals(step.getRefType(), ApiScenarioStepRefType.PARTIAL_REF.name())) { - // 如果是部分引用的场景,重新获取详情再解析 - setPartialRefStepEnable(step, stepDetailMap); + if (isPartialRef(step)) { + // 如果是部分引用的场景,不递归解析了,上层的递归会解析 continue; } // 非完全引用和部分引用的步骤,递归设置子步骤 if (CollectionUtils.isNotEmpty(step.getChildren())) { - setChildPartialRefEnable(step.getChildren(), enableStepIds, stepDetailMap); + setChildPartialRefEnable(step.getChildren(), enableStepIds); } } } + private boolean isPartialRef(ApiScenarioStepCommonDTO step) { + return StringUtils.equals(step.getStepType(), ApiScenarioStepType.API_SCENARIO.name()) && + StringUtils.equals(step.getRefType(), ApiScenarioStepRefType.PARTIAL_REF.name()); + } + private Map getStepDetailMap(List steps, Map stepDetailsParam) { List needBlobStepIds = new ArrayList<>(); for (ApiScenarioStepRequest step : steps) { @@ -836,8 +837,8 @@ public class ApiScenarioService { return apiScenarioBlobMapper.selectByExampleWithBLOBs(example); } - private void buildRefResourceIdMap(List steps, Map> refResourceIdMap) { - for (ApiScenarioStepRequest step : steps) { + private void buildRefResourceIdMap(List steps, Map> refResourceIdMap) { + for (ApiScenarioStepCommonDTO step : steps) { if (isRef(step.getRefType()) && BooleanUtils.isTrue(step.getEnable())) { // 记录引用的步骤ID List resourceIds = refResourceIdMap.get(step.getStepType()); @@ -888,4 +889,165 @@ public class ApiScenarioService { update.setUpdateTime(System.currentTimeMillis()); apiScenarioMapper.updateByPrimaryKeySelective(update); } + + public ApiScenarioDetail get(String scenarioId) { + checkResourceExist(scenarioId); + ApiScenario apiScenario = apiScenarioMapper.selectByPrimaryKey(scenarioId); + ApiScenarioDetail apiScenarioDetail = BeanUtils.copyBean(new ApiScenarioDetail(), apiScenario); + ApiScenarioBlob apiScenarioBlob = apiScenarioBlobMapper.selectByPrimaryKey(scenarioId); + if (apiScenarioBlob != null) { + apiScenarioDetail.setScenarioConfig(JSON.parseObject(new String(apiScenarioBlob.getConfig()), ScenarioConfig.class)); + } + + // 获取所有步骤 + List allSteps = getAllStepsByScenarioIds(List.of(scenarioId)); + // 构造 map,key 为场景ID,value 为步骤列表 + Map> scenarioStepMap = allSteps.stream() + .collect(Collectors.groupingBy(step -> Optional.ofNullable(step.getScenarioId()).orElse(StringUtils.EMPTY))); + + // 查询部分引用的步骤详情和接口定义的步骤详情 + Map stepDetailMap = getStepDetailMap(allSteps); + + // key 为父步骤ID,value 为子步骤列表 + Map> parentStepMap = scenarioStepMap.get(scenarioId) + .stream() + .collect(Collectors.groupingBy(step -> Optional.ofNullable(step.getParentId()).orElse(StringUtils.EMPTY))); + List steps = buildStepTree(parentStepMap.get(StringUtils.EMPTY), scenarioStepMap, scenarioStepMap); + // 设置部分引用的步骤的启用状态 + setPartialRefStepsEnable(steps, stepDetailMap); + + apiScenarioDetail.setSteps(steps); + + return apiScenarioDetail; + } + + /** + * 设置部分引用的步骤的启用状态 + * + * @param steps + * @param stepDetailMap + */ + private void setPartialRefStepsEnable(List steps, Map stepDetailMap) { + if (CollectionUtils.isNotEmpty(steps)) { + return; + } + for (ApiScenarioStepCommonDTO step : steps) { + setPartialRefStepEnable(step, stepDetailMap); + if (CollectionUtils.isNotEmpty(step.getChildren())) { + setPartialRefStepsEnable(step.getChildren(), stepDetailMap); + } + } + } + + /** + * 查询部分引用的步骤详情 和 接口定义的步骤详情 + * + * @param allSteps + */ + private Map getStepDetailMap(List allSteps) { + List stepDetailIds = allSteps.stream().filter(step -> isRefApi(step.getStepType(), step.getRefType()) + || StringUtils.equals(step.getRefType(), ApiScenarioStepRefType.PARTIAL_REF.name())) + .map(ApiScenarioStepDTO::getId) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(stepDetailIds)) { + return new HashMap<>(0); + } + ApiScenarioBlobExample example = new ApiScenarioBlobExample(); + example.createCriteria().andIdIn(stepDetailIds); + return apiScenarioBlobMapper.selectByExample(example).stream() + .collect(Collectors.toMap(ApiScenarioBlob::getId, blob -> new String(blob.getConfig()))); + } + + /** + * 判断步骤是否是引用的接口定义 + * 引用的接口定义允许修改参数值,需要特殊处理 + * + * @param stepType + * @param refType + * @return + */ + private boolean isRefApi(String stepType, String refType) { + return StringUtils.equals(stepType, ApiScenarioStepType.API.name()) && StringUtils.equals(refType, ApiScenarioStepRefType.REF.name()); + } + + /** + * 递归构造步骤树 + * + * @param steps 当前场景下,当前层级的步骤 + * @param parentStepMap 当前场景所有的步骤,key 为父步骤ID,value 为子步骤列表 + * @param scenarioStepMap 所有场景步骤,key 为场景ID,value 为子步骤列表 + */ + private List buildStepTree(List steps, + Map> parentStepMap, + Map> scenarioStepMap) { + if (CollectionUtils.isEmpty(steps)) { + return Collections.emptyList(); + } + steps.forEach(step -> { + // 获取当前步骤的子步骤 + List children = parentStepMap.get(step.getId()); + if (CollectionUtils.isEmpty(children)) { + return; + } + if (isRefApiScenario(step)) { + // 如果当前步骤是引用的场景,获取该场景的子步骤 + Map> childStepMap = scenarioStepMap.get(step.getResourceId()) + .stream() + .collect(Collectors.groupingBy(ApiScenarioStepDTO::getParentId)); + step.setChildren(buildStepTree(children, childStepMap, scenarioStepMap)); + } else { + step.setChildren(buildStepTree(children, parentStepMap, scenarioStepMap)); + } + }); + // 排序 + return steps.stream() + .sorted(Comparator.comparing(ApiScenarioStepDTO::getSort)) + .collect(Collectors.toList()); + } + + /** + * 判断步骤是否是引用的场景 + * @param step + * @return + */ + private boolean isRefApiScenario(ApiScenarioStepDTO step) { + return isRef(step.getRefType()) && StringUtils.equals(step.getStepType(), ApiScenarioStepType.API_SCENARIO.name()); + } + + /** + * 递归获取所有的场景步骤 + * + * @param scenarioIds + * @return + */ + private List getAllStepsByScenarioIds(List scenarioIds) { + List steps = getStepDTOByScenarioIds(scenarioIds); + if (CollectionUtils.isEmpty(steps)) { + return steps; + } + + // 将 config 转换成对象 + steps.stream().forEach(step -> { + if (step.getConfig() != null && StringUtils.isNotBlank(step.getConfig().toString())) { + step.setConfig(JSON.parseObject(step.getConfig().toString())); + } + }); + + // 获取步骤中引用的场景ID + List childScenarioIds = steps.stream() + .filter(step -> isRefApiScenario(step)) + .map(ApiScenarioStepDTO::getResourceId) + .collect(Collectors.toList()); + + // 嵌套获取引用的场景步骤 + steps.addAll(getAllStepsByScenarioIds(childScenarioIds)); + return steps; + } + + private List getStepDTOByScenarioIds(List scenarioIds) { + if (CollectionUtils.isEmpty(scenarioIds)) { + return Collections.emptyList(); + } + return extApiScenarioStepMapper.getStepDTOByScenarioIds(scenarioIds); + } } \ 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 6e53f17a33..3037a7fffc 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 @@ -94,7 +94,9 @@ public class ApiScenarioControllerTests extends BaseTest { @Resource private BaseResourcePoolTestService baseResourcePoolTestService; private static ApiScenario addApiScenario; + private static List addApiScenarioSteps; private static ApiScenario anOtherAddApiScenario; + private static List anOtherAddApiScenarioSteps; private static ApiDefinition apiDefinition; private static ApiTestCase apiTestCase; @@ -229,6 +231,7 @@ public class ApiScenarioControllerTests extends BaseTest { stepRequest.setName(addApiScenario.getName()); stepRequest.setResourceId(addApiScenario.getId()); stepRequest.setRefType(ApiScenarioStepRefType.REF.name()); + stepRequest.setStepType(ApiScenarioStepType.API_SCENARIO.name()); ApiScenarioStepRequest stepRequest2 = new ApiScenarioStepRequest(); stepRequest2.setId(IDGenerator.nextStr()); @@ -236,12 +239,14 @@ public class ApiScenarioControllerTests extends BaseTest { stepRequest2.setResourceId(addApiScenario.getId()); stepRequest2.setRefType(ApiScenarioStepRefType.PARTIAL_REF.name()); stepRequest2.setChildren(List.of(stepRequest)); + stepRequest2.setStepType(ApiScenarioStepType.API_SCENARIO.name()); steps = List.of(stepRequest, stepRequest2); request.setSteps(steps); mvcResult = this.requestPostWithOkAndReturn(DEFAULT_ADD, request); this.anOtherAddApiScenario = apiScenarioMapper.selectByPrimaryKey(getResultData(mvcResult, ApiScenario.class).getId()); assertUpdateApiScenario(request, request.getScenarioConfig(), anOtherAddApiScenario.getId()); assertUpdateSteps(steps, steptDetailMap); + anOtherAddApiScenarioSteps = steps; // @@重名校验异常 assertErrorCode(this.requestPost(DEFAULT_ADD, request), API_SCENARIO_EXIST); @@ -314,6 +319,8 @@ public class ApiScenarioControllerTests extends BaseTest { stepRequest.setName(apiTestCase.getName()); stepRequest.setResourceId(apiTestCase.getId()); stepRequest.setRefType(ApiScenarioStepRefType.REF.name()); + stepRequest.setStepType(ApiScenarioStepType.API_CASE.name()); + stepRequest.setProjectId(DEFAULT_PROJECT_ID); stepRequest.setConfig(new HashMap<>()); ApiScenarioStepRequest stepRequest2 = new ApiScenarioStepRequest(); @@ -322,17 +329,22 @@ public class ApiScenarioControllerTests extends BaseTest { stepRequest2.setConfig(new HashMap<>()); stepRequest2.setEnable(true); stepRequest2.setResourceId(apiTestCase.getId()); - stepRequest.setName(apiTestCase.getName() + "2"); + stepRequest2.setName(apiTestCase.getName() + "2"); + stepRequest2.setStepType(ApiScenarioStepType.API_CASE.name()); stepRequest2.setRefType(ApiScenarioStepRefType.COPY.name()); + stepRequest2.setProjectId(DEFAULT_PROJECT_ID); ApiScenarioStepRequest stepRequest3 = new ApiScenarioStepRequest(); stepRequest3.setId(IDGenerator.nextStr()); stepRequest3.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(DEFAULT_PROJECT_ID)); stepRequest3.setConfig(new HashMap<>()); stepRequest3.setEnable(true); + stepRequest3.setStepType(ApiScenarioStepType.API.name()); stepRequest3.setResourceId(apiDefinition.getId()); - stepRequest.setName(apiDefinition.getName() + "3"); + stepRequest3.setName(apiDefinition.getName() + "3"); stepRequest3.setRefType(ApiScenarioStepRefType.REF.name()); + stepRequest3.setProjectId(DEFAULT_PROJECT_ID); + return new ArrayList<>() {{ add(stepRequest); add(stepRequest2); @@ -409,6 +421,7 @@ public class ApiScenarioControllerTests extends BaseTest { steps.get(0).setName("test name update"); this.requestPostWithOk(DEFAULT_UPDATE, request); assertUpdateSteps(steps, steptDetailMap); + addApiScenarioSteps = steps; // @@重名校验异常 request.setName(anOtherAddApiScenario.getName()); @@ -475,6 +488,37 @@ public class ApiScenarioControllerTests extends BaseTest { requestPostPermissionTest(PermissionConstants.PROJECT_API_SCENARIO_EXECUTE, DEBUG, request); } + @Test + @Order(7) + public void get() throws Exception { + MvcResult mvcResult = this.requestGetWithOkAndReturn(DEFAULT_GET, addApiScenario.getId()); + ApiScenarioDetail apiScenarioDetail = getResultData(mvcResult, ApiScenarioDetail.class); + // 验证数据 + asserGetApiScenarioSteps(this.addApiScenarioSteps, apiScenarioDetail.getSteps()); + + mvcResult = this.requestGetWithOkAndReturn(DEFAULT_GET, anOtherAddApiScenario.getId()); + apiScenarioDetail = getResultData(mvcResult, ApiScenarioDetail.class); + // 验证数据 + Assertions.assertEquals(this.anOtherAddApiScenarioSteps.size(), apiScenarioDetail.getSteps().size()); + // @@校验权限 + requestGetPermissionTest(PermissionConstants.PROJECT_API_SCENARIO_READ, DEFAULT_GET, addApiScenario.getId()); + } + + private void asserGetApiScenarioSteps(List addApiScenarioSteps, List steps) { + if (addApiScenarioSteps == null || steps == null) { + Assertions.assertEquals(addApiScenarioSteps, null); + Assertions.assertEquals(steps, null); + return; + } + Assertions.assertEquals(addApiScenarioSteps.size(), steps.size()); + for (int i = 0; i < addApiScenarioSteps.size(); i++) { + ApiScenarioStepRequest stepRequest = (ApiScenarioStepRequest) addApiScenarioSteps.get(i); + ApiScenarioStepDTO stepDTO = (ApiScenarioStepDTO) steps.get(i); + Assertions.assertEquals(BeanUtils.copyBean(new ApiScenarioStepCommonDTO(), stepRequest), BeanUtils.copyBean(new ApiScenarioStepCommonDTO(), stepDTO)); + asserGetApiScenarioSteps(stepRequest.getChildren(), stepDTO.getChildren()); + } + } + private String doUploadTempFile(MockMultipartFile file) throws Exception { return JSON.parseObject(requestUploadFileWithOkAndReturn(UPLOAD_TEMP_FILE, file) .getResponse() @@ -497,7 +541,8 @@ public class ApiScenarioControllerTests extends BaseTest { public void deleteToGc() throws Exception { // @@请求成功 this.requestGetWithOk(DELETE_TO_GC, addApiScenario.getId()); - // todo 校验请求成功数据 + ApiScenario apiScenario = apiScenarioMapper.selectByPrimaryKey(addApiScenario.getId()); + Assertions.assertTrue(apiScenario.getDeleted()); // @@校验日志 checkLog(addApiScenario.getId(), OperationLogType.DELETE); // @@校验权限 @@ -509,7 +554,10 @@ public class ApiScenarioControllerTests extends BaseTest { public void delete() throws Exception { // @@请求成功 this.requestGetWithOk(DEFAULT_DELETE, addApiScenario.getId()); - // todo 校验请求成功数据 + ApiScenario apiScenario = apiScenarioMapper.selectByPrimaryKey(addApiScenario.getId()); + ApiScenarioBlob apiScenarioBlob = apiScenarioBlobMapper.selectByPrimaryKey(addApiScenario.getId()); + Assertions.assertNull(apiScenario); + Assertions.assertNull(apiScenarioBlob); // @@校验日志 checkLog(addApiScenario.getId(), OperationLogType.DELETE); // @@校验权限