diff --git a/backend/src/main/java/io/metersphere/api/controller/ApiDelimitController.java b/backend/src/main/java/io/metersphere/api/controller/ApiDelimitController.java index 01ee2abcdd..d517249bac 100644 --- a/backend/src/main/java/io/metersphere/api/controller/ApiDelimitController.java +++ b/backend/src/main/java/io/metersphere/api/controller/ApiDelimitController.java @@ -2,6 +2,7 @@ package io.metersphere.api.controller; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; +import io.metersphere.api.dto.APIReportResult; import io.metersphere.api.dto.delimit.ApiDelimitRequest; import io.metersphere.api.dto.delimit.ApiDelimitResult; import io.metersphere.api.dto.delimit.SaveApiDelimitRequest; @@ -59,4 +60,18 @@ public class ApiDelimitController { return apiDelimitService.get(id); } + @PostMapping(value = "/run/debug", consumes = {"multipart/form-data"}) + public String runDebug(@RequestPart("request") SaveApiDelimitRequest request, @RequestPart(value = "file") MultipartFile file, @RequestPart(value = "files") List bodyFiles) { + return apiDelimitService.run(request, file, bodyFiles); + } + + @PostMapping(value = "/run", consumes = {"multipart/form-data"}) + public String run(@RequestPart("request") SaveApiDelimitRequest request, @RequestPart(value = "file") MultipartFile file, @RequestPart(value = "files") List bodyFiles) { + return apiDelimitService.run(request, file, bodyFiles); + } + + @GetMapping("/report/get/{testId}/{test}") + public APIReportResult getReport(@PathVariable String testId, @PathVariable String test) { + return apiDelimitService.getResult(testId, test); + } } diff --git a/backend/src/main/java/io/metersphere/api/dto/delimit/SaveApiDelimitRequest.java b/backend/src/main/java/io/metersphere/api/dto/delimit/SaveApiDelimitRequest.java index ed9f0cf66e..5fc4499e07 100644 --- a/backend/src/main/java/io/metersphere/api/dto/delimit/SaveApiDelimitRequest.java +++ b/backend/src/main/java/io/metersphere/api/dto/delimit/SaveApiDelimitRequest.java @@ -1,6 +1,7 @@ package io.metersphere.api.dto.delimit; import io.metersphere.api.dto.scenario.request.Request; +import io.metersphere.api.dto.delimit.response.Response; import io.metersphere.base.domain.Schedule; import lombok.Getter; import lombok.Setter; @@ -13,6 +14,8 @@ public class SaveApiDelimitRequest { private String id; + private String reportId; + private String projectId; private String name; @@ -31,6 +34,10 @@ public class SaveApiDelimitRequest { private Request request; + private Response response; + + private String environmentId; + private String userId; private Schedule schedule; diff --git a/backend/src/main/java/io/metersphere/api/dto/delimit/response/HttpResponse.java b/backend/src/main/java/io/metersphere/api/dto/delimit/response/HttpResponse.java new file mode 100644 index 0000000000..08d9ce60f4 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/delimit/response/HttpResponse.java @@ -0,0 +1,26 @@ +package io.metersphere.api.dto.delimit.response; + +import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson.annotation.JSONType; +import io.metersphere.api.dto.scenario.Body; +import io.metersphere.api.dto.scenario.KeyValue; +import io.metersphere.api.dto.scenario.request.RequestType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@JSONType(typeName = RequestType.HTTP) +public class HttpResponse extends Response { + // type 必须放最前面,以便能够转换正确的类 + private String type = RequestType.HTTP; + @JSONField(ordinal = 1) + private List headers; + @JSONField(ordinal = 2) + private List statusCode; + @JSONField(ordinal = 3) + private Body body; + +} diff --git a/backend/src/main/java/io/metersphere/api/dto/delimit/response/Response.java b/backend/src/main/java/io/metersphere/api/dto/delimit/response/Response.java new file mode 100644 index 0000000000..d5eff6e2bf --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/delimit/response/Response.java @@ -0,0 +1,23 @@ +package io.metersphere.api.dto.delimit.response; + +import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson.annotation.JSONType; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.metersphere.api.dto.scenario.request.RequestType; +import lombok.Data; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = HttpResponse.class, name = RequestType.HTTP), +}) +@JSONType(seeAlso = {HttpResponse.class}, typeKey = "type") +@Data +public abstract class Response { + @JSONField(ordinal = 1) + private String id; + @JSONField(ordinal = 2) + private String name; + @JSONField(ordinal = 3) + private Boolean enable; +} diff --git a/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java b/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java index 5073eeefbf..fa2ea2f532 100644 --- a/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java +++ b/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java @@ -9,5 +9,7 @@ public class Body { private String type; private String raw; private String format; + private Object json; + private String xml; private List kvs; } diff --git a/backend/src/main/java/io/metersphere/api/dto/scenario/KeyValue.java b/backend/src/main/java/io/metersphere/api/dto/scenario/KeyValue.java index 45ab26da49..7c0bc61994 100644 --- a/backend/src/main/java/io/metersphere/api/dto/scenario/KeyValue.java +++ b/backend/src/main/java/io/metersphere/api/dto/scenario/KeyValue.java @@ -14,15 +14,18 @@ public class KeyValue { private String description; private String contentType; private boolean enable; + private boolean required; public KeyValue() { this.enable = true; + this.required = true; } public KeyValue(String name, String value) { this.name = name; this.value = value; this.enable = true; + this.required = true; } public KeyValue(String name, String value, String description) { @@ -30,5 +33,6 @@ public class KeyValue { this.value = value; this.enable = true; this.description = description; + this.required = true; } } diff --git a/backend/src/main/java/io/metersphere/api/jmeter/ApiTestResult.java b/backend/src/main/java/io/metersphere/api/jmeter/ApiTestResult.java new file mode 100644 index 0000000000..efb0f72acd --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/jmeter/ApiTestResult.java @@ -0,0 +1,65 @@ +package io.metersphere.api.jmeter; + +import com.alibaba.fastjson.JSON; +import io.metersphere.base.domain.ApiDelimitExecResult; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ApiTestResult { + + private String id; + + private String name; + + private long responseTime; + + private int error = 0; + + private int success = 0; + + private int totalAssertions = 0; + + private int passAssertions = 0; + + private final List requestResults = new ArrayList<>(); + + public void addRequestResult(RequestResult result) { + requestResults.add(result); + } + + public ApiTestResult() { + + } + + public ApiTestResult(ApiDelimitExecResult result) { + this.id = result.getId(); + this.responseTime = result.getEndTime(); + this.addRequestResult(JSON.parseObject(result.getContent(), RequestResult.class)); + } + + public void addResponseTime(long time) { + this.responseTime += time; + } + + public void addError(int count) { + this.error += count; + } + + public void addSuccess() { + this.success++; + } + public int getSuccess() { + return this.success; + } + + public void addTotalAssertions(int count) { + this.totalAssertions += count; + } + + public void addPassAssertions(int count) { + this.passAssertions += count; + } +} diff --git a/backend/src/main/java/io/metersphere/api/jmeter/DelimitBackendListenerClient.java b/backend/src/main/java/io/metersphere/api/jmeter/DelimitBackendListenerClient.java new file mode 100644 index 0000000000..901932a7b1 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/jmeter/DelimitBackendListenerClient.java @@ -0,0 +1,165 @@ +package io.metersphere.api.jmeter; + +import io.metersphere.api.service.ApiDelimitExecResultService; +import io.metersphere.api.service.ApiDelimitService; +import io.metersphere.commons.constants.ApiRunMode; +import io.metersphere.commons.utils.CommonBeanFactory; +import io.metersphere.commons.utils.LogUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.assertions.AssertionResult; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient; +import org.apache.jmeter.visualizers.backend.BackendListenerContext; +import org.springframework.http.HttpMethod; + +import java.io.Serializable; +import java.util.*; + +/** + * JMeter BackendListener扩展, jmx脚本中使用 + */ +public class DelimitBackendListenerClient extends AbstractBackendListenerClient implements Serializable { + + public final static String TEST_ID = "ms.test.id"; + + private final List queue = new ArrayList<>(); + + public String runMode = ApiRunMode.RUN.name(); + private ApiDelimitService apiDelimitService; + private ApiDelimitExecResultService apiDelimitExecResultService; + // 测试ID + private String testId; + // 运行结果报告ID + private String reportId; + + @Override + public void setupTest(BackendListenerContext context) throws Exception { + setParam(context); + apiDelimitService = CommonBeanFactory.getBean(ApiDelimitService.class); + if (apiDelimitService == null) { + LogUtil.error("apiDelimitService is required"); + } + apiDelimitExecResultService = CommonBeanFactory.getBean(ApiDelimitExecResultService.class); + if (apiDelimitExecResultService == null) { + LogUtil.error("apiDelimitExecResultService is required"); + } + super.setupTest(context); + } + + + @Override + public void handleSampleResults(List sampleResults, BackendListenerContext context) { + queue.addAll(sampleResults); + } + + @Override + public void teardownTest(BackendListenerContext context) throws Exception { + ApiTestResult testResult = new ApiTestResult(); + testResult.setId(testId); + queue.forEach(result -> { + RequestResult reqResult = getRequestResult(result); + testResult.addRequestResult(reqResult); + testResult.addPassAssertions(reqResult.getPassAssertions()); + testResult.addTotalAssertions(reqResult.getTotalAssertions()); + }); + // 调试操作,不需要存储结果 + if (StringUtils.isBlank(reportId)) { + apiDelimitService.addResult(testResult); + } else { + apiDelimitExecResultService.saveApiResult(testResult); + } + queue.clear(); + super.teardownTest(context); + } + + + private RequestResult getRequestResult(SampleResult result) { + RequestResult reqResult = new RequestResult(); + reqResult.setName(result.getSampleLabel()); + reqResult.setUrl(result.getUrlAsString()); + reqResult.setMethod(getMethod(result)); + reqResult.setBody(result.getSamplerData()); + reqResult.setHeaders(result.getRequestHeaders()); + reqResult.setRequestSize(result.getSentBytes()); + reqResult.setStartTime(result.getStartTime()); + reqResult.setTotalAssertions(result.getAssertionResults().length); + reqResult.setSuccess(result.isSuccessful()); + reqResult.setError(result.getErrorCount()); + for (SampleResult subResult : result.getSubResults()) { + reqResult.getSubRequestResults().add(getRequestResult(subResult)); + } + + ResponseResult responseResult = reqResult.getResponseResult(); + responseResult.setBody(result.getResponseDataAsString()); + responseResult.setHeaders(result.getResponseHeaders()); + responseResult.setLatency(result.getLatency()); + responseResult.setResponseCode(result.getResponseCode()); + responseResult.setResponseSize(result.getResponseData().length); + responseResult.setResponseTime(result.getTime()); + responseResult.setResponseMessage(result.getResponseMessage()); + + if (JMeterVars.get(result.hashCode()) != null) { + List vars = new LinkedList<>(); + JMeterVars.get(result.hashCode()).entrySet().parallelStream().reduce(vars, (first, second) -> { + first.add(second.getKey() + ":" + second.getValue()); + return first; + }, (first, second) -> { + if (first == second) { + return first; + } + first.addAll(second); + return first; + }); + responseResult.setVars(StringUtils.join(vars, "\n")); + JMeterVars.remove(result.hashCode()); + } + for (AssertionResult assertionResult : result.getAssertionResults()) { + ResponseAssertionResult responseAssertionResult = getResponseAssertionResult(assertionResult); + if (responseAssertionResult.isPass()) { + reqResult.addPassAssertions(); + } + responseResult.getAssertions().add(responseAssertionResult); + } + return reqResult; + } + + private String getMethod(SampleResult result) { + String body = result.getSamplerData(); + // Dubbo Protocol + String start = "RPC Protocol: "; + String end = "://"; + if (StringUtils.contains(body, start)) { + return StringUtils.substringBetween(body, start, end).toUpperCase(); + } else { + // Http Method + String method = StringUtils.substringBefore(body, " "); + for (HttpMethod value : HttpMethod.values()) { + if (StringUtils.equals(method, value.name())) { + return method; + } + } + return "Request"; + } + } + + private void setParam(BackendListenerContext context) { + this.testId = context.getParameter(TEST_ID); + this.runMode = context.getParameter("runMode"); + this.reportId = context.getParameter("reportId"); + if (StringUtils.isBlank(this.runMode)) { + this.runMode = ApiRunMode.RUN.name(); + } + } + + private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) { + ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult(); + responseAssertionResult.setName(assertionResult.getName()); + responseAssertionResult.setPass(!assertionResult.isFailure() && !assertionResult.isError()); + + if (!responseAssertionResult.isPass()) { + responseAssertionResult.setMessage(assertionResult.getFailureMessage()); + } + return responseAssertionResult; + } + +} diff --git a/backend/src/main/java/io/metersphere/api/jmeter/DelimitJMeterService.java b/backend/src/main/java/io/metersphere/api/jmeter/DelimitJMeterService.java new file mode 100644 index 0000000000..3141c3e56f --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/jmeter/DelimitJMeterService.java @@ -0,0 +1,80 @@ +package io.metersphere.api.jmeter; + +import io.metersphere.commons.constants.ApiRunMode; +import io.metersphere.commons.exception.MSException; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.config.JmeterProperties; +import io.metersphere.i18n.Translator; +import org.apache.jmeter.config.Arguments; +import org.apache.jmeter.save.SaveService; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jmeter.visualizers.backend.BackendListener; +import org.apache.jorphan.collections.HashTree; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.File; +import java.io.InputStream; +import java.lang.reflect.Field; + +@Service +public class DelimitJMeterService { + + @Resource + private JmeterProperties jmeterProperties; + + public void run(String testId, String reportId, InputStream is) { + String home = getJmeterHome(); + String jmeterProperties = home + "/bin/jmeter.properties"; + JMeterUtils.loadJMeterProperties(jmeterProperties); + JMeterUtils.setJMeterHome(home); + JMeterUtils.setLocale(LocaleContextHolder.getLocale()); + + try { + Object scriptWrapper = SaveService.loadElement(is); + HashTree testPlan = getHashTree(scriptWrapper); + JMeterVars.addJSR223PostProcessor(testPlan); + addBackendListener(testId, reportId, testPlan); + LocalRunner runner = new LocalRunner(testPlan); + runner.run(); + } catch (Exception e) { + LogUtil.error(e.getMessage(), e); + MSException.throwException(Translator.get("api_load_script_error")); + } + } + + public String getJmeterHome() { + String home = getClass().getResource("/").getPath() + "jmeter"; + try { + File file = new File(home); + if (file.exists()) { + return home; + } else { + return jmeterProperties.getHome(); + } + } catch (Exception e) { + return jmeterProperties.getHome(); + } + } + + private HashTree getHashTree(Object scriptWrapper) throws Exception { + Field field = scriptWrapper.getClass().getDeclaredField("testPlan"); + field.setAccessible(true); + return (HashTree) field.get(scriptWrapper); + } + + private void addBackendListener(String testId, String debugReportId, HashTree testPlan) { + BackendListener backendListener = new BackendListener(); + backendListener.setName(testId); + Arguments arguments = new Arguments(); + arguments.addArgument(DelimitBackendListenerClient.TEST_ID, testId); + + arguments.addArgument("runMode", ApiRunMode.DEBUG.name()); + arguments.addArgument("reportId", debugReportId); + + backendListener.setArguments(arguments); + backendListener.setClassname(DelimitBackendListenerClient.class.getCanonicalName()); + testPlan.add(testPlan.getArray()[0], backendListener); + } +} diff --git a/backend/src/main/java/io/metersphere/api/service/ApiDelimitExecResultService.java b/backend/src/main/java/io/metersphere/api/service/ApiDelimitExecResultService.java new file mode 100644 index 0000000000..1003514982 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/service/ApiDelimitExecResultService.java @@ -0,0 +1,38 @@ +package io.metersphere.api.service; + +import com.alibaba.fastjson.JSON; +import io.metersphere.api.jmeter.ApiTestResult; +import io.metersphere.base.domain.ApiDelimitExecResult; +import io.metersphere.base.mapper.ApiDelimitExecResultMapper; +import io.metersphere.commons.utils.SessionUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Objects; +import java.util.UUID; + +@Service +@Transactional(rollbackFor = Exception.class) +public class ApiDelimitExecResultService { + @Resource + private ApiDelimitExecResultMapper apiDelimitExecResultMapper; + + + public void saveApiResult(ApiTestResult result) { + result.getRequestResults().forEach(item -> { + // 清理原始资源,每个执行 保留一条结果 + apiDelimitExecResultMapper.deleteByResourceId(item.getName()); + ApiDelimitExecResult saveResult = new ApiDelimitExecResult(); + saveResult.setId(UUID.randomUUID().toString()); + saveResult.setUserId(Objects.requireNonNull(SessionUtils.getUser()).getId()); + saveResult.setName(item.getUrl()); + saveResult.setResourceId(item.getName()); + saveResult.setContent(JSON.toJSONString(item)); + saveResult.setStartTime(item.getStartTime()); + saveResult.setEndTime(item.getResponseResult().getResponseTime()); + saveResult.setStatus(item.getResponseResult().getResponseCode().equals("200") ? "success" : "error"); + apiDelimitExecResultMapper.insert(saveResult); + }); + } +} diff --git a/backend/src/main/java/io/metersphere/api/service/ApiDelimitService.java b/backend/src/main/java/io/metersphere/api/service/ApiDelimitService.java index afe2489d8b..eae5a6b4c4 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiDelimitService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiDelimitService.java @@ -1,10 +1,15 @@ package io.metersphere.api.service; +import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import io.metersphere.api.dto.APIReportResult; import io.metersphere.api.dto.delimit.ApiComputeResult; import io.metersphere.api.dto.delimit.ApiDelimitRequest; import io.metersphere.api.dto.delimit.ApiDelimitResult; import io.metersphere.api.dto.delimit.SaveApiDelimitRequest; +import io.metersphere.api.jmeter.ApiTestResult; +import io.metersphere.api.jmeter.DelimitJMeterService; +import io.metersphere.api.parse.JmeterDocumentParser; import io.metersphere.base.domain.*; import io.metersphere.base.mapper.ApiDelimitExecResultMapper; import io.metersphere.base.mapper.ApiDelimitMapper; @@ -23,6 +28,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.MultipartFile; +import sun.security.util.Cache; import javax.annotation.Resource; import java.io.*; @@ -46,6 +52,9 @@ public class ApiDelimitService { private ApiTestCaseService apiTestCaseService; @Resource private ApiDelimitExecResultMapper apiDelimitExecResultMapper; + @Resource + private DelimitJMeterService jMeterService; + private static Cache cache = Cache.newHardMemoryCache(0, 3600 * 24); private static final String BODY_FILE_DIR = "/opt/metersphere/data/body"; @@ -79,7 +88,7 @@ public class ApiDelimitService { public void create(SaveApiDelimitRequest request, MultipartFile file, List bodyFiles) { List bodyUploadIds = new ArrayList<>(request.getBodyUploadIds()); ApiDelimit test = createTest(request, file); - createBodyFiles(test, bodyUploadIds, bodyFiles); + createBodyFiles(test.getId(), bodyUploadIds, bodyFiles); } private ApiDelimit createTest(SaveApiDelimitRequest request, MultipartFile file) { @@ -109,13 +118,13 @@ public class ApiDelimitService { List bodyUploadIds = new ArrayList<>(request.getBodyUploadIds()); request.setBodyUploadIds(null); ApiDelimit test = updateTest(request); - createBodyFiles(test, bodyUploadIds, bodyFiles); + createBodyFiles(test.getId(), bodyUploadIds, bodyFiles); saveFile(test.getId(), file); } - private void createBodyFiles(ApiDelimit test, List bodyUploadIds, List bodyFiles) { + private void createBodyFiles(String testId, List bodyUploadIds, List bodyFiles) { if (bodyUploadIds.size() > 0) { - String dir = BODY_FILE_DIR + "/" + test.getId(); + String dir = BODY_FILE_DIR + "/" + testId; File testDir = new File(dir); if (!testDir.exists()) { testDir.mkdirs(); @@ -181,6 +190,8 @@ public class ApiDelimitService { test.setPath(request.getPath()); test.setUrl(request.getUrl()); test.setDescription(request.getDescription()); + test.setResponse(JSONObject.toJSONString(request.getResponse())); + test.setEnvironmentId(request.getEnvironmentId()); test.setUserId(request.getUserId()); apiDelimitMapper.updateByPrimaryKeySelective(test); @@ -201,6 +212,8 @@ public class ApiDelimitService { test.setUpdateTime(System.currentTimeMillis()); test.setStatus(APITestStatus.Underway.name()); test.setModulePath(request.getModulePath()); + test.setResponse(JSONObject.toJSONString(request.getResponse())); + test.setEnvironmentId(request.getEnvironmentId()); if (request.getUserId() == null) { test.setUserId(Objects.requireNonNull(SessionUtils.getUser()).getId()); } else { @@ -212,18 +225,18 @@ public class ApiDelimitService { } private void saveFile(String apiId, MultipartFile file) { - final FileMetadata fileMetadata = fileService.saveFile(file); + final FileMetadata metadata = fileService.saveFile(file); ApiTestFile apiTestFile = new ApiTestFile(); apiTestFile.setTestId(apiId); - apiTestFile.setFileId(fileMetadata.getId()); + apiTestFile.setFileId(metadata.getId()); apiTestFileMapper.insert(apiTestFile); } private void deleteFileByTestId(String apiId) { - ApiTestFileExample ApiTestFileExample = new ApiTestFileExample(); - ApiTestFileExample.createCriteria().andTestIdEqualTo(apiId); - final List ApiTestFiles = apiTestFileMapper.selectByExample(ApiTestFileExample); - apiTestFileMapper.deleteByExample(ApiTestFileExample); + ApiTestFileExample apiTestFileExample = new ApiTestFileExample(); + apiTestFileExample.createCriteria().andTestIdEqualTo(apiId); + final List ApiTestFiles = apiTestFileMapper.selectByExample(apiTestFileExample); + apiTestFileMapper.deleteByExample(apiTestFileExample); if (!CollectionUtils.isEmpty(ApiTestFiles)) { final List fileIds = ApiTestFiles.stream().map(ApiTestFile::getFileId).collect(Collectors.toList()); @@ -231,4 +244,61 @@ public class ApiDelimitService { } } + /** + * 测试执行 + * + * @param request + * @param file + * @param bodyFiles + * @return + */ + public String run(SaveApiDelimitRequest request, MultipartFile file, List bodyFiles) { + if (file == null) { + throw new IllegalArgumentException(Translator.get("file_cannot_be_null")); + } + List bodyUploadIds = new ArrayList<>(request.getBodyUploadIds()); + createBodyFiles(request.getId(), bodyUploadIds, bodyFiles); + InputStream is = null; + try { + // 解析 xml 处理 mock 数据 + byte[] bytes = JmeterDocumentParser.parse(file.getBytes()); + is = new ByteArrayInputStream(bytes); + } catch (IOException e) { + LogUtil.error(e); + } + jMeterService.run(request.getId(), request.getReportId(), is); + return request.getId(); + } + + public void addResult(ApiTestResult res) { + cache.put(res.getId(), res); + } + + /** + * 获取执行结果报告 + * + * @param testId + * @param test + * @return + */ + public APIReportResult getResult(String testId, String test) { + if (test.equals("debug")) { + Object res = cache.get(testId); + if (res != null) { + cache.remove(testId); + APIReportResult reportResult = new APIReportResult(); + reportResult.setContent(JSON.toJSONString(res)); + return reportResult; + } + } else { + ApiDelimitExecResult data = apiDelimitExecResultMapper.selectByResourceId(testId); + if (data == null) { + return null; + } + APIReportResult reportResult = new APIReportResult(); + reportResult.setContent(data.getContent()); + return reportResult; + } + return null; + } } diff --git a/backend/src/main/java/io/metersphere/base/domain/ApiDelimit.java b/backend/src/main/java/io/metersphere/base/domain/ApiDelimit.java index 7d86acfcfd..d6ced334e3 100644 --- a/backend/src/main/java/io/metersphere/base/domain/ApiDelimit.java +++ b/backend/src/main/java/io/metersphere/base/domain/ApiDelimit.java @@ -16,10 +16,12 @@ public class ApiDelimit implements Serializable { private String url; - private String description; + private String environmentId; private String status; + private String description; + private String userId; private String moduleId; @@ -32,5 +34,7 @@ public class ApiDelimit implements Serializable { private String request; + private String response; + private static final long serialVersionUID = 1L; } \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/ApiDelimitExecResult.java b/backend/src/main/java/io/metersphere/base/domain/ApiDelimitExecResult.java index e25cb328c8..6854226e03 100644 --- a/backend/src/main/java/io/metersphere/base/domain/ApiDelimitExecResult.java +++ b/backend/src/main/java/io/metersphere/base/domain/ApiDelimitExecResult.java @@ -12,7 +12,7 @@ public class ApiDelimitExecResult implements Serializable { private String content; private String status; private String userId; - private String startTime; - private String endTime; + private Long startTime; + private Long endTime; } diff --git a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.java index d47ffb6156..c72bb963f2 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.java +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.java @@ -7,4 +7,9 @@ public interface ApiDelimitExecResultMapper { int deleteByResourceId(String id); int insert(ApiDelimitExecResult record); + + ApiDelimitExecResult selectByResourceId(String resourceId); + + ApiDelimitExecResult selectByPrimaryKey(String id); + } \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.xml index a44a7ab56b..059584fa30 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitExecResultMapper.xml @@ -10,7 +10,18 @@ insert into api_delimit_exec_result (id, resource_id,name,content, status, user_id, start_time, end_time) values - (#{id,jdbcType=VARCHAR}, #{resourceId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{content,jdbcType=LONGVARCHAR}, #{status,jdbcType=VARCHAR}, + (#{id,jdbcType=VARCHAR}, #{resourceId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{content,jdbcType=LONGVARCHAR}, #{status,jdbcType=VARCHAR}, #{userId,jdbcType=VARCHAR}, #{startTime,jdbcType=BIGINT}, #{endTime,jdbcType=BIGINT}) + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.java index 68f2e5b406..9be490d838 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.java +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.java @@ -23,7 +23,6 @@ public interface ApiDelimitMapper { int insert(ApiDelimit record); - int insertSelective(ApiDelimit record); List selectByExampleWithBLOBs(ApiDelimitExample example); @@ -31,11 +30,6 @@ public interface ApiDelimitMapper { ApiDelimit selectByPrimaryKey(String id); - int updateByExampleSelective(@Param("record") ApiDelimit record, @Param("example") ApiDelimitExample example); - - int updateByExampleWithBLOBs(@Param("record") ApiDelimit record, @Param("example") ApiDelimitExample example); - - int updateByExample(@Param("record") ApiDelimit record, @Param("example") ApiDelimitExample example); int updateByPrimaryKeySelective(ApiDelimit record); diff --git a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.xml index 22ef97ecd2..ea82bb95c2 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiDelimitMapper.xml @@ -17,6 +17,7 @@ + @@ -204,7 +205,7 @@ + select count(*) from api_delimit @@ -396,89 +319,7 @@ - - update api_delimit - - - id = #{record.id,jdbcType=VARCHAR}, - - - project_id = #{record.projectId,jdbcType=VARCHAR}, - - - name = #{record.name,jdbcType=VARCHAR}, - - - module_id = #{record.moduleId,jdbcType=BIGINT}, - - - module_path = #{record.modulePath,jdbcType=BIGINT}, - - - - url = #{record.url,jdbcType=LONGVARCHAR}, - - - path = #{record.path,jdbcType=LONGVARCHAR}, - - - - description = #{record.description,jdbcType=VARCHAR}, - - - status = #{record.status,jdbcType=VARCHAR}, - - - user_id = #{record.userId,jdbcType=VARCHAR}, - - - create_time = #{record.createTime,jdbcType=BIGINT}, - - - update_time = #{record.updateTime,jdbcType=BIGINT}, - - - request = #{record.request,jdbcType=LONGVARCHAR}, - - - - - - - - update api_delimit - set id = #{record.id,jdbcType=VARCHAR}, - project_id = #{record.projectId,jdbcType=VARCHAR}, - name = #{record.name,jdbcType=VARCHAR}, - module_id = #{moduleId,jdbcType=VARCHAR}, - module_path = #{modulePath,jdbcType=VARCHAR}, - url = #{url,jdbcType=VARCHAR}, - path = #{path,jdbcType=VARCHAR}, - description = #{record.description,jdbcType=VARCHAR}, - status = #{record.status,jdbcType=VARCHAR}, - user_id = #{record.userId,jdbcType=VARCHAR}, - create_time = #{record.createTime,jdbcType=BIGINT}, - update_time = #{record.updateTime,jdbcType=BIGINT}, - request = #{record.request,jdbcType=LONGVARCHAR} - - - - - - update api_delimit - set id = #{record.id,jdbcType=VARCHAR}, - project_id = #{record.projectId,jdbcType=VARCHAR}, - name = #{record.name,jdbcType=VARCHAR}, - description = #{record.description,jdbcType=VARCHAR}, - status = #{record.status,jdbcType=VARCHAR}, - user_id = #{record.userId,jdbcType=VARCHAR}, - create_time = #{record.createTime,jdbcType=BIGINT}, - update_time = #{record.updateTime,jdbcType=BIGINT} - - - - update api_delimit @@ -519,7 +360,12 @@ request = #{request,jdbcType=LONGVARCHAR}, - + + response = #{response,jdbcType=LONGVARCHAR}, + + + environment_id = #{environmentId,jdbcType=VARCHAR}, + where id = #{id,jdbcType=VARCHAR} @@ -536,7 +382,9 @@ path = #{path,jdbcType=VARCHAR}, create_time = #{createTime,jdbcType=BIGINT}, update_time = #{updateTime,jdbcType=BIGINT}, - request = #{request,jdbcType=LONGVARCHAR} + request = #{request,jdbcType=LONGVARCHAR}, + response = #{response,jdbcType=LONGVARCHAR}, + environment_id = #{environmentId,jdbcType=VARCHAR} where id = #{id,jdbcType=VARCHAR} diff --git a/frontend/package.json b/frontend/package.json index 6a709fd88a..0f3d82d717 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,27 +17,28 @@ "@fortawesome/vue-fontawesome": "^0.1.9", "axios": "^0.19.0", "core-js": "^3.4.3", + "diffable-html": "^4.0.0", "echarts": "^4.6.0", + "el-table-infinite-scroll": "^1.0.10", "element-ui": "^2.13.0", + "html2canvas": "^1.0.0-rc.7", + "js-base64": "^3.4.4", + "json-bigint": "^1.0.0", + "jsoneditor": "^9.1.2", + "jspdf": "^2.1.1", + "md5": "^2.3.0", + "mockjs": "^1.1.0", + "nprogress": "^0.2.0", + "sha.js": "^2.4.11", "vue": "^2.6.10", + "vue-calendar-heatmap": "^0.8.4", "vue-echarts": "^4.1.0", "vue-i18n": "^8.15.3", + "vue-pdf": "^4.2.0", "vue-router": "^3.1.3", "vuedraggable": "^2.23.2", "vuex": "^3.1.2", - "vue-calendar-heatmap": "^0.8.4", - "mockjs": "^1.1.0", - "md5": "^2.3.0", - "sha.js": "^2.4.11", - "js-base64": "^3.4.4", - "json-bigint": "^1.0.0", - "html2canvas": "^1.0.0-rc.7", - "jspdf": "^2.1.1", - "yan-progress": "^1.0.3", - "nprogress": "^0.2.0", - "el-table-infinite-scroll": "^1.0.10", - "vue-pdf": "^4.2.0", - "diffable-html": "^4.0.0" + "yan-progress": "^1.0.3" }, "devDependencies": { "@vue/cli-plugin-babel": "^4.1.0", diff --git a/frontend/src/business/components/api/delimit/ApiDelimit.vue b/frontend/src/business/components/api/delimit/ApiDelimit.vue index 390084be98..143a9df6c0 100644 --- a/frontend/src/business/components/api/delimit/ApiDelimit.vue +++ b/frontend/src/business/components/api/delimit/ApiDelimit.vue @@ -47,7 +47,7 @@
- +
@@ -182,7 +182,7 @@ }, saveApi(data) { this.setTabTitle(data); - this.$refs.apiList[0].initTableData(data); + this.$refs.apiList[0].initApiTable(data); }, initTree(data) { this.moduleOptions = data; diff --git a/frontend/src/business/components/api/delimit/components/ApiCaseList.vue b/frontend/src/business/components/api/delimit/components/ApiCaseList.vue index 9c549f49bd..d35c694660 100644 --- a/frontend/src/business/components/api/delimit/components/ApiCaseList.vue +++ b/frontend/src/business/components/api/delimit/components/ApiCaseList.vue @@ -62,7 +62,9 @@
- +{{$t('api_test.delimit.request.case')}} + + +{{$t('api_test.delimit.request.case')}} +
@@ -78,7 +80,7 @@ - +
@@ -97,16 +99,21 @@ - - {{item.type!= 'create' ? item.name:''}} + + + {{item.type!= 'create' ? item.name:''}} + +
{{item.createTime | timestampFormatDate }} {{item.createUser}} 创建 {{item.updateTime | timestampFormatDate }} {{item.updateUser}} 更新
- @@ -126,7 +133,15 @@
+

{{$t('api_test.delimit.request.req_param')}}

+ + +

{{$t('api_test.delimit.request.assertions_rule')}}

+ + + {{$t('commons.save')}} @@ -146,11 +161,12 @@ import MsTag from "../../../common/components/MsTag"; import MsTipButton from "../../../common/components/MsTipButton"; import MsApiRequestForm from "./request/ApiRequestForm"; - import {Test, RequestFactory} from "../model/ScenarioModel"; + import {Test, RequestFactory} from "../model/ApiTestModel"; import {downloadFile, getUUID} from "@/common/js/utils"; import {parseEnvironment} from "../model/EnvironmentModel"; import ApiEnvironmentConfig from "../../test/components/ApiEnvironmentConfig"; import {PRIORITY} from "../model/JsonData"; + import MsApiAssertions from "./assertion/ApiAssertions"; export default { name: 'ApiCaseList', @@ -158,18 +174,21 @@ MsTag, MsTipButton, MsApiRequestForm, - ApiEnvironmentConfig + ApiEnvironmentConfig, + MsApiAssertions }, props: { api: { type: Object }, + loaded: Boolean, currentProject: {}, }, data() { return { grades: [], environments: [], + environment: {}, envValue: {}, name: "", priorityValue: "", @@ -177,6 +196,7 @@ selectedEvent: Object, priority: PRIORITY, apiCaseList: [], + loading: false, } }, @@ -191,6 +211,7 @@ }, created() { this.getApiTest(); + this.getEnvironments(); }, methods: { getResult(data) { @@ -202,11 +223,58 @@ return '执行结果:未执行'; } }, + showInput(row) { + row.type = "create"; + row.active = true; + this.active(row); + }, apiCaseClose() { this.apiCaseList = []; this.$emit('apiCaseClose'); }, - runCase() { + getReport(id) { + let url = "/api/delimit/report/get/" + id + "/run"; + this.$get(url, response => { + if (response.data) { + this.loading = false; + this.$success(this.$t('schedule.event_success')); + this.$emit('refresh'); + } else { + setTimeout(this.getReport, 2000) + } + }); + }, + runCase(row) { + if (!this.environment) { + this.$warning(this.$t('api_test.environment.select_environment')); + return; + } + row.request = row.test.request; + let url = "/api/delimit/run"; + let bodyFiles = this.getBodyUploadFiles(row); + let env = this.environment.config.httpConfig.socket ? (this.environment.config.httpConfig.protocol + '://' + this.environment.config.httpConfig.socket) : ''; + if (env.endsWith("/")) { + env = env.substr(0, env.length - 1); + } + let sendUrl = this.api.url; + if (!sendUrl.startsWith("/")) { + sendUrl = "/" + sendUrl; + } + row.test.request.url = env + sendUrl; + row.test.request.path = this.api.path; + row.test.request.name = row.id; + row.test.request.connectTimeout = "6000"; + row.test.request.responseTimeout = "0"; + row.reportId = "run"; + this.loading = true; + let jmx = row.test.toJMX(); + let blob = new Blob([jmx.xml], {type: "application/octet-stream"}); + let file = new File([blob], jmx.name); + this.$fileUpload(url, file, bodyFiles, row, response => { + this.getReport(row.id); + }, erro => { + this.loading = false; + }); }, deleteCase(index, row) { this.$get('/api/testcase/delete/' + row.id, () => { @@ -222,19 +290,24 @@ active: false, test: data.test, }; - this.apiCaseList.push(obj); + this.apiCaseList.unshift(obj); }, - createCase() { - let test = new Test(); + createCase(row) { let obj = { - id: this.apiCaseList.size + 1, name: '', priority: 'P0', type: 'create', active: false, - test: test, }; - this.apiCaseList.push(obj); + let request = {}; + if (row) { + request = row.request; + obj.apiDelimitId = row.apiDelimitId; + } else { + request: new RequestFactory(JSON.parse(this.api.request)) + } + obj.test = new Test({request: request}); + this.apiCaseList.unshift(obj); }, active(item) { item.active = !item.active; @@ -289,7 +362,7 @@ row.test.request.url = this.api.url; row.test.request.path = this.api.path; row.projectId = this.api.projectId; - row.apiDelimitId = this.api.id; + row.apiDelimitId = row.apiDelimitId || this.api.id; row.request = row.test.request; let jmx = row.test.toJMX(); let blob = new Blob([jmx.xml], {type: "application/octet-stream"}); @@ -313,19 +386,17 @@ let hasEnvironment = false; for (let i in this.environments) { if (this.environments[i].id === this.api.environmentId) { - this.api.environment = this.environments[i]; + this.environment = this.environments[i]; hasEnvironment = true; break; } } if (!hasEnvironment) { - this.api.environmentId = ''; - this.api.environment = undefined; + this.environment = undefined; } }); } else { - this.api.environmentId = ''; - this.api.environment = undefined; + this.environment = undefined; } }, openEnvironmentConfig() { @@ -338,7 +409,7 @@ environmentChange(value) { for (let i in this.environments) { if (this.environments[i].id === value) { - this.api.environment = this.environments[i]; + this.environment = this.environments[i]; break; } } @@ -347,7 +418,7 @@ this.getEnvironments(); }, selectTestCase(item, $event) { - if (item.type === "create") { + if (item.type === "create" || !this.loaded) { return; } if ($event.currentTarget.className.indexOf('is-selected') > 0) { @@ -413,6 +484,19 @@ transform: rotate(90deg); } + .tip { + padding: 3px 5px; + font-size: 16px; + border-radius: 4px; + border-left: 4px solid #783887; + margin: 20px 0; + } + + .environment-button { + margin-left: 20px; + padding: 7px; + } + .is-selected { background: #EFF7FF; } diff --git a/frontend/src/business/components/api/delimit/components/ApiConfig.vue b/frontend/src/business/components/api/delimit/components/ApiConfig.vue index 96d334e3f6..4187c85072 100644 --- a/frontend/src/business/components/api/delimit/components/ApiConfig.vue +++ b/frontend/src/business/components/api/delimit/components/ApiConfig.vue @@ -10,7 +10,7 @@ + + diff --git a/frontend/src/business/components/api/delimit/components/body/ApiBody.vue b/frontend/src/business/components/api/delimit/components/body/ApiBody.vue index 1c4b58fb03..460d92c5c2 100644 --- a/frontend/src/business/components/api/delimit/components/body/ApiBody.vue +++ b/frontend/src/business/components/api/delimit/components/body/ApiBody.vue @@ -1,66 +1,87 @@ diff --git a/frontend/src/business/components/api/delimit/components/body/ApiJsonVariable.vue b/frontend/src/business/components/api/delimit/components/body/ApiJsonVariable.vue new file mode 100644 index 0000000000..ae349f2238 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/body/ApiJsonVariable.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/complete/AddCompleteHttpApi.vue b/frontend/src/business/components/api/delimit/components/complete/AddCompleteHttpApi.vue index d49d83f183..b19a08dd72 100644 --- a/frontend/src/business/components/api/delimit/components/complete/AddCompleteHttpApi.vue +++ b/frontend/src/business/components/api/delimit/components/complete/AddCompleteHttpApi.vue @@ -9,8 +9,7 @@ {{$t('commons.test')}}

-
{{$t('test_track.plan_view.base_info')}}
-
+

{{$t('test_track.plan_view.base_info')}}

@@ -59,23 +58,21 @@ :rows="2" size="small"/> - -
{{$t('api_test.delimit.request.req_param')}}
-
- - +

{{$t('api_test.delimit.request.req_param')}}

+ -
{{$t('api_test.delimit.request.res_param')}}
-
- + + +

{{$t('api_test.delimit.request.res_param')}}

+
diff --git a/frontend/src/business/components/api/delimit/components/response/RequestMetric.vue b/frontend/src/business/components/api/delimit/components/response/RequestMetric.vue new file mode 100644 index 0000000000..0961c1f632 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/response/RequestMetric.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/response/RequestResultTail.vue b/frontend/src/business/components/api/delimit/components/response/RequestResultTail.vue new file mode 100644 index 0000000000..7ccf5d502f --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/response/RequestResultTail.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/response/ResponseResult.vue b/frontend/src/business/components/api/delimit/components/response/ResponseResult.vue new file mode 100644 index 0000000000..e05c162826 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/response/ResponseResult.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/response/ResponseText.vue b/frontend/src/business/components/api/delimit/components/response/ResponseText.vue index d949f0d665..84192acfaa 100644 --- a/frontend/src/business/components/api/delimit/components/response/ResponseText.vue +++ b/frontend/src/business/components/api/delimit/components/response/ResponseText.vue @@ -1,33 +1,33 @@ @@ -35,7 +35,10 @@ import MsAssertionResults from "./AssertionResults"; import MsCodeEdit from "../../../../common/components/MsCodeEdit"; import MsDropdown from "../../../../common/components/MsDropdown"; - import {BODY_FORMAT} from "../../model/ScenarioModel"; + import {BODY_FORMAT} from "../../model/ApiTestModel"; + import MsApiKeyValue from "../ApiKeyValue"; + import {REQUEST_HEADERS} from "@/common/js/constants"; + import MsApiBody from "../body/ApiBody"; export default { name: "MsResponseText", @@ -44,6 +47,8 @@ MsDropdown, MsCodeEdit, MsAssertionResults, + MsApiKeyValue, + MsApiBody, }, props: { @@ -53,9 +58,11 @@ data() { return { isActive: true, - activeName: "body", + activeName: "headers", modes: ['text', 'json', 'xml', 'html'], - mode: BODY_FORMAT.TEXT + mode: BODY_FORMAT.TEXT, + headerSuggestions: REQUEST_HEADERS + } }, @@ -97,13 +104,13 @@ } .text-container .pane { - background-color: #F5F5F5; + background-color: white; padding: 0 10px; height: 250px; overflow-y: auto; } - .text-container .pane.assertions { + .text-container .pane.cookie { padding: 0; } diff --git a/frontend/src/business/components/api/delimit/components/runtest/RunTestHttpPage.vue b/frontend/src/business/components/api/delimit/components/runtest/RunTestHttpPage.vue index 8ec297a92a..55f55a3486 100644 --- a/frontend/src/business/components/api/delimit/components/runtest/RunTestHttpPage.vue +++ b/frontend/src/business/components/api/delimit/components/runtest/RunTestHttpPage.vue @@ -1,22 +1,46 @@ diff --git a/frontend/src/business/components/api/delimit/model/ScenarioModel.js b/frontend/src/business/components/api/delimit/model/ApiTestModel.js similarity index 95% rename from frontend/src/business/components/api/delimit/model/ScenarioModel.js rename to frontend/src/business/components/api/delimit/model/ApiTestModel.js index 9e1ce63a07..e46d023586 100644 --- a/frontend/src/business/components/api/delimit/model/ScenarioModel.js +++ b/frontend/src/business/components/api/delimit/model/ApiTestModel.js @@ -81,9 +81,10 @@ export const BODY_TYPE = { KV: "KeyValue", FORM_DATA: "Form Data", RAW: "Raw", - WWW_FORM: "WWW_Form", + WWW_FORM: "WWW_FORM", XML: "XML", - BINARY: "BINARY" + BINARY: "BINARY", + JSON: "JSON" } export const BODY_FORMAT = { @@ -295,6 +296,29 @@ export class RequestFactory { } } +export class ResponseFactory { + static TYPES = { + HTTP: "HTTP", + DUBBO: "DUBBO", + SQL: "SQL", + TCP: "TCP", + } + + constructor(options = {}) { + options.type = options.type || ResponseFactory.TYPES.HTTP + switch (options.type) { + case RequestFactory.TYPES.DUBBO: + return new DubboRequest(options); + case RequestFactory.TYPES.SQL: + return new SqlRequest(options); + case RequestFactory.TYPES.TCP: + return new TCPRequest(options); + default: + return new HttpResponse(options); + } + } +} + export class Request extends BaseConfig { constructor(type, options = {}) { super(); @@ -388,6 +412,32 @@ export class HttpRequest extends Request { } + +export class Response extends BaseConfig { + constructor(type, options = {}) { + super(); + this.type = type; + this.id = options.id || uuid(); + this.name = options.name; + this.enable = options.enable === undefined ? true : options.enable; + this.assertions = new Assertions(options.assertions); + this.extract = new Extract(options.extract); + this.jsr223PreProcessor = new JSR223Processor(options.jsr223PreProcessor); + this.jsr223PostProcessor = new JSR223Processor(options.jsr223PostProcessor); + } +} + + +export class HttpResponse extends Response { + constructor(options) { + super(ResponseFactory.TYPES.HTTP, options); + this.headers = []; + this.body = new Body(options.body); + this.statusCode = []; + this.sets({statusCode: KeyValue,headers: KeyValue}, options); + } +} + export class DubboRequest extends Request { static PROTOCOLS = { DUBBO: "dubbo://", @@ -658,9 +708,12 @@ export class Body extends BaseConfig { this.type = undefined; this.raw = undefined; this.kvs = []; - + this.fromUrlencoded = []; + this.binary = []; + this.xml = undefined; + this.json = undefined; this.set(options); - this.sets({kvs: KeyValue}, options); + this.sets({kvs: KeyValue},{fromUrlencoded: KeyValue},{binary: KeyValue}, options); } isValid() { diff --git a/frontend/src/business/components/api/delimit/model/EnvironmentModel.js b/frontend/src/business/components/api/delimit/model/EnvironmentModel.js index 1b0c414805..2be5d57666 100644 --- a/frontend/src/business/components/api/delimit/model/EnvironmentModel.js +++ b/frontend/src/business/components/api/delimit/model/EnvironmentModel.js @@ -1,4 +1,4 @@ -import {BaseConfig, DatabaseConfig, KeyValue} from "./ScenarioModel"; +import {BaseConfig, DatabaseConfig, KeyValue} from "./ApiTestModel"; import {TCPConfig} from "@/business/components/api/test/model/ScenarioModel"; export class Environment extends BaseConfig { diff --git a/frontend/src/business/components/api/delimit/model/JsonData.js b/frontend/src/business/components/api/delimit/model/JsonData.js index d335ee2249..fb84802e3b 100644 --- a/frontend/src/business/components/api/delimit/model/JsonData.js +++ b/frontend/src/business/components/api/delimit/model/JsonData.js @@ -37,3 +37,8 @@ export const API_METHOD_COLOUR = [ ['HEAD', '#8E58E7'], ['CONNECT', '#90AFAE'], ['DUBBO', '#C36EEF'], ['SQL', '#0AEAD4'], ['TCP', '#0A52DF'], ] + +export const REQUIRED = [ + {name: '必填', id: true}, + {name: '非必填', id: false} +] diff --git a/frontend/src/business/components/api/report/components/ResponseText.vue b/frontend/src/business/components/api/report/components/ResponseText.vue index 764afdf31f..3c6e4cf331 100644 --- a/frontend/src/business/components/api/report/components/ResponseText.vue +++ b/frontend/src/business/components/api/report/components/ResponseText.vue @@ -1,5 +1,10 @@ diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index eb33afbb1d..0f84aee5d8 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -480,6 +480,7 @@ export default { case: "Case", title: "Create api", path_info: "Please enter the URL of the interface, such as /api/demo/#{id}, where id is the path parameter", + path_all_info: "Please enter the complete test address", fast_debug: "Fast debug", close_all_label: "close all label", save_as: "Save as new interface", @@ -499,9 +500,14 @@ export default { verified: "verified", encryption: "encryption", req_param: "Request parameter", - res_param: "Response template", + res_param: "Response content", batch_delete: "Batch deletion", - delete_confirm: "confirm deletion", + delete_confirm: "Confirm deletion", + assertions_rule: "Assertion rule", + response_header: "Response header", + response_body: "Response body", + console: "Console", + status_code: "Status code", } }, environment: { diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index da78891e14..dab894de2c 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -481,6 +481,7 @@ export default { responsible: "责任人", title: "创建接口", path_info: "请输入接口的URL,如/api/demo/#{id},其中id为路径参数", + path_all_info: "请输入完整测试地址", fast_debug: "快捷调试", close_all_label: "关闭所有标签", save_as: "另存为新接口", @@ -500,9 +501,15 @@ export default { verified: "认证", encryption: "加密", req_param: "请求参数", - res_param: "响应模版", + res_param: "响应内容", batch_delete: "批量删除", delete_confirm: "确认删除接口", + assertions_rule: "断言规则", + response_header: "响应头", + response_body: "响应体", + console: "控制台", + status_code: "状态码", + } }, environment: { diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index 0014e1aca6..cba80a8cfd 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -481,6 +481,7 @@ export default { responsible: "责任人", title: "创建接口", path_info: "請輸入接口的URL,如/api/demo/#{id},其中id為路徑參數", + path_all_info: "請輸入完整測試地址", fast_debug: "快捷調試", close_all_label: "關閉所有標簽", save_as: "另存為新接口", @@ -500,9 +501,15 @@ export default { verified: "認證", encryption: "加密", req_param: "請求參數", - res_param: "響應模版", + res_param: "響應内容", batch_delete: "批量删除", delete_confirm: "確認刪除接口", + assertions_rule: "斷言規則", + response_header: "響應頭", + response_body: "響應體", + console: "控制臺", + status_code: "狀態碼", + } }, environment: {