diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/pom.xml b/backend/framework/plugin/metersphere-platform-plugin-sdk/pom.xml index 1ac4dd98f4..c178185fff 100644 --- a/backend/framework/plugin/metersphere-platform-plugin-sdk/pom.xml +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/pom.xml @@ -18,6 +18,20 @@ metersphere-plugin-sdk ${revision} + + + + org.springframework + spring-web + + + org.apache.httpcomponents.client5 + httpclient5 + + + commons-codec + commons-codec + diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatform.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatform.java new file mode 100644 index 0000000000..d62cef52a0 --- /dev/null +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatform.java @@ -0,0 +1,25 @@ +package io.metersphere.plugin.platform.api; + +import io.metersphere.plugin.platform.dto.PlatformRequest; +import io.metersphere.plugin.sdk.util.JSON; +import io.metersphere.plugin.sdk.util.MSPluginException; +import org.apache.commons.lang3.StringUtils; + +public abstract class AbstractPlatform implements Platform { + protected PlatformRequest request; + + public AbstractPlatform(PlatformRequest request) { + this.request = request; + } + + public T getIntegrationConfig(String integrationConfig, Class clazz) { + if (StringUtils.isBlank(integrationConfig)) { + throw new MSPluginException("服务集成配置为空"); + } + return JSON.parseObject(integrationConfig, clazz); + } + + public String getPluginId() { + return request.getPluginId(); + } +} diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java index dcead1c8d8..9dfa0c244b 100644 --- a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java @@ -3,9 +3,48 @@ package io.metersphere.plugin.platform.api; import io.metersphere.plugin.sdk.api.AbstractMsPlugin; public abstract class AbstractPlatformPlugin extends AbstractMsPlugin { - private static final String PLATFORM_PLUGIN_TYPE = "PLATFORM"; + private static final String DEFAULT_PLATFORM_PLUGIN_TYPE = "PLATFORM"; + private static final String DEFAULT_INTEGRATION_SCRIPT_ID = "integration"; + private static final String DEFAULT_PROJECT_SCRIPT_ID = "project"; + private static final String DEFAULT_ACCOUNT_SCRIPT_ID = "account"; @Override public String getType() { - return PLATFORM_PLUGIN_TYPE; + return DEFAULT_PLATFORM_PLUGIN_TYPE; + } + + /** + * 返回插件的描述信息 + * @return + */ + public abstract String getDescription(); + + /** + * 返回插件的logo路径,比如:/static/jira.png + * @return + */ + public abstract String getLogo(); + + /** + * 返回服务集成脚本的ID,默认为 integration + * @return + */ + public String getIntegrationScriptId() { + return DEFAULT_INTEGRATION_SCRIPT_ID; + } + + /** + * 返回项目配置脚本的ID,默认为 project + * @return + */ + public String getProjectScriptId() { + return DEFAULT_PROJECT_SCRIPT_ID; + } + + /** + * 返回个人账号脚本的ID,默认为 account + * @return + */ + public String getAccountScriptId() { + return DEFAULT_ACCOUNT_SCRIPT_ID; } } diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/BaseClient.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/BaseClient.java new file mode 100644 index 0000000000..a1d652bddd --- /dev/null +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/BaseClient.java @@ -0,0 +1,108 @@ +package io.metersphere.plugin.platform.api; + +import io.metersphere.plugin.platform.utils.EnvProxySelector; +import io.metersphere.plugin.platform.utils.PluginCodingUtils; +import io.metersphere.plugin.sdk.util.JSON; +import io.metersphere.plugin.sdk.util.MSPluginException; +import io.metersphere.plugin.sdk.util.PluginLogUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.ssl.TrustStrategy; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import javax.net.ssl.SSLContext; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +public abstract class BaseClient { + + protected RestTemplate restTemplate; + + public BaseClient() { + try { + TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true; + + SSLContext sslContext = SSLContexts.custom() + .loadTrustMaterial(null, acceptingTrustStrategy) + .build(); + + SSLConnectionSocketFactory csf = SSLConnectionSocketFactoryBuilder + .create() + .setSslContext(sslContext) + .build(); + + CloseableHttpClient httpClient = HttpClients.custom() + // 可以支持设置系统代理 + .setRoutePlanner(new SystemDefaultRoutePlanner(new EnvProxySelector())) + // 忽略 https + .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(csf) + .build()) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + + restTemplate = new RestTemplate(requestFactory); + } catch (Exception e) { + PluginLogUtils.error(e); + } + } + + protected HttpHeaders getBasicHttpHeaders(String userName, String passWd) { + String authKey = PluginCodingUtils.base64Encoding(userName + ":" + passWd); + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth(authKey); + headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + return headers; + } + + protected HttpHeaders getBearHttpHeaders(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + return headers; + } + + protected String getResult(ResponseEntity response) { + int statusCodeValue = response.getStatusCodeValue(); + PluginLogUtils.info("responseCode: " + statusCodeValue); + if (statusCodeValue >= 400) { + throw new MSPluginException(response.getBody()); + } + PluginLogUtils.info("result: " + response.getBody()); + return response.getBody(); + } + + protected Object getResultForList(Class clazz, ResponseEntity response) { + return Arrays.asList(JSON.parseArray(getResult(response), clazz).toArray()); + } + + protected Object getResultForObject(Class clazz, ResponseEntity response) { + return JSON.parseObject(getResult(response), clazz); + } + + public void validateProxyUrl(String url, String... path) { + try { + if (!StringUtils.containsAny(new URI(url).getPath(), path)) { + // 只允许访问图片 + throw new MSPluginException("illegal path"); + } + } catch (URISyntaxException e) { + PluginLogUtils.error(e); + throw new MSPluginException("illegal path"); + } + } +} diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java index c6dde0a4e5..c570791df1 100644 --- a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java @@ -1,147 +1,14 @@ package io.metersphere.plugin.platform.api; -import io.metersphere.plugin.platform.dto.*; - -import java.util.List; - /** - * 平台对接相关业务 + * 平台对接相关业务接口 * @author jianxing.chen */ public interface Platform { - /** - * 获取平台相关需求 - * 功能用例关联需求时调用 - * @param projectConfig 项目设置表单值 - * @return 需求列表 - */ - List getDemands(String projectConfig); - - /** - * 创建缺陷并封装 MS 返回 - * 创建缺陷时调用 - * @param bugsRequest bugRequest - * @return MS 缺陷 - */ - MsBugDTO addBug(PlatformBugUpdateRequest bugsRequest); - - /** - * 项目设置和缺陷表单中,调用接口获取下拉框选项 - * 配置文件的表单中选项值配置了 optionMethod ,则调用获取表单的选项值 - * @return 返回下拉列表 - */ - List getFormOptions(GetOptionRequest request); - - /** - * 更新缺陷 - * 编辑缺陷时调用 - * @param request - * @return MS 缺陷 - */ - MsBugDTO updateBug(PlatformBugUpdateRequest request); - - /** - * 删除缺陷平台缺陷 - * 删除缺陷时调用 - * @param id 平台的缺陷 ID - */ - void deleteBug(String id); - /** * 校验服务集成配置 * 服务集成点击校验时调用 */ void validateIntegrationConfig(); - - /** - * 校验项目配置 - * 项目设置成点击校验项目 key 时调用 - */ - void validateProjectConfig(String projectConfig); - - /** - * 校验用户配置配置 - * 用户信息,校验第三方信息时调用 - */ - void validateUserConfig(String userConfig); - - /** - * 支持附件上传 - * 编辑缺陷上传附件是会调用判断是否支持附件上传 - * 如果支持会调用 syncBugsAttachment 上传缺陷到第三方平台 - */ - boolean isAttachmentUploadSupport(); - - /** - * 同步缺陷最新变更 - * 开源用户点击同步缺陷时调用 - */ - SyncBugResult syncBugs(SyncBugRequest request); - - /** - * 同步项目下所有的缺陷 - * 企业版用户会调用同步缺陷 - */ - void syncAllBugs(SyncAllBugRequest request); - - /** - * 获取附件内容 - * 同步缺陷中,同步附件时会调用 - * @param fileKey 文件关键字 - */ - byte[] getAttachmentContent(String fileKey); - - /** - * 获取第三方平台缺陷的自定义字段 - * 需要 PluginMetaInfo 的 isThirdPartTemplateSupport 返回 true - * @return - */ - List getThirdPartCustomField(String projectConfig); - - /** - * Get请求的代理 - * 目前使用场景:富文本框中如果有图片是存储在第三方平台,MS 通过 url 访问 - * 这时如果第三方平台需要登入才能访问到静态资源,可以通过将富文本框图片内容构造如下格式访问 - * ![name](/resource/md/get/path?platform=Jira?project_id=&organization_id=&path=) - * @param path - * @return - */ - Object proxyForGet(String path, Class responseEntityClazz); - - /** - * 同步 MS 缺陷附件到第三方平台 - * isAttachmentUploadSupport 返回为 true 时,同步和创建缺陷时会调用 - */ - void syncBugsAttachment(SyncBugAttachmentRequest request); - - /** - * 获取第三方平台的状态列表 - * 缺陷列表和编辑缺陷时会调用 - * @return - */ - List getStatusList(String projectConfig); - - /** - * 获取第三方平台的状态转移列表 - * 即编辑缺陷时的可选状态 - * 默认会调用 getStatusList,可重写覆盖 - * @param bugId - * @return - */ - List getTransitions(String projectConfig, String bugId); - - /** - * 用例关联需求时调用 - * 可在第三方平台添加用例和需求的关联关系 - * @param request - */ - void handleDemandUpdate(DemandUpdateRequest request); - - /** - * 用例批量关联需求时调用 - * 可在第三方平台添加用例和需求的关联关系 - * @param request - */ - void handleDemandUpdateBatch(DemandUpdateRequest request); } diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/dto/PlatformRequest.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/dto/PlatformRequest.java index bebd25c9be..1ec77aaddf 100644 --- a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/dto/PlatformRequest.java +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/dto/PlatformRequest.java @@ -8,5 +8,6 @@ import lombok.Setter; public class PlatformRequest { private String integrationConfig; private String organizationId; - private String userPlatformInfo; + private String userAccountConfig; + private String pluginId; } diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/EnvProxySelector.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/EnvProxySelector.java new file mode 100644 index 0000000000..8d3c75b132 --- /dev/null +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/EnvProxySelector.java @@ -0,0 +1,45 @@ +package io.metersphere.plugin.platform.utils; + +import io.metersphere.plugin.sdk.util.PluginLogUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.net.*; +import java.util.Arrays; +import java.util.List; + +public class EnvProxySelector extends ProxySelector { + ProxySelector defaultProxySelector = ProxySelector.getDefault(); + + @Override + public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + PluginLogUtils.error("connectFailed : " + uri); + } + + /** + * 获取环境变量http代理配置, + * + * @param uri + * @return + */ + @Override + public List select(URI uri) { + String httpProxy = System.getenv("http_proxy"); + String httpsProxy = System.getenv("https_proxy"); + URI proxy = null; + try { + if (StringUtils.equalsIgnoreCase(uri.getScheme(), "https") + && StringUtils.isNotBlank(httpsProxy)) { + proxy = new URI(httpsProxy); + } else if (StringUtils.isNotBlank(httpProxy)) { + proxy = new URI(httpProxy); + } + if (proxy != null) { + return Arrays.asList(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxy.getHost(), proxy.getPort()))); + } + } catch (URISyntaxException e) { + PluginLogUtils.error(e); + } + return defaultProxySelector.select(uri); + } +} diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/PluginBeanUtils.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/PluginBeanUtils.java new file mode 100644 index 0000000000..8d65a4710f --- /dev/null +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/PluginBeanUtils.java @@ -0,0 +1,69 @@ +package io.metersphere.plugin.platform.utils; + +import io.metersphere.plugin.sdk.util.PluginLogUtils; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Method; + +public class PluginBeanUtils { + + public static T copyBean(T target, Object source) { + try { + org.springframework.beans.BeanUtils.copyProperties(source, target); + return target; + } catch (Exception e) { + throw new RuntimeException("Failed to copy object: ", e); + } + } + + public static T copyBean(T target, Object source, String... ignoreProperties) { + try { + org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProperties); + return target; + } catch (Exception e) { + throw new RuntimeException("Failed to copy object: ", e); + } + } + + public static Object getFieldValueByName(String fieldName, Object bean) { + try { + if (StringUtils.isBlank(fieldName)) { + return null; + } + String firstLetter = fieldName.substring(0, 1).toUpperCase(); + String getter = "get" + firstLetter + fieldName.substring(1); + Method method = bean.getClass().getMethod(getter); + return method.invoke(bean); + } catch (Exception e) { + PluginLogUtils.error("failed to getFieldValueByName. ", e); + return null; + } + } + + public static void setFieldValueByName(Object bean, String fieldName, Object value, Class type) { + try { + if (StringUtils.isBlank(fieldName)) { + return; + } + String firstLetter = fieldName.substring(0, 1).toUpperCase(); + String setter = "set" + firstLetter + fieldName.substring(1); + Method method = bean.getClass().getMethod(setter, type); + method.invoke(bean, value); + } catch (Exception e) { + PluginLogUtils.error("failed to setFieldValueByName. ", e); + } + } + + public static Method getMethod(Object bean, String fieldName, Class type) { + try { + if (StringUtils.isBlank(fieldName)) { + return null; + } + String firstLetter = fieldName.substring(0, 1).toUpperCase(); + String setter = "set" + firstLetter + fieldName.substring(1); + return bean.getClass().getMethod(setter, type); + } catch (Exception e) { + return null; + } + } +} diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/PluginCodingUtils.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/PluginCodingUtils.java new file mode 100644 index 0000000000..1a1758a47d --- /dev/null +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/utils/PluginCodingUtils.java @@ -0,0 +1,168 @@ +package io.metersphere.plugin.platform.utils; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +/** + * 加密解密工具 + * + * @author kun.mo + */ +public class PluginCodingUtils { + + private static final String UTF_8 = StandardCharsets.UTF_8.name(); + + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * MD5加密 + * + * @param src 要加密的串 + * @return 加密后的字符串 + */ + public static String md5(String src) { + return md5(src, UTF_8); + } + + /** + * MD5加密 + * + * @param src 要加密的串 + * @param charset 加密字符集 + * @return 加密后的字符串 + */ + public static String md5(String src, String charset) { + try { + byte[] strTemp = StringUtils.isEmpty(charset) ? src.getBytes() : src.getBytes(charset); + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + mdTemp.update(strTemp); + + byte[] md = mdTemp.digest(); + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + + for (byte byte0 : md) { + str[k++] = HEX_DIGITS[byte0 >>> 4 & 0xf]; + str[k++] = HEX_DIGITS[byte0 & 0xf]; + } + + return new String(str); + } catch (Exception e) { + throw new RuntimeException("MD5 encrypt error:", e); + } + } + + /** + * BASE64解密 + * + * @param src 待解密的字符串 + * @return 解密后的字符串 + */ + public static String base64Decoding(String src) { + byte[] b; + String result = null; + if (src != null) { + try { + b = Base64.decodeBase64(src); + result = new String(b, UTF_8); + } catch (Exception e) { + throw new RuntimeException("BASE64 decoding error:", e); + } + } + return result; + } + + /** + * BASE64加密 + * + * @param src 待加密的字符串 + * @return 加密后的字符串 + */ + public static String base64Encoding(String src) { + String result = null; + if (src != null) { + try { + result = Base64.encodeBase64String(src.getBytes(UTF_8)); + } catch (Exception e) { + throw new RuntimeException("BASE64 encoding error:", e); + } + } + return result; + } + + /** + * AES加密 + * + * @param src 待加密字符串 + * @param secretKey 密钥 + * @param iv 向量 + * @return 加密后字符串 + */ + public static String aesEncrypt(String src, String secretKey, String iv) { + if (StringUtils.isBlank(secretKey)) { + throw new RuntimeException("secretKey is empty"); + } + + try { + byte[] raw = secretKey.getBytes(UTF_8); + SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES"); + // "算法/模式/补码方式" ECB + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + IvParameterSpec iv1 = new IvParameterSpec(iv.getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv1); + byte[] encrypted = cipher.doFinal(src.getBytes(UTF_8)); + return Base64.encodeBase64String(encrypted); + } catch (Exception e) { + throw new RuntimeException("AES encrypt error:", e); + } + + } + + /** + * AES 解密 + * + * @param src 待解密字符串 + * @param secretKey 密钥 + * @param iv 向量 + * @return 解密后字符串 + */ + public static String aesDecrypt(String src, String secretKey, String iv) { + if (StringUtils.isBlank(secretKey)) { + throw new RuntimeException("secretKey is empty"); + } + try { + byte[] raw = secretKey.getBytes(UTF_8); + SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + IvParameterSpec iv1 = new IvParameterSpec(iv.getBytes()); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv1); + byte[] encrypted1 = Base64.decodeBase64(src); + byte[] original = cipher.doFinal(encrypted1); + return new String(original, UTF_8); + } catch (BadPaddingException | IllegalBlockSizeException e) { + // 解密的原字符串为非加密字符串,则直接返回原字符串 + return src; + } catch (Exception e) { + throw new RuntimeException("decrypt error,please check parameters", e); + } + } + + public static String secretKey() { + try { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(128); + SecretKey secretKey = keyGen.generateKey(); + return Base64.encodeBase64String(secretKey.getEncoded()); + } catch (Exception e) { + throw new RuntimeException("generate secretKey error", e); + } + + } +} diff --git a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/util/MSPluginException.java b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/util/MSPluginException.java new file mode 100644 index 0000000000..746979737c --- /dev/null +++ b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/util/MSPluginException.java @@ -0,0 +1,23 @@ +package io.metersphere.plugin.sdk.util; + +/** + * 插件异常类 + * @author jianxing + */ +public class MSPluginException extends RuntimeException { + public MSPluginException() { + super(); + } + + public MSPluginException(String message) { + super(message); + } + + public MSPluginException(String message, Throwable cause) { + super(message, cause); + } + + public MSPluginException(Throwable cause) { + super(cause); + } +} diff --git a/backend/framework/plugin/pom.xml b/backend/framework/plugin/pom.xml index dd574d75a5..7d910b64d5 100644 --- a/backend/framework/plugin/pom.xml +++ b/backend/framework/plugin/pom.xml @@ -8,7 +8,6 @@ io.metersphere framework ${revision} - io.metersphere @@ -25,29 +24,4 @@ metersphere-api-plugin-sdk metersphere-platform-plugin-sdk - - - - org.apache.jmeter - ApacheJMeter_core - ${jmeter.version} - - - org.apache.jmeter - jorphan - ${jmeter.version} - - - org.projectlombok - lombok - - - org.apache.commons - commons-lang3 - - - com.fasterxml.jackson.core - jackson-annotations - - \ No newline at end of file diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/RestControllerExceptionHandler.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/RestControllerExceptionHandler.java index 4f57231fdb..883079f6ce 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/RestControllerExceptionHandler.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/RestControllerExceptionHandler.java @@ -3,6 +3,7 @@ package io.metersphere.sdk.controller.handler; import io.metersphere.sdk.controller.handler.result.IResultCode; import io.metersphere.sdk.controller.handler.result.MsHttpResultCode; import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.Translator; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.shiro.ShiroException; @@ -76,11 +77,11 @@ public class RestControllerExceptionHandler { if (errorCode instanceof MsHttpResultCode) { // 如果是 MsHttpResultCode,则设置响应的状态码,取状态码的后三位 return ResponseEntity.status(errorCode.getCode() % 1000) - .body(ResultHolder.error(errorCode.getCode(), errorCode.getMessage())); + .body(ResultHolder.error(errorCode.getCode(), Translator.get(errorCode.getMessage(), errorCode.getMessage()))); } else { // 响应码返回 500,设置业务状态码 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ResultHolder.error(errorCode.getCode(), errorCode.getMessage(), e.getMessage())); + .body(ResultHolder.error(errorCode.getCode(), Translator.get(errorCode.getMessage(), errorCode.getMessage()), e.getMessage())); } } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java index 94c4f99968..1cfcbbfea7 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java @@ -13,7 +13,10 @@ public enum CommonResultCode implements IResultCode { * 调用获取全局用户组接口,如果操作的是内置的用户组,会返回该响应码 */ INTERNAL_USER_ROLE_PERMISSION(101003, "internal_user_role_permission_error"), - USER_ROLE_RELATION_REMOVE_ADMIN_USER_PERMISSION(100004, "user_role_relation_remove_admin_user_permission_error"); + USER_ROLE_RELATION_REMOVE_ADMIN_USER_PERMISSION(100004, "user_role_relation_remove_admin_user_permission_error"), + FILE_NAME_ILLEGAL(100005, "file_name_illegal_error"), + PLUGIN_ENABLE(100006, "plugin_enable_error"), + PLUGIN_PERMISSION(100007, "plugin_permission_error"); private int code; private String message; diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java index 7bd388c59b..5a8d2cc454 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java @@ -9,10 +9,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * @author jianxing.chen @@ -24,6 +21,14 @@ public class PluginManager { */ protected Map classLoaderMap = new HashMap<>(); + /** + * 缓存查找过的类 + * 内层 map + * key 未接口的类 + * value 为实现类 + */ + protected Map> implClassCache = new HashMap<>(); + public PluginClassLoader getClassLoader(String pluginId) { return classLoaderMap.get(pluginId); } @@ -48,6 +53,7 @@ public class PluginManager { public void deletePlugin(String id) { classLoaderMap.remove(id); + implClassCache.remove(id); } /** @@ -70,40 +76,52 @@ public class PluginManager { * 获取接口的单一实现类 */ public Class getImplClass(String pluginId, Class superClazz) { - PluginClassLoader classLoader = classLoaderMap.get(pluginId); + PluginClassLoader classLoader = getPluginClassLoader(pluginId); + Map classes = implClassCache.get(pluginId); + if (classes == null) { + classes = new HashMap<>(); + implClassCache.put(pluginId, classes); + } + if (classes.get(superClazz) != null) { + return classes.get(superClazz); + } LinkedHashSet> result = new LinkedHashSet<>(); Set clazzSet = classLoader.getClazzSet(); for (Class item : clazzSet) { if (isImplClazz(superClazz, item) && !result.contains(item)) { + classes.put(superClazz, item); return item; } } return null; } + private PluginClassLoader getPluginClassLoader(String pluginId) { + PluginClassLoader classLoader = classLoaderMap.get(pluginId); + if (classLoader == null) { + throw new MSException("插件未加载"); + } + return classLoader; + } + /** * 获取指定接口最后一次加载的实现类实例 */ public T getImplInstance(String pluginId, Class superClazz) { - try { - Class clazz = getImplClass(pluginId, superClazz); - if (clazz == null) { - throw new MSException("未找到插件实现类"); - } - return clazz.getConstructor().newInstance(); - } catch (InvocationTargetException e) { - LogUtils.error(e); - throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getTargetException().getMessage()); - } catch (Exception e) { - LogUtils.error(e); - throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getMessage()); - } + return this.getImplInstance(pluginId, superClazz, null); } public T getImplInstance(String pluginId, Class superClazz, Object param) { try { Class clazz = getImplClass(pluginId, superClazz); - return clazz.getConstructor(param.getClass()).newInstance(param); + if (clazz == null) { + throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE); + } + if (param == null) { + return clazz.getConstructor().newInstance(); + } else { + return clazz.getConstructor(param.getClass()).newInstance(param); + } } catch (InvocationTargetException e) { LogUtils.error(e.getTargetException()); throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getTargetException().getMessage()); diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PlatformPluginService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PlatformPluginService.java new file mode 100644 index 0000000000..dfcfcd4834 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PlatformPluginService.java @@ -0,0 +1,108 @@ +package io.metersphere.sdk.service; + +import io.metersphere.plugin.platform.api.Platform; +import io.metersphere.plugin.platform.dto.PlatformRequest; +import io.metersphere.sdk.constants.PluginScenarioType; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.system.domain.*; +import io.metersphere.system.mapper.PluginMapper; +import io.metersphere.system.mapper.PluginOrganizationMapper; +import io.metersphere.system.mapper.ServiceIntegrationMapper; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static io.metersphere.sdk.controller.handler.result.CommonResultCode.PLUGIN_ENABLE; +import static io.metersphere.sdk.controller.handler.result.CommonResultCode.PLUGIN_PERMISSION; + +@Service +@Transactional(rollbackFor = Exception.class) +public class PlatformPluginService { + + @Resource + private PluginLoadService pluginLoadService; + @Resource + private ServiceIntegrationMapper serviceIntegrationMapper; + @Resource + private PluginMapper pluginMapper; + @Resource + private PluginOrganizationMapper pluginOrganizationMapper; + + /** + * 获取平台实例 + * + * @param pluginId + * @param orgId + * @param integrationConfig + * @return + */ + public Platform getPlatform(String pluginId, String orgId, String integrationConfig) { + if (StringUtils.isNotBlank(orgId)) { + // 服务集成的测试链接,不需要校验插件开启和状态 + Plugin plugin = pluginMapper.selectByPrimaryKey(pluginId); + checkPluginEnable(plugin); + checkPluginPermission(pluginId, orgId, plugin); + } + PlatformRequest pluginRequest = new PlatformRequest(); + pluginRequest.setIntegrationConfig(integrationConfig); + pluginRequest.setOrganizationId(orgId); + return pluginLoadService.getImplInstance(pluginId, Platform.class, pluginRequest); + } + + public Platform getPlatform(String pluginId, String orgId) { + // 这里会校验插件是否存在 + pluginLoadService.getMsPluginInstance(pluginId); + ServiceIntegration serviceIntegration = getServiceIntegrationByPluginId(pluginId); + return getPlatform(pluginId, orgId, new String(serviceIntegration.getConfiguration())); + } + + /** + * 校验该组织是否能访问插件 + * @param pluginId + * @param orgId + * @param plugin + */ + private void checkPluginPermission(String pluginId, String orgId, Plugin plugin) { + if (plugin.getGlobal()) { + return; + } + PluginOrganizationExample example = new PluginOrganizationExample(); + example.createCriteria() + .andOrganizationIdEqualTo(orgId) + .andPluginIdEqualTo(pluginId); + List pluginOrganizations = pluginOrganizationMapper.selectByExample(example); + for (PluginOrganization pluginOrganization : pluginOrganizations) { + if (StringUtils.equals(pluginOrganization.getOrganizationId(), orgId)) { + return; + } + } + throw new MSException(PLUGIN_PERMISSION); + } + + /** + * 校验插件是否启用 + * @param plugin + */ + private static void checkPluginEnable(Plugin plugin) { + if (BooleanUtils.isFalse(plugin.getEnable())) { + throw new MSException(PLUGIN_ENABLE); + } + } + + private ServiceIntegration getServiceIntegrationByPluginId(String pluginId) { + ServiceIntegrationExample example = new ServiceIntegrationExample(); + example.createCriteria().andPluginIdEqualTo(pluginId); + return serviceIntegrationMapper.selectByExampleWithBLOBs(example).get(0); + } + + public List getPlatformPlugins() { + PluginExample example = new PluginExample(); + example.createCriteria() + .andScenarioEqualTo(PluginScenarioType.PLATFORM.name()); + return pluginMapper.selectByExample(example); + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java index 4ddd2cb06c..3fed1d8694 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java @@ -1,15 +1,19 @@ package io.metersphere.sdk.service; +import io.metersphere.plugin.platform.api.AbstractPlatformPlugin; import io.metersphere.plugin.sdk.api.MsPlugin; import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.plugin.loader.PluginClassLoader; import io.metersphere.sdk.plugin.loader.PluginManager; import io.metersphere.sdk.plugin.storage.MsStorageStrategy; import io.metersphere.sdk.plugin.storage.StorageStrategy; +import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.LogUtils; import io.metersphere.system.domain.Plugin; import io.metersphere.system.domain.PluginExample; +import io.metersphere.system.domain.PluginScript; import io.metersphere.system.mapper.PluginMapper; +import io.metersphere.system.mapper.PluginScriptMapper; import jakarta.annotation.Resource; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.StringUtils; @@ -21,6 +25,8 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * @author jianxing @@ -33,6 +39,8 @@ public class PluginLoadService { @Resource private PluginMapper pluginMapper; + @Resource + private PluginScriptMapper pluginScriptMapper; /** * 上传插件到 minio @@ -178,6 +186,28 @@ public class PluginLoadService { return pluginManager.getImplInstance(id, MsPlugin.class); } + public T getImplInstance(String pluginId, Class superClazz, Object param) { + return pluginManager.getImplInstance(pluginId, superClazz, param); + } + + public T getImplInstance(String pluginId, Class superClazz) { + return pluginManager.getImplInstance(pluginId, superClazz); + } + + public List getPlatformPluginInstanceList() { + return getImplInstanceList(AbstractPlatformPlugin.class); + } + + public AbstractPlatformPlugin getPlatformPluginInstance(String pluginId) { + return getImplInstance(pluginId, AbstractPlatformPlugin.class); + } + + public List getImplInstanceList(Class clazz, Object... initArgs) { + return pluginManager.getClassLoaderMap().keySet().stream() + .map(pluginId -> pluginManager.getImplInstance(pluginId, clazz, initArgs) + ).collect(Collectors.toList()); + } + public boolean hasPluginKey(String currentPluginId, String pluginKey) { for (String pluginId : pluginManager.getClassLoaderMap().keySet()) { MsPlugin msPlugin = getMsPluginInstance(pluginId); @@ -187,4 +217,17 @@ public class PluginLoadService { } return false; } + + public InputStream getResourceAsStream(String pluginId, String name) { + return pluginManager.getClassLoaderMap().get(pluginId).getResourceAsStream(name); + } + + public Map getPluginScriptConfig(String pluginId, String scriptId) { + PluginScript pluginScript = pluginScriptMapper.selectByPrimaryKey(pluginId, scriptId); + return JSON.parseMap(new String(pluginScript.getScript())); + } + + public Object getPluginScriptContent(String pluginId, String scriptId) { + return getPluginScriptConfig(pluginId, scriptId).get("script"); + } } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/FilterChainUtils.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/FilterChainUtils.java index a5108bb443..5848c232cc 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/FilterChainUtils.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/FilterChainUtils.java @@ -55,7 +55,7 @@ public class FilterChainUtils { filterChainDefinitionMap.put("/websocket/**", "csrf"); // 获取插件中的图片 - filterChainDefinitionMap.put("/platform/plugin/image/**", "anon"); + filterChainDefinitionMap.put("/plugin/image/**", "anon"); return filterChainDefinitionMap; } diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties index 12fa5b393d..42d84c033b 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties @@ -427,4 +427,8 @@ permission.add=Create permission.edit=Update permission.delete=Delete permission.import=Import -permission.recover=Recover \ No newline at end of file +permission.recover=Recover + +file_name_illegal_error=File name is invalid +plugin_enable_error=Plugin is not enabled +plugin_permission_error=No access to this plugin \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties index 52cde17e47..b5920f3158 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties @@ -426,4 +426,8 @@ permission.add=创建 permission.edit=修改 permission.delete=删除 permission.import=导入 -permission.recover=恢复 \ No newline at end of file +permission.recover=恢复 + +file_name_illegal_error=文件名不合法 +plugin_enable_error=插件未启用 +plugin_permission_error=没有该插件的访问权限 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties index ddf132b0dd..1f71ba53a1 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties @@ -426,4 +426,8 @@ permission.add=創建 permission.edit=修改 permission.delete=刪除 permission.import=導入 -permission.recover=恢復 \ No newline at end of file +permission.recover=恢復 + +file_name_illegal_error=文件名不合法 +plugin_enable_error=插件未啟用 +plugin_permission_error=沒有該插件的訪問權限 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties index 94e6ac80ff..119978b66f 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties @@ -175,6 +175,7 @@ service_integration.plugin_id.length_range=pluginId length must be between {min} service_integration.organization_id.not_blank=organizationId cannot be empty service_integration.organization_id.length_range=organizationId length must be between {min} and {max} service_integration_exist_error=Service integration configuration already exists +service_integration.configuration.not_blank=Service integration configuration cannot be empty # permission permission.system_plugin.name=Plugin permission.system_organization_project.name=Organization Project diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties index 3f6ea74d05..4453f4406a 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties @@ -160,20 +160,21 @@ plugin.file_name.not_blank=文件名不能为空 plugin.file_name.length_range=文件名长度必须在{min}和{max}之间 plugin.create_user.not_blank=创建人不能为空 plugin.create_user.length_range=创建人长度必须在{min}和{max}之间 -plugin.scenario.not_blank=插件使用场景PAI/PLATFORM不能为空 -plugin.scenario.length_range=插件使用场景PAI/PLATFORM长度必须在{min}和{max}之间 +plugin.scenario.not_blank=插件使用场景API/PLATFORM不能为空 +plugin.scenario.length_range=插件使用场景API/PLATFORM长度必须在{min}和{max}之间 plugin.exist=插件名称或文件名已存在 plugin.type.exist=插件类型已存在 plugin.script.exist=脚本id重复 plugin.script.format=脚本格式错误 # serviceIntegration -service_integration.id.not_blank=ID不能為空 -service_integration.id.length_range=ID長度必須在{min}和{max}之间 -service_integration.plugin_id.not_blank=插件的ID不能為空 -service_integration.plugin_id.length_range=插件的ID長度必須在{min}和{max}之间 -service_integration.organization_id.not_blank=组织ID不能為空 -service_integration.organization_id.length_range=组织ID長度必須在{min}和{max}之间 +service_integration.id.not_blank=ID不能为空 +service_integration.id.length_range=ID长度必须在{min}和{max}之间 +service_integration.plugin_id.not_blank=插件的ID不能为空 +service_integration.plugin_id.length_range=插件的ID长度必须在{min}和{max}之间 +service_integration.organization_id.not_blank=组织ID不能为空 +service_integration.organization_id.length_range=组织ID长度必须在{min}和{max}之间 service_integration_exist_error=服务集成配置已存在 +service_integration.configuration.not_blank=服务集成配置不能為空 # permission permission.system_plugin.name=插件 permission.system_organization_project.name=组织与项目 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties index 58f439753b..b688ca669e 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties @@ -174,6 +174,7 @@ service_integration.plugin_id.length_range=插件的ID长度必须在{min}和{ma service_integration.organization_id.not_blank=组织ID不能为空 service_integration.organization_id.length_range=组织ID长度必须在{min}和{max}之间 service_integration_exist_error=服務集成配置已存在 +service_integration.configuration.not_blank=服务集成配置不能為空 # permission permission.system_plugin.name=插件 permission.system_organization_project.name=組織與項目 diff --git a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java index 8754f46f30..a19b4b6be8 100644 --- a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java +++ b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java @@ -40,6 +40,8 @@ import org.springframework.util.MultiValueMap; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import java.nio.file.Files; import java.util.HashMap; import java.util.List; @@ -232,20 +234,24 @@ public abstract class BaseTest { } protected T getResultData(MvcResult mvcResult, Class clazz) throws Exception { - Object data = JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("data"); + Object data = parseResponse(mvcResult).get("data"); return JSON.parseObject(JSON.toJSONString(data), clazz); } protected T getResultMessageDetail(MvcResult mvcResult, Class clazz) throws Exception { - Object data = JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("messageDetail"); + Object data = parseResponse(mvcResult).get("messageDetail"); return JSON.parseObject(JSON.toJSONString(data), clazz); } protected List getResultDataArray(MvcResult mvcResult, Class clazz) throws Exception { - Object data = JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("data"); + Object data = parseResponse(mvcResult).get("data"); return JSON.parseArray(JSON.toJSONString(data), clazz); } + private static Map parseResponse(MvcResult mvcResult) throws UnsupportedEncodingException { + return JSON.parseMap(mvcResult.getResponse().getContentAsString(Charset.defaultCharset())); + } + /** * 校验错误响应码 */ @@ -258,7 +264,7 @@ public abstract class BaseTest { } protected Pager> getPageResult(MvcResult mvcResult, Class clazz) throws Exception { - Map pagerResult = (Map) JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("data"); + Map pagerResult = (Map) parseResponse(mvcResult).get("data"); List list = JSON.parseArray(JSON.toJSONString(pagerResult.get("list")), clazz); Pager pager = new Pager(); pager.setPageSize(Long.valueOf(pagerResult.get("pageSize").toString())); diff --git a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/param/NotEmptyParamGenerator.java b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/param/NotEmptyParamGenerator.java index 1ffc1b1bd2..6fb01f3dec 100644 --- a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/param/NotEmptyParamGenerator.java +++ b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/param/NotEmptyParamGenerator.java @@ -2,7 +2,7 @@ package io.metersphere.sdk.base.param; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.util.ArrayList; +import java.util.*; /** * @author jianxing @@ -14,6 +14,15 @@ public class NotEmptyParamGenerator extends ParamGenerator { */ @Override public Object invalidGenerate(Annotation annotation, Field field) { - return new ArrayList<>(0); + if (field.getType().equals(List.class)) { + return new ArrayList<>(0); + } + if (field.getType().equals(Set.class)) { + return new HashSet<>(0); + } + if (field.getType().equals(Map.class)) { + return new HashMap<>(0); + } + throw new RuntimeException("不支持该类型"); } } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java index a8d75649cd..80b75b9150 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java @@ -22,6 +22,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; /** @@ -86,7 +87,7 @@ public class PluginController { @Schema(description = "图片路径", requiredMode = Schema.RequiredMode.REQUIRED) @RequestParam("imagePath") String imagePath, - HttpServletResponse response) { - // todo + HttpServletResponse response) throws IOException { + pluginService.getPluginImg(pluginId, imagePath, response); } } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/ServiceIntegrationController.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/ServiceIntegrationController.java index cdccdc98a0..3e30f8d5d4 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/ServiceIntegrationController.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/ServiceIntegrationController.java @@ -14,12 +14,13 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotEmpty; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; import java.util.List; -import java.util.Map; /** * @author : jianxing @@ -32,6 +33,7 @@ public class ServiceIntegrationController { @Resource private ServiceIntegrationService serviceIntegrationService; + @GetMapping("/list/{organizationId}") @Operation(summary = "获取服务集成列表") @RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ) @@ -59,24 +61,27 @@ public class ServiceIntegrationController { @Operation(summary = "删除服务集成") @RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_DELETE) @Log(type = OperationLogType.DELETE, expression = "#msClass.deleteLog(#id)", msClass = ServiceIntegrationLogService.class) - public String delete(@PathVariable String id) { - return serviceIntegrationService.delete(id); + public void delete(@PathVariable String id) { + serviceIntegrationService.delete(id); } - @PostMapping("/validate") + @PostMapping("/validate/{pluginId}") @Operation(summary = "校验服务集成信息") @RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE) - public boolean validate(@Validated({Updated.class}) @RequestBody - @Schema(description = "配置的表单键值对", requiredMode = Schema.RequiredMode.REQUIRED) - Map serviceIntegrationInfo) { - return serviceIntegrationService.validate(serviceIntegrationInfo); + public void validate(@PathVariable String pluginId, + @Validated({Updated.class}) + @RequestBody + @NotEmpty + @Schema(description = "配置的表单键值对", requiredMode = Schema.RequiredMode.REQUIRED) + HashMap serviceIntegrationInfo) { + serviceIntegrationService.validate(pluginId, serviceIntegrationInfo); } @GetMapping("/validate/{id}") @Operation(summary = "校验服务集成信息") @RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE) - public boolean validate(@PathVariable String id) { - return serviceIntegrationService.validate(id); + public void validate(@PathVariable String id) { + serviceIntegrationService.validate(id); } @GetMapping("/script/{pluginId}") diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ServiceIntegrationDTO.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ServiceIntegrationDTO.java index 8328f2786a..471887d44e 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ServiceIntegrationDTO.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ServiceIntegrationDTO.java @@ -20,7 +20,7 @@ public class ServiceIntegrationDTO implements Serializable { private String title; @Schema(description = "插件描述") - private String describe; + private String description; @Schema(description = "是否启用") private Boolean enable; diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java b/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java index 3dd9f5ba7b..38c1c8e9ae 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java @@ -28,7 +28,7 @@ public class PluginUpdateRequest { private Boolean global; @Schema(description = "插件描述") - @Size(min = 1, max = 500, message = "{plugin.scenario.length_range}", groups = {Created.class, Updated.class}) + @Size(max = 500, message = "{plugin.scenario.length_range}", groups = {Created.class, Updated.class}) private String description; @Schema(hidden = true) diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/request/ServiceIntegrationUpdateRequest.java b/backend/services/system-setting/src/main/java/io/metersphere/system/request/ServiceIntegrationUpdateRequest.java index af5f32c05b..58728b1af8 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/request/ServiceIntegrationUpdateRequest.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/request/ServiceIntegrationUpdateRequest.java @@ -1,12 +1,14 @@ package io.metersphere.system.request; + import io.metersphere.validation.groups.Created; import io.metersphere.validation.groups.Updated; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.Data; + import java.io.Serializable; import java.util.Map; @@ -17,7 +19,7 @@ public class ServiceIntegrationUpdateRequest implements Serializable { @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "{service_integration.id.not_blank}", groups = {Updated.class}) - @Size(min = 1, max = 50, message = "{service_integration.id.length_range}", groups = {Created.class, Updated.class}) + @Size(min = 1, max = 50, message = "{service_integration.id.length_range}", groups = {Updated.class}) private String id; @Schema(description = "插件的ID", requiredMode = Schema.RequiredMode.REQUIRED) @@ -34,6 +36,6 @@ public class ServiceIntegrationUpdateRequest implements Serializable { private String organizationId; @Schema(description = "配置的表单键值对", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "{service_integration.configuration.not_blank}", groups = {Created.class}) - private Map configuration; + @NotEmpty(message = "{service_integration.configuration.not_blank}", groups = {Created.class}) + private Map configuration; } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java index d2aa37c46c..0f5f4c96e2 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java @@ -4,6 +4,7 @@ package io.metersphere.system.service; import io.metersphere.plugin.sdk.api.MsPlugin; import io.metersphere.sdk.constants.KafkaPluginTopicType; import io.metersphere.sdk.constants.KafkaTopicConstants; +import io.metersphere.sdk.controller.handler.result.CommonResultCode; import io.metersphere.sdk.dto.OptionDTO; import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.service.PluginLoadService; @@ -15,6 +16,7 @@ import io.metersphere.system.mapper.ExtPluginMapper; import io.metersphere.system.mapper.PluginMapper; import io.metersphere.system.request.PluginUpdateRequest; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -23,10 +25,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.*; import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_EXIST; import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_TYPE_EXIST; @@ -138,8 +140,9 @@ public class PluginService { } /** - * 通知其他节点加载插件 - * 这里需要传一下 fileName,事务未提交,查询不到文件名 + * 通知其他节点加载插件 + * 这里需要传一下 fileName,事务未提交,查询不到文件名 + * * @param pluginId * @param fileName */ @@ -149,7 +152,8 @@ public class PluginService { } /** - * 通知其他节点卸载插件 + * 通知其他节点卸载插件 + * * @param pluginId */ public void notifiedPluginDelete(String pluginId) { @@ -201,4 +205,34 @@ public class PluginService { public String getScript(String pluginId, String scriptId) { return pluginScriptService.get(pluginId, scriptId); } + + public void getPluginImg(String pluginId, String filePath, HttpServletResponse response) throws IOException { + validateImageFileName(filePath); + InputStream inputStream = pluginLoadService.getResourceAsStream(pluginId, filePath); + writeImage(filePath, inputStream, response); + } + + public void validateImageFileName(String filename) { + Set imgSuffix = new HashSet<>() {{ + add("jpg"); + add("png"); + add("gif"); + add("jpeg"); + }}; + if (!imgSuffix.contains(StringUtils.substringAfterLast(filename, "."))) { + throw new MSException(CommonResultCode.FILE_NAME_ILLEGAL); + } + } + + public void writeImage(String filePath, InputStream in, HttpServletResponse response) throws IOException { + response.setContentType("image/" + StringUtils.substringAfterLast(filePath, ".")); + try (OutputStream out = response.getOutputStream()) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + response.getOutputStream().write(buffer, 0, bytesRead); + } + out.flush(); + } + } } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/ServiceIntegrationService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/ServiceIntegrationService.java index 13d83b956b..b5e2ba418e 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/ServiceIntegrationService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/ServiceIntegrationService.java @@ -1,8 +1,13 @@ package io.metersphere.system.service; +import io.metersphere.plugin.platform.api.AbstractPlatformPlugin; +import io.metersphere.plugin.platform.api.Platform; import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.service.PlatformPluginService; +import io.metersphere.sdk.service.PluginLoadService; import io.metersphere.sdk.util.BeanUtils; import io.metersphere.sdk.util.JSON; +import io.metersphere.system.domain.Plugin; import io.metersphere.system.domain.ServiceIntegration; import io.metersphere.system.domain.ServiceIntegrationExample; import io.metersphere.system.dto.ServiceIntegrationDTO; @@ -10,13 +15,14 @@ import io.metersphere.system.mapper.ServiceIntegrationMapper; import io.metersphere.system.request.ServiceIntegrationUpdateRequest; import jakarta.annotation.Resource; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import static io.metersphere.system.controller.result.SystemResultCode.SERVICE_INTEGRATION_EXIST; @@ -27,12 +33,48 @@ import static io.metersphere.system.controller.result.SystemResultCode.SERVICE_I @Service @Transactional(rollbackFor = Exception.class) public class ServiceIntegrationService { - @Resource private ServiceIntegrationMapper serviceIntegrationMapper; + @Resource + private PluginLoadService pluginLoadService; + @Resource + private PlatformPluginService platformPluginService; + + public static final String PLUGIN_IMAGE_GET_PATH = "/plugin/image/%s?imagePath=%s"; public List list(String organizationId) { - return Arrays.asList(new ServiceIntegrationDTO()); + // 查询服务集成已配置数据 + Map serviceIntegrationMap = getServiceIntegrationByOrgId(organizationId).stream() + .collect(Collectors.toMap(ServiceIntegration::getPluginId, i -> i)); + + List plugins = platformPluginService.getPlatformPlugins(); + return plugins.stream().map(plugin -> { + AbstractPlatformPlugin msPluginInstance = pluginLoadService.getPlatformPluginInstance(plugin.getId()); + // 获取插件基础信息 + ServiceIntegrationDTO serviceIntegrationDTO = new ServiceIntegrationDTO(); + serviceIntegrationDTO.setTitle(msPluginInstance.getName()); + serviceIntegrationDTO.setEnable(false); + serviceIntegrationDTO.setConfig(false); + serviceIntegrationDTO.setDescription(msPluginInstance.getDescription()); + serviceIntegrationDTO.setLogo(String.format(PLUGIN_IMAGE_GET_PATH, plugin.getId(), msPluginInstance.getLogo())); + serviceIntegrationDTO.setPluginId(plugin.getId()); + ServiceIntegration serviceIntegration = serviceIntegrationMap.get(plugin.getId()); + if (serviceIntegration != null) { + serviceIntegrationDTO.setConfiguration(JSON.parseMap(new String(serviceIntegration.getConfiguration()))); + serviceIntegrationDTO.setId(serviceIntegration.getId()); + serviceIntegrationDTO.setEnable(serviceIntegration.getEnable()); + serviceIntegrationDTO.setConfig(true); + serviceIntegrationDTO.setOrganizationId(serviceIntegration.getOrganizationId()); + } + return serviceIntegrationDTO; + }).toList(); + } + + private List getServiceIntegrationByOrgId(String organizationId) { + ServiceIntegrationExample example = new ServiceIntegrationExample(); + example.createCriteria() + .andOrganizationIdEqualTo(organizationId); + return serviceIntegrationMapper.selectByExampleWithBLOBs(example); } public ServiceIntegration add(ServiceIntegrationUpdateRequest request) { @@ -47,6 +89,8 @@ public class ServiceIntegrationService { public ServiceIntegration update(ServiceIntegrationUpdateRequest request) { ServiceIntegration serviceIntegration = new ServiceIntegration(); + // 组织不能修改 + serviceIntegration.setOrganizationId(null); BeanUtils.copyBean(serviceIntegration, request); if (request.getConfiguration() != null) { serviceIntegration.setConfiguration(JSON.toJSONBytes(request.getConfiguration())); @@ -55,9 +99,8 @@ public class ServiceIntegrationService { return serviceIntegration; } - public String delete(String id) { + public void delete(String id) { serviceIntegrationMapper.deleteByPrimaryKey(id); - return id; } private void checkAddExist(ServiceIntegration serviceIntegration) { @@ -70,13 +113,15 @@ public class ServiceIntegrationService { } } - public boolean validate(Map serviceIntegrationInfo) { - return serviceIntegrationInfo == null; + public void validate(String pluginId, Map serviceIntegrationInfo) { + Platform platform = platformPluginService.getPlatform(pluginId, StringUtils.EMPTY, JSON.toJSONString(serviceIntegrationInfo)); + platform.validateIntegrationConfig(); } - public boolean validate(String id) { + public void validate(String id) { ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(id); - return serviceIntegration == null; + Platform platform = platformPluginService.getPlatform(serviceIntegration.getPluginId(), StringUtils.EMPTY); + platform.validateIntegrationConfig(); } public ServiceIntegration get(String id) { @@ -84,6 +129,7 @@ public class ServiceIntegrationService { } public Object getPluginScript(String pluginId) { - return new ServiceIntegration(); + AbstractPlatformPlugin platformPlugin = pluginLoadService.getImplInstance(pluginId, AbstractPlatformPlugin.class); + return pluginLoadService.getPluginScriptContent(pluginId, platformPlugin.getIntegrationScriptId()); } } \ No newline at end of file diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java index 91b2a026b5..b7872341d9 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java @@ -29,6 +29,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import static io.metersphere.sdk.controller.handler.result.CommonResultCode.FILE_NAME_ILLEGAL; import static io.metersphere.system.controller.result.SystemResultCode.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -260,8 +261,10 @@ public class PluginControllerTests extends BaseTest { @Order(5) public void getPluginImg() throws Exception { // @@请求成功 - mockMvc.perform(getRequestBuilder(PLUGIN_IMAGE, "pluginId", "/static/jira.jpg")) + mockMvc.perform(getRequestBuilder(PLUGIN_IMAGE, anotherAddPlugin.getId(), "/static/jira.jpg")) .andExpect(status().isOk()); + + assertErrorCode( this.requestGet(PLUGIN_IMAGE, anotherAddPlugin.getId(), "/static/jira.doc"), FILE_NAME_ILLEGAL); } @Test @@ -274,6 +277,7 @@ public class PluginControllerTests extends BaseTest { Assertions.assertNull(plugin); Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(addPlugin.getId())); Assertions.assertEquals(new ArrayList<>(0), getScriptIdsByPlugId(addPlugin.getId())); + this.requestGetWithOk(DEFAULT_DELETE, anotherAddPlugin.getId()); // @@校验日志 checkLog(addPlugin.getId(), OperationLogType.DELETE); diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/ServiceIntegrationControllerTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/ServiceIntegrationControllerTests.java index c9c4f50db6..0fd5fb9a22 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/ServiceIntegrationControllerTests.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/ServiceIntegrationControllerTests.java @@ -1,22 +1,44 @@ package io.metersphere.system.controller; +import io.metersphere.plugin.platform.api.AbstractPlatformPlugin; +import io.metersphere.plugin.sdk.util.JSON; import io.metersphere.sdk.base.BaseTest; import io.metersphere.sdk.constants.PermissionConstants; import io.metersphere.sdk.log.constants.OperationLogType; +import io.metersphere.sdk.service.PluginLoadService; import io.metersphere.system.controller.param.ServiceIntegrationUpdateRequestDefinition; +import io.metersphere.system.domain.Organization; +import io.metersphere.system.domain.Plugin; import io.metersphere.system.domain.ServiceIntegration; +import io.metersphere.system.dto.ServiceIntegrationDTO; import io.metersphere.system.mapper.ServiceIntegrationMapper; +import io.metersphere.system.request.PluginUpdateRequest; import io.metersphere.system.request.ServiceIntegrationUpdateRequest; +import io.metersphere.system.service.OrganizationService; +import io.metersphere.system.service.PluginService; import jakarta.annotation.Resource; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.*; +import org.mockserver.client.MockServerClient; +import org.mockserver.model.Header; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MvcResult; -import java.util.HashMap; +import java.io.File; +import java.io.FileInputStream; +import java.util.List; +import java.util.Map; + +import static io.metersphere.sdk.constants.InternalUserRole.ADMIN; +import static io.metersphere.system.controller.result.SystemResultCode.SERVICE_INTEGRATION_EXIST; +import static io.metersphere.system.service.ServiceIntegrationService.PLUGIN_IMAGE_GET_PATH; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; /** * @author jianxing @@ -29,41 +51,68 @@ public class ServiceIntegrationControllerTests extends BaseTest { private static final String BASE_PATH = "/service/integration/"; private static final String LIST = "/list/{0}"; private static final String VALIDATE_GET = "/validate/{0}"; - private static final String VALIDATE_POST = "/validate"; + private static final String VALIDATE_POST = "/validate/{0}"; private static final String SCRIPT_GET = "/script/{0}"; + private static ServiceIntegration addServiceIntegration; + private static Plugin plugin; + private static Organization defaultOrg; + @Resource private ServiceIntegrationMapper serviceIntegrationMapper; - private static ServiceIntegration addServiceIntegration; + @Resource + private OrganizationService organizationService; + @Resource + private PluginLoadService pluginLoadService; + @Resource + private PluginService pluginService; + @Resource + private MockServerClient mockServerClient; + @Value("${embedded.mockserver.host}") + private String mockServerHost; + @Value("${embedded.mockserver.port}") + private int mockServerHostPort; + @Override protected String getBasePath() { return BASE_PATH; } @Test - public void list() throws Exception { - // @@请求成功 - this.requestGetWithOkAndReturn(LIST, "orgId"); -// List serviceIntegrationList = getResultDataArray(mvcResult, ServiceIntegration.class); - // todo 校验数据是否正确 - // @@校验权限 - requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, LIST,"orgId"); + @Order(0) + public void listEmpty() throws Exception { + defaultOrg = organizationService.getDefault(); + // @@请求成功, 校验空数据,请求是否正常 + this.requestGetWithOkAndReturn(LIST, defaultOrg.getId()); } + @Test - @Order(0) + @Order(1) public void add() throws Exception { + plugin = addPlugin(); + + JiraIntegrationConfig integrationConfig = new JiraIntegrationConfig(); + Map integrationConfigMap = JSON.parseMap(JSON.toJSONString(integrationConfig)); + // @@请求成功 ServiceIntegrationUpdateRequest request = new ServiceIntegrationUpdateRequest(); request.setEnable(false); - request.setPluginId("pluginId"); - request.setConfiguration(new HashMap<>()); - request.setOrganizationId("orgId"); - // todo 填充数据 + request.setPluginId(plugin.getId()); + request.setConfiguration(integrationConfigMap); + request.setOrganizationId(defaultOrg.getId()); MvcResult mvcResult = this.requestPostWithOkAndReturn(DEFAULT_ADD, request); + // 校验请求成功数据 ServiceIntegration resultData = getResultData(mvcResult, ServiceIntegration.class); ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(resultData.getId()); - this.addServiceIntegration= serviceIntegration; - // todo 校验请求成功数据 + Assertions.assertEquals(JSON.parseMap(new String(serviceIntegration.getConfiguration())), integrationConfigMap); + Assertions.assertEquals(serviceIntegration.getEnable(), request.getEnable()); + Assertions.assertEquals(serviceIntegration.getPluginId(), request.getPluginId()); + Assertions.assertEquals(serviceIntegration.getOrganizationId(), request.getOrganizationId()); + this.addServiceIntegration = serviceIntegration; + + // @@重名校验异常 + assertErrorCode(this.requestPost(DEFAULT_ADD, request), SERVICE_INTEGRATION_EXIST); + // @@校验日志 checkLog(this.addServiceIntegration.getId(), OperationLogType.ADD); // @@异常参数校验 @@ -73,20 +122,32 @@ public class ServiceIntegrationControllerTests extends BaseTest { } @Test - @Order(1) + @Order(2) public void update() throws Exception { + JiraIntegrationConfig integrationConfig = new JiraIntegrationConfig(); + integrationConfig.setAddress(String.format("http://%s:%s", mockServerHost, mockServerHostPort)); + Map integrationConfigMap = JSON.parseMap(JSON.toJSONString(integrationConfig)); + // @@请求成功 ServiceIntegrationUpdateRequest request = new ServiceIntegrationUpdateRequest(); request.setId(addServiceIntegration.getId()); request.setEnable(false); - request.setPluginId("pluginId"); - request.setConfiguration(new HashMap<>()); - request.setOrganizationId("orgId"); - // todo 填充数据 + request.setPluginId(plugin.getId()); + request.setConfiguration(integrationConfigMap); + request.setOrganizationId(defaultOrg.getId()); this.requestPostWithOk(DEFAULT_UPDATE, request); // 校验请求成功数据 -// ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(request.getId()); - // todo 校验请求成功数据 + ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(request.getId()); + Assertions.assertEquals(JSON.parseMap(new String(serviceIntegration.getConfiguration())), integrationConfigMap); + Assertions.assertEquals(serviceIntegration.getEnable(), request.getEnable()); + Assertions.assertEquals(serviceIntegration.getPluginId(), request.getPluginId()); + // 验证组织修改无效 + Assertions.assertEquals(serviceIntegration.getOrganizationId(), defaultOrg.getId()); + + // 提升覆盖率 + request.setConfiguration(null); + this.requestPostWithOk(DEFAULT_UPDATE, request); + // @@校验日志 checkLog(request.getId(), OperationLogType.UPDATE); // @@异常参数校验 @@ -95,41 +156,132 @@ public class ServiceIntegrationControllerTests extends BaseTest { requestPostPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, DEFAULT_UPDATE, request); } + @Test + @Order(3) + public void list() throws Exception { + // @@请求成功 + MvcResult mvcResult = this.requestGetWithOkAndReturn(LIST, defaultOrg.getId()); + // 校验请求成功数据 + List serviceIntegrationList = getResultDataArray(mvcResult, ServiceIntegrationDTO.class); + ServiceIntegrationDTO serviceIntegrationDTO = serviceIntegrationList.get(0); + ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(serviceIntegrationDTO.getId()); + Assertions.assertEquals(JSON.parseMap(new String(serviceIntegration.getConfiguration())), + serviceIntegrationDTO.getConfiguration()); + Assertions.assertEquals(serviceIntegration.getEnable(), serviceIntegrationDTO.getEnable()); + Assertions.assertEquals(serviceIntegration.getPluginId(), serviceIntegrationDTO.getPluginId()); + AbstractPlatformPlugin msPluginInstance = pluginLoadService.getPlatformPluginInstance(plugin.getId()); + Assertions.assertEquals(serviceIntegrationDTO.getDescription(), msPluginInstance.getDescription()); + Assertions.assertEquals(serviceIntegrationDTO.getOrganizationId(), defaultOrg.getId()); + Assertions.assertEquals(serviceIntegrationDTO.getTitle(), msPluginInstance.getName()); + Assertions.assertEquals(serviceIntegrationDTO.getConfig(), true); + Assertions.assertEquals(serviceIntegrationDTO.getLogo(), + String.format(PLUGIN_IMAGE_GET_PATH, plugin.getId(), msPluginInstance.getLogo())); + + // @@校验权限 + requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, LIST, defaultOrg.getId()); + } + + @Test + @Order(4) + public void validateGet() throws Exception { + mockServerClient + .when( + request() + .withMethod("GET") + .withPath("/rest/api/2/myself")) + .respond( + response() + .withStatusCode(200) + .withHeaders( + new Header("Content-Type", "application/json; charset=utf-8"), + new Header("Cache-Control", "public, max-age=86400")) + .withBody("{\"self\"") + ); + + // @@请求成功 + this.requestGetWithOk(VALIDATE_GET, addServiceIntegration.getId()); + // @@校验权限 + requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_GET, addServiceIntegration.getId()); + } + + @Test + @Order(5) + public void validatePost() throws Exception { + JiraIntegrationConfig integrationConfig = new JiraIntegrationConfig(); + integrationConfig.setAddress(String.format("http://%s:%s", mockServerHost, mockServerHostPort)); + Map integrationConfigMap = JSON.parseMap(JSON.toJSONString(integrationConfig)); + // @@请求成功 + this.requestPostWithOk(VALIDATE_POST, integrationConfigMap, plugin.getId()); + // @@校验权限 + requestPostPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_POST, integrationConfigMap, plugin.getId()); + } + + @Test + @Order(6) + public void getPluginScript() throws Exception { + // @@请求成功 + MvcResult mvcResult = this.requestGetWithOkAndReturn(SCRIPT_GET, plugin.getId()); + // 校验请求成功数据 + Assertions.assertTrue(StringUtils.isNotBlank(mvcResult.getResponse().getContentAsString())); + // @@校验权限 + requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, SCRIPT_GET, plugin.getId()); + } + @Test public void delete() throws Exception { // @@请求成功 this.requestGetWithOk(DEFAULT_DELETE, addServiceIntegration.getId()); - // todo 校验请求成功数据 + // 校验请求成功数据 + ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(addServiceIntegration.getId()); + Assertions.assertNull(serviceIntegration); + + // 清理插件 + deletePlugin(); + // @@校验日志 checkLog(addServiceIntegration.getId(), OperationLogType.DELETE); // @@校验权限 requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_DELETE, DEFAULT_DELETE, addServiceIntegration.getId()); } - @Test - public void validateGet() throws Exception { - // @@请求成功 - this.requestGetWithOk(VALIDATE_GET, addServiceIntegration.getId()); - // todo 校验请求成功数据 - // @@校验权限 - requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_GET, addServiceIntegration.getId()); + + /** + * 添加插件,供测试使用 + * + * @return + * @throws Exception + */ + public Plugin addPlugin() throws Exception { + PluginUpdateRequest request = new PluginUpdateRequest(); + File jarFile = new File( + this.getClass().getClassLoader().getResource("file/metersphere-jira-plugin-3.x.jar") + .getPath() + ); + FileInputStream inputStream = new FileInputStream(jarFile); + MockMultipartFile mockMultipartFile = new MockMultipartFile(jarFile.getName(), inputStream); + request.setName("测试插件"); + request.setGlobal(true); + request.setEnable(true); + request.setCreateUser(ADMIN.name()); + return pluginService.add(request, mockMultipartFile); } - @Test - public void validatePost() throws Exception { - // @@请求成功 - this.requestPostWithOk(VALIDATE_POST, new HashMap<>()); - // todo 校验请求成功数据 - // @@校验权限 - requestPostPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_POST, new HashMap<>()); + /** + * 删除插件 + * @throws Exception + */ + public void deletePlugin() throws Exception { + this.requestGetWithOk(DEFAULT_DELETE, plugin.getId()); } - @Test - public void getPluginScript() throws Exception { - // @@请求成功 - this.requestGetWithOk(SCRIPT_GET, "pluginId"); - // todo 校验请求成功数据 - // @@校验权限 - requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, SCRIPT_GET, "pluginId"); + @Getter + @Setter + public class JiraIntegrationConfig { + private String account; + private String password; + private String token; + private String authType; + private String address; + private String version; } } \ No newline at end of file diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java index f3e886bd79..d285ca31fb 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java @@ -16,6 +16,6 @@ public class PluginUpdateRequestDefinition { @Size(min = 1, max = 255, groups = {Created.class, Updated.class}) private String name; - @Size(min = 1, max = 500, groups = {Created.class, Updated.class}) + @Size(max = 500, groups = {Created.class, Updated.class}) private String description; } diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/ServiceIntegrationUpdateRequestDefinition.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/ServiceIntegrationUpdateRequestDefinition.java index 162002c6c6..b381fe49a1 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/ServiceIntegrationUpdateRequestDefinition.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/ServiceIntegrationUpdateRequestDefinition.java @@ -14,7 +14,7 @@ import java.util.Map; public class ServiceIntegrationUpdateRequestDefinition { @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(groups = {Updated.class}) - @Size(min = 1, max = 50, groups = {Created.class, Updated.class}) + @Size(min = 1, max = 50, groups = {Updated.class}) private String id; @Schema(description = "插件的ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar b/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar index f8b4cb2f6f..c8a6ea2338 100644 Binary files a/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar and b/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar differ