From b6599fec96d82bfbbbae0f04002a487b3025a9f6 Mon Sep 17 00:00:00 2001 From: AgAngle <1323481023@qq.com> Date: Wed, 8 Nov 2023 15:10:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A3=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=8D=8F=E8=AE=AE=E5=88=97=E8=A1=A8=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/spi/AbstractProtocolPlugin.java | 2 +- .../sdk/dto/api/request/http/QueryParam.java | 12 +++++ .../sdk/dto/api/request/http/RestParam.java | 12 +++++ .../api/controller/ApiTestController.java | 32 ++++++++++++ .../api/service/ApiTestService.java | 26 ++++++++++ .../controller/ApiTestControllerTests.java | 32 ++++++++++++ .../test/resources/file/jdbc-sampler-v3.x.jar | Bin 5949 -> 5989 bytes .../metersphere/system/dto/ProtocolDTO.java | 9 +++- .../system/service/ApiPluginService.java | 44 ++++++++++++++-- .../system/service/ApiPluginServiceTests.java | 48 ++++++++++++++++++ .../test/resources/file/jdbc-sampler-v3.x.jar | Bin 0 -> 5989 bytes 11 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/controller/ApiTestController.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/service/ApiTestService.java create mode 100644 backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiTestControllerTests.java create mode 100644 backend/services/system-setting/src/test/java/io/metersphere/system/service/ApiPluginServiceTests.java create mode 100644 backend/services/system-setting/src/test/resources/file/jdbc-sampler-v3.x.jar diff --git a/backend/framework/plugin/plugin-api-sdk/src/main/java/io/metersphere/plugin/api/spi/AbstractProtocolPlugin.java b/backend/framework/plugin/plugin-api-sdk/src/main/java/io/metersphere/plugin/api/spi/AbstractProtocolPlugin.java index 793e98a529..2614596d53 100644 --- a/backend/framework/plugin/plugin-api-sdk/src/main/java/io/metersphere/plugin/api/spi/AbstractProtocolPlugin.java +++ b/backend/framework/plugin/plugin-api-sdk/src/main/java/io/metersphere/plugin/api/spi/AbstractProtocolPlugin.java @@ -8,7 +8,7 @@ package io.metersphere.plugin.api.spi; public abstract class AbstractProtocolPlugin extends AbstractApiPlugin { /** - * 返回协议名称 + * 返回协议名 * @return */ abstract public String getProtocol(); diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/QueryParam.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/QueryParam.java index 7b68be2979..419f4853aa 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/QueryParam.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/QueryParam.java @@ -19,4 +19,16 @@ public class QueryParam extends KeyValueParam { * 是否必填 */ private Boolean required = false; + /** + * 最大长度 + */ + private Integer minLength; + /** + * 最小长度 + */ + private Integer maxLength; + /** + * 是否编码 + */ + private Boolean encode = false; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/RestParam.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/RestParam.java index 6aab61368e..ec5bc309ab 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/RestParam.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/api/request/http/RestParam.java @@ -18,4 +18,16 @@ public class RestParam extends KeyValueParam { * 是否必填 */ private Boolean required = false; + /** + * 最大长度 + */ + private Integer minLength; + /** + * 最小长度 + */ + private Integer maxLength; + /** + * 是否编码 + */ + private Boolean encode = false; } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/controller/ApiTestController.java b/backend/services/api-test/src/main/java/io/metersphere/api/controller/ApiTestController.java new file mode 100644 index 0000000000..99ea236386 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/controller/ApiTestController.java @@ -0,0 +1,32 @@ +package io.metersphere.api.controller; + +import io.metersphere.api.service.ApiTestService; +import io.metersphere.system.dto.ProtocolDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * @Author: jianxing + * @CreateTime: 2023-11-06 10:54 + */ +@RestController +@RequestMapping("/api/test") +@Tag(name = "接口测试") +public class ApiTestController { + + @Resource + private ApiTestService apiTestService; + + @GetMapping("/protocol/{organizationId}") + @Operation(summary = "获取协议插件的的协议列表") + public List getProtocols(@PathVariable String organizationId) { + return apiTestService.getProtocols(organizationId); + } +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/ApiTestService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/ApiTestService.java new file mode 100644 index 0000000000..5dccb008c3 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/ApiTestService.java @@ -0,0 +1,26 @@ +package io.metersphere.api.service; + +import io.metersphere.system.dto.ProtocolDTO; +import io.metersphere.system.service.ApiPluginService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author jianxing + * @date : 2023-11-6 + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class ApiTestService { + + @Resource + private ApiPluginService apiPluginService; + + public List getProtocols(String orgId) { + return apiPluginService.getProtocols(orgId); + } + +} \ No newline at end of file diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiTestControllerTests.java b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiTestControllerTests.java new file mode 100644 index 0000000000..c1dfd3c4ff --- /dev/null +++ b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiTestControllerTests.java @@ -0,0 +1,32 @@ +package io.metersphere.api.controller; + +import io.metersphere.system.base.BaseTest; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * @Author: jianxing + * @CreateTime: 2023-11-07 17:07 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@AutoConfigureMockMvc +public class ApiTestControllerTests extends BaseTest { + + private static final String BASE_PATH = "/api/test/"; + protected static final String PROTOCOL_LIST = "protocol/{0}"; + + @Override + protected String getBasePath() { + return BASE_PATH; + } + + @Test + public void getProtocols() throws Exception { + // @@请求成功 + this.requestGetWithOk(PROTOCOL_LIST, this.DEFAULT_ORGANIZATION_ID).andReturn(); + } +} diff --git a/backend/services/api-test/src/test/resources/file/jdbc-sampler-v3.x.jar b/backend/services/api-test/src/test/resources/file/jdbc-sampler-v3.x.jar index 96819f90c61bd9e4fb7478e6a24830258f9d262b..b176f2bf1cd7df4983687b82539ed772c7371f49 100644 GIT binary patch delta 988 zcmdn1_f(HJz?+$civa{~7iCQ3Rbjpbq)eVL19_9B8RfvV4WkyA&SW$M({mZk!1Ogn zYY@Fzgh_{y7sv>I=EAk3osogz++<&7%le&uhaCjk{`dOkDYX37dcnYW`$FRcCkqxM z&m)40QdYYwi41JF;)al-9J*mwOXlT_IMUop1Rq& zZPQnUc`W5Ex1Why1>{w?CvJJQbD`L}Jg(*1p62IXPyd>A*GpmZtGj`fcY@ZfDYe#> zjOm)Dyf|~7?wn;DAxkDNTKPWo#D>Jb^|iMY7az)wztLa+@%#M=dET+Yva`&$?lQ}H z$s}3#Ptp0`q1SBNY=SPbJ+7a;T}ep$#0BZbb2e^5!84YfF=)QYa?j(zl#ezJ(|(<}qHpPb`pb#4EheXS zPG>CH#cx`7u~ji6Q9v|SqD$}Cx~2KBDCPh~@#c>#7A)Xc)8U*3W=uZHTLF$v2R=wN z=ki&C6|CgzX5s`zYwx+t)rFI71zqZc%lyxJ>w4*U`UZNe3i;rx<9+t=(;z|pyS)_-jr>AnKP`8)SWueL5Ho-ZYgC3Wn^a;59aJu#^cX=!!ya%XOgW7QXD2=HcP5@CiVP{xc&LMEUT zI@w&v9_;2yAw4kND=Inpu#h5{{anZqOlu3ff$0)qZ!mpGI0`M5YKjDcWokvFnYdXe zPZU;~9Lvc&S)Y|{@_7*+u+%-Elp7~lN>)@u9uypgi40zDzzpL8#Nwcw0|iSO?@vw< zRRe456_sX;n!HjJl7UW%N;9?dO=jd*n0!NsM*(CAOcRLKhI0fM{{KG=Qm8vQK}?H@ qhabo^nS4MDq6ZugK()m{Yzf!Hz_6q-P!OolaI&np6q}?FNCp5jz9$OGZ_uR^jtD6gkiV#) z=-Bzu>C`2M!kc&Bh|NFMJkQ7G2ZP;|!m^tRiZV7O%Z&g2Ph0l(`}ybM4QyJGM-r9@ zoL6jAVGie;@1<*dU)I~d==!YQDQ8<(@9Wf^@fssKI@jPJDRV5Zg#Ovua2{gMWP1V%ev)X3u-UF+b&%`ooDGampvSLoYhBm z-wOWrqDAq3NlrX>^!FCi$t~t{6?Zy$-`r$7n{P?EKHD^&jN>9<{uNpsi&J9yr=Bmk z@$3JDE5=LzxQEX;|4?_nhMebJvEWtV>#kkno%z`p35|}`zc@C{p7QXC5@*Ca?aKxqaK_YYtpCwqq1-@=3L69L9B@rwIK+VNKY&rQJ zpWftIf_(M7CpK~&G7w<6@b|&LhYL2Vs`<70H9fkZ_lx=Zvnw6GhfLZ2R`0frE1srt z%GJi#UTTMzONO6RhR@WV#eWwXFDstbcU?L%S(YiAvwYs^qu=%I&+eW&{g0SyMaHiu zxz1VQnv1gQy11O~N(8-GoxPu4E7>LRMg! zU)T{$2MfD_={dsQVEUDC6k4(j6bV$o7Fa;i?hzc3$CFdj-7b6@4 z3Fk!Vj=A5S3=!F}X)nO&(+;l1c`CI7fit|9|7j z7e(bT)X1TzNtnzh1~Ju6Oqyv9-{kvz`jflFG?<=(n7Wf6iV1`Lg%W>o6F`pB5(Fy* PM(1RIaRIiEf*@G{fOKJK diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ProtocolDTO.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ProtocolDTO.java index f8b72b6d76..93c76585fd 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ProtocolDTO.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ProtocolDTO.java @@ -8,6 +8,13 @@ import lombok.Data; */ @Data public class ProtocolDTO { + /** + * 协议名 + */ private String protocol; - private String name; + /** + * 协议对应的组件名 + * 例如 Http 对应 MsHTTPElement + */ + private String polymorphicName; } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/ApiPluginService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/ApiPluginService.java index 875fad2372..8e57f2d3ab 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/ApiPluginService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/ApiPluginService.java @@ -1,13 +1,20 @@ package io.metersphere.system.service; import io.metersphere.plugin.api.spi.AbstractProtocolPlugin; +import io.metersphere.plugin.api.spi.MsTestElement; import io.metersphere.sdk.constants.PluginScenarioType; +import io.metersphere.sdk.dto.api.request.http.MsHTTPElement; +import io.metersphere.sdk.util.LogUtils; import io.metersphere.system.domain.Plugin; +import io.metersphere.system.dto.ProtocolDTO; import jakarta.annotation.Resource; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.pf4j.PluginWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -20,23 +27,50 @@ public class ApiPluginService { private PluginLoadService pluginLoadService; @Resource private BasePluginService basePluginService; + private static final String HTTP_PROTOCOL = "HTTP"; /** * 获取协议插件的的协议列表 * @param orgId * @return */ - public List getProtocols(String orgId) { + public List getProtocols(String orgId) { // 查询组织下有权限的插件 Set pluginIds = basePluginService.getOrgEnabledPlugins(orgId, PluginScenarioType.API_PROTOCOL) .stream() .map(Plugin::getId) .collect(Collectors.toSet()); + // 过滤协议插件 List plugins = pluginLoadService.getMsPluginManager().getPlugins(); - return plugins.stream() - .filter(plugin -> pluginIds.contains(plugin.getPluginId()) && plugin.getPlugin() instanceof AbstractProtocolPlugin) - .map(plugin -> ((AbstractProtocolPlugin) plugin.getPlugin()).getProtocol()) - .collect(Collectors.toList()); + List pluginWrappers = plugins.stream() + .filter(plugin -> pluginIds.contains(plugin.getPluginId()) && plugin.getPlugin() instanceof AbstractProtocolPlugin).toList(); + + List protocols = new ArrayList(); + pluginWrappers.forEach(pluginWrapper -> { + try { + // 获取插件中 MsTestElement 的实现类 + List> extensionClasses = pluginWrapper.getPluginManager() + .getExtensionClasses(MsTestElement.class, pluginWrapper.getPluginId()); + + AbstractProtocolPlugin protocolPlugin = ((AbstractProtocolPlugin) pluginWrapper.getPlugin()); + ProtocolDTO protocolDTO = new ProtocolDTO(); + protocolDTO.setProtocol(protocolPlugin.getProtocol()); + if (CollectionUtils.isNotEmpty(extensionClasses)) { + protocolDTO.setPolymorphicName(extensionClasses.get(0).getSimpleName()); + } + if (StringUtils.isNoneBlank(protocolDTO.getProtocol(), protocolDTO.getPolymorphicName())) { + protocols.add(protocolDTO); + } + } catch (Exception e) { + LogUtils.error(e); + } + }); + // 将 http 协议放最前面 + ProtocolDTO protocolDTO = new ProtocolDTO(); + protocolDTO.setProtocol(HTTP_PROTOCOL); + protocolDTO.setPolymorphicName(MsHTTPElement.class.getSimpleName()); + protocols.addFirst(protocolDTO); + return protocols; } } diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/service/ApiPluginServiceTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/service/ApiPluginServiceTests.java new file mode 100644 index 0000000000..242a16327e --- /dev/null +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/service/ApiPluginServiceTests.java @@ -0,0 +1,48 @@ +package io.metersphere.system.service; + +import io.metersphere.sdk.dto.api.request.http.MsHTTPElement; +import io.metersphere.system.base.BaseApiPluginTestService; +import io.metersphere.system.base.BaseTest; +import io.metersphere.system.dto.ProtocolDTO; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: jianxing + * @CreateTime: 2023-11-08 16:34 + */ +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ApiPluginServiceTests extends BaseTest { + @Resource + private BaseApiPluginTestService baseApiPluginTestService; + @Resource + private ApiPluginService apiPluginService; + + @Test + public void getProtocols() throws Exception { + baseApiPluginTestService.addJdbcPlugin(); + + // 校验数据是否正确 + List protocols = apiPluginService.getProtocols(this.DEFAULT_ORGANIZATION_ID); + List expected = new ArrayList(); + ProtocolDTO httpProtocol = new ProtocolDTO(); + httpProtocol.setProtocol("HTTP"); + httpProtocol.setPolymorphicName(MsHTTPElement.class.getSimpleName()); + ProtocolDTO jdbcProtocol = new ProtocolDTO(); + jdbcProtocol.setProtocol("JDBC"); + jdbcProtocol.setPolymorphicName("MsJDBCElement"); + expected.add(httpProtocol); + expected.add(jdbcProtocol); + Assertions.assertEquals(protocols, expected); + } +} diff --git a/backend/services/system-setting/src/test/resources/file/jdbc-sampler-v3.x.jar b/backend/services/system-setting/src/test/resources/file/jdbc-sampler-v3.x.jar new file mode 100644 index 0000000000000000000000000000000000000000..b176f2bf1cd7df4983687b82539ed772c7371f49 GIT binary patch literal 5989 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