feat(接口测试): 用例同步相关接口

--task=1015860 --user=陈建星 【接口测试】接口用例支持同步更新接口变更-后端-批量同步更新接口 https://www.tapd.cn/55049933/s/1559954
This commit is contained in:
AgAngle 2024-08-08 11:54:09 +08:00 committed by jianxing
parent 37ae81423c
commit a9da9a7a59
10 changed files with 843 additions and 44 deletions

View File

@ -11,6 +11,7 @@ import io.metersphere.api.dto.definition.*;
import io.metersphere.api.dto.request.ApiTransferRequest;
import io.metersphere.api.service.ApiFileResourceService;
import io.metersphere.api.service.definition.*;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.project.service.FileModuleService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.PermissionConstants;
@ -326,6 +327,14 @@ public class ApiTestCaseController {
apiTestCaseService.ignoreApiChange(id, ignore);
}
@PostMapping("/api-change/sync")
@Operation(summary = "获取同步后的用例详情")
@RequiresPermissions(value = PermissionConstants.PROJECT_API_DEFINITION_CASE_UPDATE)
@CheckOwner(resourceId = "#request.getId()", resourceType = "api_test_case")
public AbstractMsTestElement syncApiChange(@Validated @RequestBody ApiCaseSyncRequest request) {
return apiTestCaseService.syncApiChange(request);
}
@GetMapping("/api/compare/{id}")
@Operation(summary = "与接口定义对比")
@RequiresPermissions(value = PermissionConstants.PROJECT_API_DEFINITION_CASE_READ)

View File

@ -2,12 +2,10 @@ package io.metersphere.api.dto.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
public class ApiCaseBatchSyncRequest extends ApiTestCaseBatchRequest implements Serializable {
private static final long serialVersionUID = 1L;
@ -15,22 +13,12 @@ public class ApiCaseBatchSyncRequest extends ApiTestCaseBatchRequest implements
@Schema(description = "同步项")
private ApiCaseSyncItemRequest syncItems = new ApiCaseSyncItemRequest();
@Schema(description = "是否删除多余参数", defaultValue = "false")
private boolean deleteRedundantParam = false;
private Boolean deleteRedundantParam = false;
@Schema(description = "通知配置")
private ApiCaseSyncNotificationRequest notificationConfig = new ApiCaseSyncNotificationRequest();
public class ApiCaseSyncItemRequest {
@Schema(description = "请求头", defaultValue = "true")
private Boolean header = true;
@Schema(description = "请求体", defaultValue = "true")
private Boolean body = true;
@Schema(description = "Query参数", defaultValue = "true")
private Boolean query = true;
@Schema(description = "Rest参数", defaultValue = "true")
private Boolean rest = true;
}
public class ApiCaseSyncNotificationRequest {
@Data
public static class ApiCaseSyncNotificationRequest {
@Schema(description = "是否通知接口创建人", defaultValue = "true")
private Boolean apiCreator = true;
@Schema(description = "是否通知引用该用例的场景创建人", defaultValue = "true")

View File

@ -0,0 +1,25 @@
package io.metersphere.api.dto.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* @Author: jianxing
* @CreateTime: 2024-08-08 11:06
*/
@Data
public class ApiCaseSyncItemRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "请求头", defaultValue = "true")
private Boolean header = true;
@Schema(description = "请求体", defaultValue = "true")
private Boolean body = true;
@Schema(description = "Query参数", defaultValue = "true")
private Boolean query = true;
@Schema(description = "Rest参数", defaultValue = "true")
private Boolean rest = true;
}

View File

@ -0,0 +1,25 @@
package io.metersphere.api.dto.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class ApiCaseSyncRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "同步项")
private ApiCaseSyncItemRequest syncItems = new ApiCaseSyncItemRequest();
@Schema(description = "是否删除多余参数", defaultValue = "false")
private Boolean deleteRedundantParam = false;
@Schema(description = "用例的请求详情")
@NotNull
private Object apiCaseRequest;
@Schema(description = "用例ID")
@NotBlank
private String id;
}

View File

@ -111,4 +111,6 @@ public interface ExtApiTestCaseMapper {
List<ApiTestCase> getCaseListBySelectIds(@Param("isRepeat") boolean isRepeat, @Param("projectId") String projectId, @Param("ids") List<String> ids, @Param("testPlanId") String testPlanId, @Param("protocols") List<String> protocols);
void setApiChangeByApiDefinitionId(@Param("apiDefinitionId") String apiDefinitionId);
List<ApiTestCase> getApiCaseForBatchSync(@Param("ids") List<String> ids);
}

View File

@ -727,6 +727,14 @@
)
</if>
</select>
<select id="getApiCaseForBatchSync" resultType="io.metersphere.api.domain.ApiTestCase">
select id, api_definition_id
from api_test_case
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</select>
</mapper>

View File

@ -1,5 +1,6 @@
package io.metersphere.api.service.definition;
import io.metersphere.api.constants.ApiConstants;
import io.metersphere.api.constants.ApiResourceType;
import io.metersphere.api.domain.*;
import io.metersphere.api.dto.*;
@ -462,7 +463,6 @@ public class ApiTestCaseService extends MoveNodeService {
}
public void batchEdit(ApiCaseBatchEditRequest request, String userId) {
List<String> ids = doSelectIds(request, false);
if (CollectionUtils.isEmpty(ids)) {
return;
@ -995,6 +995,56 @@ public class ApiTestCaseService extends MoveNodeService {
}
public void batchSyncApiChange(ApiCaseBatchSyncRequest request, String userId) {
// todo
// 只处理 http 协议的接口
request.setProtocols(List.of(ApiConstants.HTTP_PROTOCOL));
List<String> ids = doSelectIds(request, false);
if (CollectionUtils.isEmpty(ids)) {
return;
}
SubListUtils.dealForSubList(ids, 500, subList -> doBatchSyncApiChange(request, subList, userId));
}
public void doBatchSyncApiChange(ApiCaseBatchSyncRequest request, List<String> ids, String userId) {
List<ApiTestCase> apiTestCases = extApiTestCaseMapper.getApiCaseForBatchSync(ids);
Set<String> apiDefinitionIds = apiTestCases.stream().map(ApiTestCase::getApiDefinitionId).collect(Collectors.toSet());
Map<String, ApiTestCase> apiTestCaseMap = apiTestCases.stream().collect(Collectors.toMap(ApiTestCase::getApiDefinitionId, Function.identity()));
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
ApiTestCaseBlobMapper apiTestCaseBlobBatchMapper = sqlSession.getMapper(ApiTestCaseBlobMapper.class);
ApiTestCaseMapper apiTestCaseBatchMapper = sqlSession.getMapper(ApiTestCaseMapper.class);
ApiCaseSyncRequest apiCaseSyncRequest = new ApiCaseSyncRequest();
apiCaseSyncRequest.setSyncItems(request.getSyncItems());
apiCaseSyncRequest.setDeleteRedundantParam(request.getDeleteRedundantParam());
try {
for (String apiDefinitionId : apiDefinitionIds) {
ApiDefinitionBlob apiDefinitionBlob = apiDefinitionBlobMapper.selectByPrimaryKey(apiDefinitionId);
AbstractMsTestElement apiMsTestElement = getApiMsTestElement(apiDefinitionBlob);
ApiTestCase apiTestCase = apiTestCaseMap.get(apiDefinitionId);
ApiTestCaseBlob apiTestCaseBlob = apiTestCaseBlobMapper.selectByPrimaryKey(apiTestCase.getId());
AbstractMsTestElement apiTestCaseMsTestElement = getTestElement(apiTestCaseBlob);
boolean requestParamDifferent = HttpRequestParamDiffUtils.isRequestParamDiff(request.getSyncItems(), apiMsTestElement, apiTestCaseMsTestElement);
if (requestParamDifferent) {
apiTestCase.setUpdateTime(System.currentTimeMillis());
apiTestCase.setUpdateUser(userId);
apiTestCase.setApiChange(false);
apiTestCaseBatchMapper.updateByPrimaryKeySelective(apiTestCase);
apiTestCaseMsTestElement = HttpRequestParamDiffUtils.syncRequestDiff(apiCaseSyncRequest, apiMsTestElement, apiTestCaseMsTestElement);
apiTestCaseBlob.setRequest(ApiDataUtils.toJSONString(apiTestCaseMsTestElement).getBytes());
apiTestCaseBlobBatchMapper.updateByPrimaryKeySelective(apiTestCaseBlob);
}
}
} finally {
sqlSession.flushStatements();
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
public AbstractMsTestElement syncApiChange(ApiCaseSyncRequest request) {
ApiTestCase apiTestCase = checkResourceExist(request.getId());
ApiDefinition apiDefinition = getApiDefinition(apiTestCase.getApiDefinitionId());
ApiDefinitionBlob apiDefinitionBlob = apiDefinitionBlobMapper.selectByPrimaryKey(apiDefinition.getId());
AbstractMsTestElement apiMsTestElement = getApiMsTestElement(apiDefinitionBlob);
AbstractMsTestElement apiTestCaseMsTestElement = ApiDataUtils.parseObject(JSON.toJSONString(request.getApiCaseRequest()), AbstractMsTestElement.class);
return HttpRequestParamDiffUtils.syncRequestDiff(request, apiMsTestElement, apiTestCaseMsTestElement);
}
}

View File

@ -1,9 +1,9 @@
package io.metersphere.api.utils;
import io.metersphere.api.dto.definition.ApiCaseSyncItemRequest;
import io.metersphere.api.dto.definition.ApiCaseSyncRequest;
import io.metersphere.api.dto.request.http.MsHTTPElement;
import io.metersphere.api.dto.request.http.body.Body;
import io.metersphere.api.dto.request.http.body.JsonBody;
import io.metersphere.api.dto.request.http.body.XmlBody;
import io.metersphere.api.dto.request.http.body.*;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.project.api.KeyValueParam;
import io.metersphere.sdk.util.EnumValidator;
@ -11,16 +11,21 @@ import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.XMLUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static io.metersphere.sdk.util.XMLUtils.elementToMap;
/**
* @Author: jianxing
* @CreateTime: 2024-08-01 14:01
@ -28,6 +33,10 @@ import java.util.stream.Collectors;
public class HttpRequestParamDiffUtils {
public static boolean isRequestParamDiff(Object request1, Object request2) {
return isRequestParamDiff(new ApiCaseSyncItemRequest(), request1, request2);
}
public static boolean isRequestParamDiff(ApiCaseSyncItemRequest syncItemRequest, Object request1, Object request2) {
if (!(request1 instanceof MsHTTPElement)) {
// 其他协议不比较
return false;
@ -38,6 +47,13 @@ public class HttpRequestParamDiffUtils {
boolean isRestDiff = isParamKeyDiff(httpElement1.getRest(), httpElement2.getRest());
boolean isHeaderDiff = isParamKeyDiff(httpElement1.getHeaders(), httpElement2.getHeaders());
boolean isBodyDiff = isBodyDiff(httpElement1.getBody(), httpElement2.getBody());
// 设置需要同步的同步项, 减少比较次数
syncItemRequest.setBody(isBodyDiff && BooleanUtils.isTrue(syncItemRequest.getBody()));
syncItemRequest.setHeader(isHeaderDiff && BooleanUtils.isTrue(syncItemRequest.getHeader()));
syncItemRequest.setQuery(isQueryDiff && BooleanUtils.isTrue(syncItemRequest.getQuery()));
syncItemRequest.setRest(isRestDiff && BooleanUtils.isTrue(syncItemRequest.getRest()));
if (isQueryDiff || isRestDiff || isHeaderDiff || isBodyDiff) {
return true;
}
@ -94,6 +110,7 @@ public class HttpRequestParamDiffUtils {
/**
* 将json对象的属性值都置空
* 便于比较参数名是否一致
*
* @param obj
* @return
*/
@ -124,6 +141,7 @@ public class HttpRequestParamDiffUtils {
* 因为数值类型使用 mock 函数会导致 json 串为非法 json
* 这里使用正则表达式获取key
* 使用 LinkedHashSet 按序获取近似比较两个 json 串的 key
*
* @param jsonValue
* @return
*/
@ -148,8 +166,8 @@ public class HttpRequestParamDiffUtils {
return true;
}
try {
Set<String> keySet1 = XMLUtils.elementToMap(XMLUtils.stringToDocument(value1).getRootElement()).keySet();
Set<String> keySet2 = XMLUtils.elementToMap(XMLUtils.stringToDocument(value2).getRootElement()).keySet();
Set<String> keySet1 = elementToMap(XMLUtils.stringToDocument(value1).getRootElement()).keySet();
Set<String> keySet2 = elementToMap(XMLUtils.stringToDocument(value2).getRootElement()).keySet();
return !keySet1.equals(keySet2);
} catch (Exception e) {
return !StringUtils.equals(value1, value2);
@ -173,6 +191,7 @@ public class HttpRequestParamDiffUtils {
/**
* json xml 属性值置空
* 便于前端比较差异
*
* @param httpElement
* @return
*/
@ -192,7 +211,7 @@ public class HttpRequestParamDiffUtils {
}
}
if (StringUtils.equals(body.getBodyType(), Body.BodyType.XML.name())) {
String xml = body.getXmlBody().getValue();
String xml = Optional.ofNullable(body.getXmlBody().getValue()).orElse(StringUtils.EMPTY);
try {
Element element = XMLUtils.stringToDocument(xml).getRootElement();
XMLUtils.clearElementText(element);
@ -216,6 +235,7 @@ public class HttpRequestParamDiffUtils {
* 替换成空字符串
* {"a": ""}
* 避免 json 序列化失败
*
* @param text
* @return
*/
@ -229,4 +249,320 @@ public class HttpRequestParamDiffUtils {
}
return text;
}
/**
* 同步请求参数
* @param request
* @param sourceElement
* @param targetElement
* @return
*/
public static AbstractMsTestElement syncRequestDiff(ApiCaseSyncRequest request,
AbstractMsTestElement sourceElement, AbstractMsTestElement targetElement) {
if (!(sourceElement instanceof MsHTTPElement)) {
// 其他协议不比较
return targetElement;
}
MsHTTPElement sourceHttpElement = (MsHTTPElement) sourceElement;
MsHTTPElement targetHttpElement = (MsHTTPElement) targetElement;
boolean isDeleteRedundantParam = BooleanUtils.isTrue(request.getDeleteRedundantParam());
ApiCaseSyncItemRequest syncItems = request.getSyncItems();
if (BooleanUtils.isTrue(syncItems.getHeader())) {
targetHttpElement.setHeaders(
syncKeyValueParamDiff(isDeleteRedundantParam, sourceHttpElement.getHeaders(), targetHttpElement.getHeaders())
);
}
if (BooleanUtils.isTrue(syncItems.getRest())) {
targetHttpElement.setRest(
syncKeyValueParamDiff(isDeleteRedundantParam, sourceHttpElement.getRest(), targetHttpElement.getRest())
);
}
if (BooleanUtils.isTrue(syncItems.getQuery())) {
targetHttpElement.setQuery(
syncKeyValueParamDiff(isDeleteRedundantParam, sourceHttpElement.getQuery(), targetHttpElement.getQuery())
);
}
if (BooleanUtils.isTrue(syncItems.getBody())) {
Body sourceBody = sourceHttpElement.getBody();
Body targetBody = targetHttpElement.getBody();
targetBody = syncBodyDiff(isDeleteRedundantParam, sourceBody, targetBody);
targetHttpElement.setBody(targetBody);
}
return targetElement;
}
/**
* 同步 body 参数
* @param isDeleteRedundantParam
* @param sourceBody
* @param targetBody
* @return
*/
public static Body syncBodyDiff(boolean isDeleteRedundantParam, Body sourceBody, Body targetBody) {
if (sourceBody == null || targetBody == null || sourceBody.getBodyType() != targetBody.getBodyType()) {
return sourceBody;
}
Body.BodyType bodyType = EnumValidator.validateEnum(Body.BodyType.class, sourceBody.getBodyType());
switch (bodyType) {
case FORM_DATA -> {
List<FormDataKV> formDataKVS = syncKeyValueParamDiff(isDeleteRedundantParam, sourceBody.getFormDataBody().getFormValues(), targetBody.getFormDataBody().getFormValues());
targetBody.getFormDataBody().setFormValues(formDataKVS);
}
case WWW_FORM -> {
List<WWWFormKV> wwwFormKVS = syncKeyValueParamDiff(isDeleteRedundantParam, sourceBody.getWwwFormBody().getFormValues(), targetBody.getWwwFormBody().getFormValues());
targetBody.getWwwFormBody().setFormValues(wwwFormKVS);
}
case JSON -> {
JsonBody jsonBody = syncJsonBodyDiff(isDeleteRedundantParam, sourceBody.getJsonBody(), targetBody.getJsonBody());
targetBody.setJsonBody(jsonBody);
}
case XML -> {
XmlBody xmlBody = syncXmlBodyDiff(isDeleteRedundantParam, sourceBody.getXmlBody(), targetBody.getXmlBody());
targetBody.setXmlBody(xmlBody);
}
default -> {}
// RAW,BINARY 不同步
}
return targetBody;
}
/**
* 同步 json 参数
* @param isDeleteRedundantParam
* @param sourceBody
* @param targetBody
* @return
*/
public static JsonBody syncJsonBodyDiff(boolean isDeleteRedundantParam, JsonBody sourceBody, JsonBody targetBody) {
if (sourceBody == null) {
return isDeleteRedundantParam ? new JsonBody() : targetBody;
}
if (targetBody == null) {
return sourceBody;
}
String sourceJsonStr = sourceBody.getJsonValue();
String targetJsonStr = targetBody.getJsonValue();
if (StringUtils.isBlank(sourceJsonStr)) {
return isDeleteRedundantParam ? new JsonBody() : targetBody;
}
if (StringUtils.isBlank(targetJsonStr)) {
return sourceBody;
}
try {
Object sourceJson = JSON.parseObject(sourceJsonStr);
Object targetJson = JSON.parseObject(targetJsonStr);
targetJson = syncJsonBodyDiff(isDeleteRedundantParam, sourceJson, targetJson);
targetBody.setJsonValue(JSON.toJSONString(targetJson));
} catch (Exception e) {
LogUtils.info("同步参数 json 解析异常json1: {}, json2: {}", sourceJsonStr, targetJsonStr);
// todo 处理非法 json
}
return targetBody;
}
/**
* 同步 xml 参数
* @param isDeleteRedundantParam
* @param sourceBody
* @param targetBody
* @return
*/
public static XmlBody syncXmlBodyDiff(boolean isDeleteRedundantParam, XmlBody sourceBody, XmlBody targetBody) {
if (sourceBody == null) {
return isDeleteRedundantParam ? new XmlBody() : targetBody;
}
if (targetBody == null) {
return sourceBody;
}
String sourceXmlStr = sourceBody.getValue();
String targetXmlStr = targetBody.getValue();
if (StringUtils.isBlank(sourceXmlStr)) {
return isDeleteRedundantParam ? new XmlBody() : targetBody;
}
if (StringUtils.isBlank(targetXmlStr)) {
return sourceBody;
}
try {
Element sourceElement = XMLUtils.stringToDocument(sourceXmlStr).getRootElement();
Element targetElement = XMLUtils.stringToDocument(targetXmlStr).getRootElement();
targetElement = syncXmlBodyDiff(isDeleteRedundantParam, sourceElement, targetElement);
String string = parseElementToString(targetElement);
targetBody.setValue(string);
} catch (Exception e) {
LogUtils.info("同步参数 xml 解析异常xml1: {}, xml2: {}", sourceXmlStr, targetXmlStr);
}
return targetBody;
}
public static String parseElementToString(Element element) throws IOException {
StringWriter stringWriter = new StringWriter();
XMLWriter writer = new XMLWriter(stringWriter);
writer.write(element);
return stringWriter.toString();
}
/**
* 同步 xml 参数
* @param isDeleteRedundantParam
* @param source
* @param target
* @return
*/
public static Element syncXmlBodyDiff(boolean isDeleteRedundantParam, Element source, Element target) {
if (source == null) {
return isDeleteRedundantParam ? null : target.createCopy();
}
if (target == null) {
return source.createCopy();
}
List<Element> sourceElements = source.elements();
List<Element> targetElements = target.elements();
Map<String, Element> sourceElementMap = sourceElements.stream().collect(Collectors.toMap(Element::getName, Function.identity()));
Map<String, Element> targetElementMap = targetElements.stream().collect(Collectors.toMap(Element::getName, Function.identity()));
// 删除多余参数
if (isDeleteRedundantParam) {
Iterator<Element> iterator = targetElements.iterator();
while (iterator.hasNext()) {
Element element = iterator.next();
if (!sourceElementMap.keySet().contains(element.getName())) {
iterator.remove();
}
}
}
for (int i = 0; i < sourceElements.size(); i++) {
Element sourceElement = sourceElements.get(i);
Element targetElement = targetElementMap.get(sourceElement.getName());
if (targetElement == null) {
// 添加新增参数
target.add(sourceElement.createCopy());
} else {
int index = targetElements.indexOf(targetElement);
targetElement = syncXmlBodyDiff(isDeleteRedundantParam, sourceElement, targetElement);
targetElements.set(index, targetElement);
}
}
return target.createCopy();
}
/**
* 同步 json 参数
* @param sourceJson
* @param targetJson
* @return
*/
public static Object syncJsonBodyDiff(boolean isDeleteRedundantParam, Object sourceJson, Object targetJson) {
if (sourceJson == null) {
return isDeleteRedundantParam ? null : targetJson;
}
if (targetJson == null) {
return sourceJson;
}
if (sourceJson.getClass() != targetJson.getClass()) {
return sourceJson;
}
if (sourceJson instanceof Map sourceMap && targetJson instanceof Map targetMap) {
// 删除多余参数
if (isDeleteRedundantParam) {
Iterator iterator = targetMap.keySet().iterator();
while (iterator.hasNext()) {
String key = (String) iterator.next();
if (!sourceMap.keySet().contains(key)) {
iterator.remove();
}
}
}
sourceMap.forEach((key, sourceValue) -> {
if (!targetMap.keySet().contains(key)) {
targetMap.put(key, sourceValue);
// 添加新增参数
} else {
Object targetValue = targetMap.get(key);
targetMap.put(key, syncJsonBodyDiff(isDeleteRedundantParam, sourceValue, targetValue));
}
});
} else if (sourceJson instanceof List souceList && targetJson instanceof List targetList) {
int size = Math.min(souceList.size(), targetList.size());
for (int i = 0; i < size; i++) {
Object sourceValue = souceList.get(i);
Object targetValue = targetList.get(i);
targetList.set(i, syncJsonBodyDiff(isDeleteRedundantParam, sourceValue, targetValue));
}
}
return targetJson;
}
/**
* 同步键值对参数
* @param deleteRedundantParam
* @param sourceParams
* @param targetParams
* @return
* @param <T>
*/
public static <T extends KeyValueParam> List<T> syncKeyValueParamDiff(
boolean deleteRedundantParam, List<T> sourceParams, List<T> targetParams) {
if (sourceParams == null) {
return deleteRedundantParam ? new ArrayList<>(0) : targetParams;
}
if (targetParams == null) {
return sourceParams;
}
Map<String, ? extends KeyValueParam> sourceMaps = sourceParams.stream()
.filter(KeyValueParam::isValid)
.collect(Collectors.toMap(KeyValueParam::getKey, Function.identity()));
// 删除多余参数
Iterator<? extends KeyValueParam> iterator = targetParams.iterator();
if (deleteRedundantParam) {
while (iterator.hasNext()) {
KeyValueParam targetParam = iterator.next();
if (targetParam.isValid() && !sourceMaps.keySet().contains(targetParam.getKey())) {
iterator.remove();
}
}
}
Set<String> targetKeys = targetParams.stream()
.filter(KeyValueParam::isValid)
.map(KeyValueParam::getKey)
.collect(Collectors.toSet());
// 记住最后一个有效的参数下标
int lastIndex = targetParams.size() - 1;
for (int i = targetParams.size() - 1; i >= 0; i--) {
if (targetParams.get(i).isValid()) {
lastIndex = i;
break;
}
}
for (int i = sourceParams.size() - 1; i >= 0; i--) {
KeyValueParam sourceParam = sourceParams.get(i);
if (!sourceParam.isValid()) {
continue;
}
if (!targetKeys.contains(sourceParam.getKey())) {
// 如果不包含则添加
targetParams.add(lastIndex + 1, (T) sourceParam);
}
}
return targetParams;
}
}

View File

@ -112,7 +112,8 @@ public class ApiTestCaseControllerTests extends BaseTest {
private static final String BATCH_RUN = "batch/run";
private static final String API_CHANGE_CLEAR = "api-change/clear/{0}";
private static final String API_CHANGE_IGNORE = "api-change/ignore/{0}?ignore={1}";
private static final String API_CHANGE_SYNC = "batch/api-change/sync";
private static final String BATCH_API_CHANGE_SYNC = "batch/api-change/sync";
private static final String API_CHANGE_SYNC = "api-change/sync";
private static final String API_COMPARE = "api/compare/{0}";
private static final ResultMatcher ERROR_REQUEST_MATCHER = status().is5xxServerError();
@ -479,13 +480,15 @@ public class ApiTestCaseControllerTests extends BaseTest {
updateCase.setId(apiTestCase.getId());
apiTestCaseMapper.updateByPrimaryKeySelective(updateCase);
this.requestGetWithOk(API_CHANGE_IGNORE, apiTestCase.getId(), true);
Assertions.assertFalse(apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId()).getApiChange());
Assertions.assertTrue(apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId()).getIgnoreApiDiff());
Assertions.assertTrue(apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId()).getIgnoreApiChange());
ApiTestCase result = apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId());
Assertions.assertFalse(result.getApiChange());
Assertions.assertTrue(result.getIgnoreApiDiff());
Assertions.assertTrue(result.getIgnoreApiChange());
this.requestGetWithOk(API_CHANGE_IGNORE, apiTestCase.getId(), false);
Assertions.assertFalse(apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId()).getApiChange());
Assertions.assertTrue(apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId()).getIgnoreApiDiff());
Assertions.assertFalse(apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId()).getIgnoreApiChange());
result = apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId());
Assertions.assertFalse(result.getApiChange());
Assertions.assertTrue(result.getIgnoreApiDiff());
Assertions.assertFalse(result.getIgnoreApiChange());
// @@校验权限
requestGetPermissionTest(PermissionConstants.PROJECT_API_DEFINITION_CASE_ADD, API_CHANGE_IGNORE, apiTestCase.getId(), true);
@ -497,6 +500,25 @@ public class ApiTestCaseControllerTests extends BaseTest {
public void batchSyncApiChange() throws Exception {
ApiCaseBatchSyncRequest request = new ApiCaseBatchSyncRequest();
request.setProjectId(DEFAULT_PROJECT_ID);
this.requestPostWithOk(BATCH_API_CHANGE_SYNC, request);
request.setSelectIds(List.of(apiTestCase.getId()));
request.setDeleteRedundantParam(true);
this.requestPostWithOk(BATCH_API_CHANGE_SYNC, request);
ApiTestCase result = apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId());
Assertions.assertFalse(result.getApiChange());
// @@校验权限
requestPostPermissionTest(PermissionConstants.PROJECT_API_DEFINITION_CASE_UPDATE, BATCH_API_CHANGE_SYNC, request);
}
@Test
@Order(4)
public void syncApiChange() throws Exception {
ApiCaseSyncRequest request = new ApiCaseSyncRequest();
request.setId(apiTestCase.getId());
request.setApiCaseRequest(JSON.parseObject(ApiDataUtils.toJSONString(new MsHTTPElement())));
this.requestPostWithOk(API_CHANGE_SYNC, request);
// @@校验权限

View File

@ -1,10 +1,14 @@
package io.metersphere.api.utils;
import io.metersphere.api.dto.definition.ApiCaseSyncItemRequest;
import io.metersphere.api.dto.definition.ApiCaseSyncRequest;
import io.metersphere.api.dto.request.controller.MsLoopController;
import io.metersphere.api.dto.request.http.MsHTTPElement;
import io.metersphere.api.dto.request.http.MsHeader;
import io.metersphere.api.dto.request.http.QueryParam;
import io.metersphere.api.dto.request.http.RestParam;
import io.metersphere.api.dto.request.http.body.*;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.project.api.KeyValueParam;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.XMLUtils;
@ -14,10 +18,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
/**
* @Author: jianxing
@ -481,4 +482,337 @@ public class HttpRequestParamDiffUtilsTests {
body.getXmlBody().setValue(xmlValue);
HttpRequestParamDiffUtils.getCompareHttpElement(msHTTPElement);
}
@Test
public void syncKeyValueParamDiff() {
KeyValueParam kv1 = new KeyValueParam();
kv1.setKey("key1");
kv1.setValue("value1");
KeyValueParam kv2 = new KeyValueParam();
kv2.setKey("key2");
kv2.setValue("value2");
List<KeyValueParam> formDataKVS = List.of(kv1, kv2);
List<KeyValueParam> result = HttpRequestParamDiffUtils.syncKeyValueParamDiff(true, formDataKVS, null);
Assertions.assertEquals(result, formDataKVS);
result = HttpRequestParamDiffUtils.syncKeyValueParamDiff(true, null, formDataKVS);
Assertions.assertEquals(result, List.of());
result = HttpRequestParamDiffUtils.syncKeyValueParamDiff(false, null, formDataKVS);
Assertions.assertEquals(result, formDataKVS);
KeyValueParam kv3 = new KeyValueParam();
kv3.setKey("key3");
kv3.setValue("value3");
FormDataKV kv4 = new FormDataKV();
result = HttpRequestParamDiffUtils.syncKeyValueParamDiff(true,
new ArrayList<>(List.of(kv1, kv2, kv4)),
new ArrayList<>(List.of(kv1, kv3, kv4)));
Assertions.assertEquals(result, Arrays.asList(kv1, kv2, kv4));
result = HttpRequestParamDiffUtils.syncKeyValueParamDiff(false,
new ArrayList<>(List.of(kv1, kv2, kv4)),
new ArrayList<>(List.of(kv1, kv3, kv4)));
Assertions.assertEquals(result, Arrays.asList(kv1, kv3, kv2, kv4));
}
@Test
public void syncJsonBodyDiff() {
String sourceJsonStr = """
{
"id": 10,
"name": "doggie",
"category": {
"id": null,
"name": "Dogs"
},
"photoUrls": [
"string",
{
"id": null,
"name": "Dogs"
}
],
"tags": [
{
"id": 0,
"name": "string"
}
]
}
""";
String targetJsonStr = """
{
"id": 11,
"delete": true,
"category": "",
"photoUrls": [
"string",
{
"id": "aaa",
"delete": true
}
],
"tags": [
{
"id": "111",
"name": null
}
]
}
""";
Object sourceJson = JSON.parseObject(sourceJsonStr);
Object targetJson = JSON.parseObject(targetJsonStr);
Object result = HttpRequestParamDiffUtils.syncJsonBodyDiff(true, sourceJson, targetJson);
Object assertionJson = JSON.parseObject("""
{
"id": 11,
"name": "doggie",
"category": {
"id": null,
"name": "Dogs"
},
"photoUrls": [
"string",
{
"id": null,
"name": "Dogs"
}
],
"tags": [
{
"id": 0,
"name": "string"
}
]
}
""");
Assertions.assertEquals(result, assertionJson);
JsonBody sourceJsonBody = new JsonBody();
sourceJsonBody.setJsonValue(sourceJsonStr);
JsonBody tartJsonBody = new JsonBody();
tartJsonBody.setJsonValue(targetJsonStr);
JsonBody resultBody = HttpRequestParamDiffUtils.syncJsonBodyDiff(true, sourceJsonBody, tartJsonBody);
Assertions.assertEquals(assertionJson, JSON.parseObject(resultBody.getJsonValue()));
sourceJson = JSON.parseObject(sourceJsonStr);
targetJson = JSON.parseObject(targetJsonStr);
result = HttpRequestParamDiffUtils.syncJsonBodyDiff(false, sourceJson, targetJson);
assertionJson = JSON.parseObject("""
{
"id": 11,
"name": "doggie",
"delete": true,
"category": {
"id": null,
"name": "Dogs"
},
"photoUrls": [
"string",
{
"id": "aaa",
"name": "Dogs",
"delete": true
}
],
"tags": [
{
"id": 0,
"name": "string"
}
]
}
""");
Assertions.assertEquals(result, assertionJson);
sourceJsonBody = new JsonBody();
sourceJsonBody.setJsonValue(sourceJsonStr);
tartJsonBody = new JsonBody();
tartJsonBody.setJsonValue(targetJsonStr);
resultBody = HttpRequestParamDiffUtils.syncJsonBodyDiff(false, sourceJsonBody, tartJsonBody);
Assertions.assertEquals(assertionJson, JSON.parseObject(resultBody.getJsonValue()));
resultBody = HttpRequestParamDiffUtils.syncJsonBodyDiff(false, null, tartJsonBody);
Assertions.assertEquals(resultBody, tartJsonBody);
resultBody = HttpRequestParamDiffUtils.syncJsonBodyDiff(true, null, tartJsonBody);
Assertions.assertEquals(resultBody, new JsonBody());
resultBody = HttpRequestParamDiffUtils.syncJsonBodyDiff(true, sourceJsonBody, null);
Assertions.assertEquals(resultBody, sourceJsonBody);
// 解析异常
sourceJsonBody.setJsonValue("ddd");
resultBody = HttpRequestParamDiffUtils.syncJsonBodyDiff(true, sourceJsonBody, sourceJsonBody);
Assertions.assertEquals(resultBody, sourceJsonBody);
}
@Test
public void syncXmlBodyDiff() throws Exception {
String sourceXmlStr = """
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<relativePath/>
</parent>
</project>
""";
String targetXmlStr = """
<project>
<modelVersion>4.0.0</modelVersion>
<delete>true</delete>
<parent>
<groupId>org.springframework.boot</groupId>
<relativePath/>
<delete>true</delete>
</parent>
</project>
""";
Element sourceElement = XMLUtils.stringToDocument(sourceXmlStr).getRootElement();
Element targetElement = XMLUtils.stringToDocument(targetXmlStr).getRootElement();
HttpRequestParamDiffUtils.syncXmlBodyDiff(true, null, sourceElement);
HttpRequestParamDiffUtils.syncXmlBodyDiff(true, sourceElement, null);
Element result = HttpRequestParamDiffUtils.syncXmlBodyDiff(true, sourceElement, targetElement);
String resultStr = HttpRequestParamDiffUtils.parseElementToString(result);
String assertionStr = HttpRequestParamDiffUtils.parseElementToString(XMLUtils.stringToDocument("""
<project>
<modelVersion>4.0.0</modelVersion>
\s
<parent>
<groupId>org.springframework.boot</groupId>
<relativePath/>
\s
<artifactId>spring-boot-starter-parent</artifactId></parent>
</project>
""").getRootElement());
Assertions.assertEquals(resultStr, assertionStr);
XmlBody sourceXmlBody = new XmlBody();
XmlBody tartgetXmlBody = new XmlBody();
sourceXmlBody.setValue(sourceXmlStr);
tartgetXmlBody.setValue(targetXmlStr);
XmlBody resultBody = HttpRequestParamDiffUtils.syncXmlBodyDiff(true, sourceXmlBody, tartgetXmlBody);
Assertions.assertEquals(assertionStr, resultBody.getValue());
sourceElement = XMLUtils.stringToDocument(sourceXmlStr).getRootElement();
targetElement = XMLUtils.stringToDocument(targetXmlStr).getRootElement();
result = HttpRequestParamDiffUtils.syncXmlBodyDiff(false, sourceElement, targetElement);
resultStr = HttpRequestParamDiffUtils.parseElementToString(result);
assertionStr = HttpRequestParamDiffUtils.parseElementToString(XMLUtils.stringToDocument("""
<project>
<modelVersion>4.0.0</modelVersion>
<delete>true</delete>
<parent>
<groupId>org.springframework.boot</groupId>
<relativePath/>
<delete>true</delete>
<artifactId>spring-boot-starter-parent</artifactId></parent>
</project>
""").getRootElement());
Assertions.assertEquals(resultStr, assertionStr);
// 格式错误
tartgetXmlBody.setValue("dddd");
resultBody = HttpRequestParamDiffUtils.syncXmlBodyDiff(true, sourceXmlBody, tartgetXmlBody);
Assertions.assertEquals(resultBody, tartgetXmlBody);
}
@Test
public void syncBodyDiff() {
Body sourceBody = new Body();
Body targetBody = new Body();
Body result = HttpRequestParamDiffUtils.syncBodyDiff(true, null, targetBody);
Assertions.assertEquals(result, null);
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, null);
Assertions.assertEquals(result, sourceBody);
sourceBody.setBodyType(Body.BodyType.FORM_DATA.name());
targetBody.setBodyType(Body.BodyType.WWW_FORM.name());
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, targetBody);
Assertions.assertEquals(result, sourceBody);
sourceBody.setBodyType(Body.BodyType.RAW.name());
targetBody.setBodyType(Body.BodyType.RAW.name());
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, targetBody);
Assertions.assertEquals(result, targetBody);
sourceBody.setBodyType(Body.BodyType.BINARY.name());
targetBody.setBodyType(Body.BodyType.BINARY.name());
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, targetBody);
Assertions.assertEquals(result, targetBody);
sourceBody.setBodyType(Body.BodyType.FORM_DATA.name());
targetBody.setBodyType(Body.BodyType.FORM_DATA.name());
sourceBody.setFormDataBody(new FormDataBody());
targetBody.setFormDataBody(new FormDataBody());
FormDataKV formDataKV = new FormDataKV();
formDataKV.setKey("key1");
formDataKV.setValue("value1");
sourceBody.getFormDataBody().getFormValues().add(formDataKV);
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, targetBody);
Assertions.assertEquals(result.getFormDataBody(), sourceBody.getFormDataBody());
sourceBody.setBodyType(Body.BodyType.WWW_FORM.name());
targetBody.setBodyType(Body.BodyType.WWW_FORM.name());
sourceBody.setWwwFormBody(new WWWFormBody());
targetBody.setWwwFormBody(new WWWFormBody());
WWWFormKV wwwFormKV = new WWWFormKV();
wwwFormKV.setKey("key1");
wwwFormKV.setValue("value1");
sourceBody.getWwwFormBody().getFormValues().add(wwwFormKV);
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, targetBody);
Assertions.assertEquals(result.getWwwFormBody(), sourceBody.getWwwFormBody());
sourceBody.setBodyType(Body.BodyType.JSON.name());
targetBody.setBodyType(Body.BodyType.JSON.name());
sourceBody.setJsonBody(new JsonBody());
targetBody.setJsonBody(new JsonBody());
sourceBody.getJsonBody().setJsonValue("""
{"id1":""}
""");
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, targetBody);
Assertions.assertEquals(result.getJsonBody(), sourceBody.getJsonBody());
sourceBody.setBodyType(Body.BodyType.XML.name());
targetBody.setBodyType(Body.BodyType.XML.name());
sourceBody.setXmlBody(new XmlBody());
targetBody.setXmlBody(new XmlBody());
sourceBody.getXmlBody().setValue("""
<a></a>
""");
result = HttpRequestParamDiffUtils.syncBodyDiff(true, sourceBody, targetBody);
Assertions.assertEquals(result.getXmlBody(), sourceBody.getXmlBody());
}
@Test
public void syncRequestDiff() {
MsHTTPElement sourceElement = new MsHTTPElement();
MsHTTPElement targetElement = new MsHTTPElement();
ApiCaseSyncRequest request = new ApiCaseSyncRequest();
ApiCaseSyncItemRequest syncItems = new ApiCaseSyncItemRequest();
request.setSyncItems(syncItems);
request.setDeleteRedundantParam(true);
AbstractMsTestElement resultTestElement = HttpRequestParamDiffUtils.syncRequestDiff(request, sourceElement, targetElement);
Assertions.assertEquals(resultTestElement, targetElement);
syncItems.setBody(false);
syncItems.setQuery(false);
syncItems.setRest(false);
syncItems.setHeader(false);
resultTestElement = HttpRequestParamDiffUtils.syncRequestDiff(request, sourceElement, targetElement);
Assertions.assertEquals(resultTestElement, targetElement);
resultTestElement = HttpRequestParamDiffUtils.syncRequestDiff(request, new MsLoopController(), targetElement);
Assertions.assertEquals(resultTestElement, targetElement);
}
}