From c2cc519614dd5d3080cf8eec9a2431431e75a7c2 Mon Sep 17 00:00:00 2001 From: "longpeng.zlp" Date: Fri, 20 Jun 2025 10:31:02 +0800 Subject: [PATCH] support external sql interceptor call back --- .../integration/ExternalSqlInterceptor.java | 28 +- .../client/SqlInterceptorClient.java | 69 +++-- .../integration/model/SqlCheckResult.java | 32 ++ .../model/SqlInterceptorProperties.java | 36 +++ .../integration/model/TemplateVariables.java | 3 +- .../ExternalSqlInterceptorTest.java | 286 ++++++++++++++++++ 6 files changed, 432 insertions(+), 22 deletions(-) create mode 100644 server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlCheckResult.java create mode 100644 server/odc-service/src/test/java/com/oceanbase/odc/service/intergation/ExternalSqlInterceptorTest.java diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/ExternalSqlInterceptor.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/ExternalSqlInterceptor.java index 84602fd2c0..df36a62955 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/ExternalSqlInterceptor.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/ExternalSqlInterceptor.java @@ -16,6 +16,7 @@ package com.oceanbase.odc.service.integration; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; @@ -25,6 +26,7 @@ import org.springframework.stereotype.Component; import com.oceanbase.odc.common.json.JsonUtils; +import com.oceanbase.odc.common.util.MapUtils; import com.oceanbase.odc.core.session.ConnectionSession; import com.oceanbase.odc.core.session.ConnectionSessionUtil; import com.oceanbase.odc.core.shared.constant.OrganizationType; @@ -37,8 +39,9 @@ import com.oceanbase.odc.service.connection.model.ConnectionConfig; import com.oceanbase.odc.service.iam.auth.AuthenticationFacade; import com.oceanbase.odc.service.integration.client.SqlInterceptorClient; -import com.oceanbase.odc.service.integration.model.SqlCheckStatus; +import com.oceanbase.odc.service.integration.model.SqlCheckResult; import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties; +import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties.CheckProperties; import com.oceanbase.odc.service.integration.model.TemplateVariables; import com.oceanbase.odc.service.integration.model.TemplateVariables.Variable; import com.oceanbase.odc.service.regulation.ruleset.RuleService; @@ -100,9 +103,20 @@ public boolean doPreHandle(@NonNull SqlAsyncExecuteReq request, @NonNull SqlAsyn SqlInterceptorProperties properties = (SqlInterceptorProperties) integrationService.getIntegrationProperties(interceptorOpt.get().getId()); TemplateVariables variables = buildTemplateVariables(request.getSql(), session); - SqlCheckStatus result = sqlInterceptorClient.check(properties, variables); - switch (result) { + SqlCheckResult result = sqlInterceptorClient.check(properties, variables); + // set variables in template + if (!MapUtils.isEmpty(result.getExtractedResponse())) { + for (Map.Entry entry : result.getExtractedResponse().entrySet()) { + variables.setAttribute(Variable.EXTERNAL_PROPERTIES, entry.getKey(), entry.getValue()); + } + } + CheckProperties check = properties.getApi().getCheck(); + switch (result.getSqlCheckStatus()) { case IN_WHITE_LIST: + if (null != check.onInWhiteList()) { + sqlInterceptorClient.getIntegrationResponse(properties.getHttp(), check.onInWhiteList(), variables, + properties.getEncryption()); + } return true; case IN_BLACK_LIST: ruleService.getByRulesetIdAndName(ruleSetId, SqlConsoleRules.EXTERNAL_SQL_INTERCEPTOR.getRuleName()) @@ -121,6 +135,10 @@ public boolean doPreHandle(@NonNull SqlAsyncExecuteReq request, @NonNull SqlAsyn violationRule.setViolation(violation); response.getViolatedRules().add(violationRule); }); + if (null != check.onInBlackList()) { + sqlInterceptorClient.getIntegrationResponse(properties.getHttp(), check.onInBlackList(), variables, + properties.getEncryption()); + } return false; case NEED_REVIEW: ruleService.getByRulesetIdAndName(ruleSetId, SqlConsoleRules.EXTERNAL_SQL_INTERCEPTOR.getRuleName()) @@ -139,6 +157,10 @@ public boolean doPreHandle(@NonNull SqlAsyncExecuteReq request, @NonNull SqlAsyn violationRule.setViolation(violation); response.getViolatedRules().add(violationRule); }); + if (null != check.onNeedReview()) { + sqlInterceptorClient.getIntegrationResponse(properties.getHttp(), check.onNeedReview(), variables, + properties.getEncryption()); + } return false; default: throw new UnexpectedException("SQL intercept failed, unknown intercept status: " + result); diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/client/SqlInterceptorClient.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/client/SqlInterceptorClient.java index 23be157143..648e1fae79 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/client/SqlInterceptorClient.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/client/SqlInterceptorClient.java @@ -15,6 +15,9 @@ */ package com.oceanbase.odc.service.integration.client; +import java.util.HashMap; +import java.util.Map; + import javax.annotation.PostConstruct; import org.apache.http.client.HttpClient; @@ -25,6 +28,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import com.oceanbase.odc.common.util.MapUtils; import com.oceanbase.odc.core.shared.Verify; import com.oceanbase.odc.core.shared.constant.ErrorCodes; import com.oceanbase.odc.core.shared.exception.ExternalServiceError; @@ -32,10 +36,14 @@ import com.oceanbase.odc.service.integration.HttpOperationService; import com.oceanbase.odc.service.integration.model.ApprovalProperties; import com.oceanbase.odc.service.integration.model.Encryption; +import com.oceanbase.odc.service.integration.model.IntegrationProperties; +import com.oceanbase.odc.service.integration.model.IntegrationProperties.ApiProperties; import com.oceanbase.odc.service.integration.model.IntegrationProperties.HttpProperties; import com.oceanbase.odc.service.integration.model.OdcIntegrationResponse; +import com.oceanbase.odc.service.integration.model.SqlCheckResult; import com.oceanbase.odc.service.integration.model.SqlCheckStatus; import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties; +import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties.CallBackProperties; import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties.CheckProperties; import com.oceanbase.odc.service.integration.model.TemplateVariables; import com.oceanbase.odc.service.integration.model.TemplateVariables.Variable; @@ -82,40 +90,65 @@ public void init() { * @param variables Template variables for building request, more details reference {@link Variable} * @return The check result {@link SqlCheckStatus} of SQL content */ - public SqlCheckStatus check(@NonNull SqlInterceptorProperties properties, TemplateVariables variables) { + public SqlCheckResult check(@NonNull SqlInterceptorProperties properties, TemplateVariables variables) { CheckProperties check = properties.getApi().getCheck(); HttpProperties http = properties.getHttp(); Encryption encryption = properties.getEncryption(); - HttpUriRequest request; - try { - request = httpService.buildHttpRequest(check, http, encryption, variables); - } catch (Exception e) { - throw new UnexpectedException("Build request failed: " + e.getMessage()); - } - OdcIntegrationResponse response; - try { - response = httpClient.execute(request, new OdcIntegrationResponseHandler()); - } catch (Exception e) { - throw new ExternalServiceError(ErrorCodes.ExternalServiceError, - "Request execute failed: " + e.getMessage()); - } - response.setContent(EncryptionUtil.decrypt(response.getContent(), encryption)); + OdcIntegrationResponse response = getIntegrationResponse(http, check, variables, encryption); try { + SqlCheckStatus sqlCheckStatus = null; String expression = check.getRequestSuccessExpression(); boolean valid = httpService.extractHttpResponse(response, expression, Boolean.class); Verify.verify(valid, "Response is invalid, except: " + expression + ", response body: " + response); if (httpService.extractHttpResponse(response, check.getInWhiteListExpression(), Boolean.class)) { - return SqlCheckStatus.IN_WHITE_LIST; + sqlCheckStatus = SqlCheckStatus.IN_WHITE_LIST; } else if (httpService.extractHttpResponse(response, check.getInBlackListExpression(), Boolean.class)) { - return SqlCheckStatus.IN_BLACK_LIST; + sqlCheckStatus = SqlCheckStatus.IN_BLACK_LIST; } else if (httpService.extractHttpResponse(response, check.getNeedReviewExpression(), Boolean.class)) { - return SqlCheckStatus.NEED_REVIEW; + sqlCheckStatus = SqlCheckStatus.NEED_REVIEW; } else { throw new RuntimeException( "Response mismatch any check result expression, response body: " + response.getContent()); } + // try extract value from response for future request + Map extractedResponse = new HashMap<>(); + CallBackProperties callBackProperties = check.getCallback(); + if (null != callBackProperties && !MapUtils.isEmpty(callBackProperties.getResponseExtractExpressions())) { + for (Map.Entry responseExtractExpressionEntrySet : callBackProperties + .getResponseExtractExpressions().entrySet()) { + String key = responseExtractExpressionEntrySet.getKey(); + String responseExtractExpression = responseExtractExpressionEntrySet.getValue(); + String value = extractedResponse.put(key, + httpService.extractHttpResponse(response, responseExtractExpression, String.class)); + if (null != value) { + extractedResponse.put(key, value); + } + } + } + return new SqlCheckResult(extractedResponse, sqlCheckStatus); } catch (Exception e) { throw new UnexpectedException("Extract SQL check result failed: " + e.getMessage()); } } + + public OdcIntegrationResponse getIntegrationResponse(HttpProperties http, + @NonNull IntegrationProperties.ApiProperties api, TemplateVariables variables, Encryption encryption) { + HttpUriRequest request; + try { + request = httpService.buildHttpRequest(api, http, encryption, variables); + } catch (Exception e) { + throw new UnexpectedException("Build request failed: " + e.getMessage()); + } + OdcIntegrationResponse response; + try { + response = httpClient.execute(request, new OdcIntegrationResponseHandler()); + } catch (Exception e) { + throw new ExternalServiceError(ErrorCodes.ExternalServiceError, + "Request execute failed: " + e.getMessage()); + } + response.setContent(EncryptionUtil.decrypt(response.getContent(), encryption)); + log.info("sqlInterceptorClient getIntegrationResponse request = {}, response ={}", request, + response.getContent()); + return response; + } } diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlCheckResult.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlCheckResult.java new file mode 100644 index 0000000000..916ff4bea6 --- /dev/null +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlCheckResult.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.service.integration.model; + +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * @author longpeng.zlp + * @date 2025/6/19 13:49 + */ +@Data +@AllArgsConstructor +public class SqlCheckResult { + private Map extractedResponse; + private SqlCheckStatus sqlCheckStatus; +} diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlInterceptorProperties.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlInterceptorProperties.java index fdc67fa1ac..127b501636 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlInterceptorProperties.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/SqlInterceptorProperties.java @@ -15,6 +15,8 @@ */ package com.oceanbase.odc.service.integration.model; +import java.util.Map; + import javax.validation.Valid; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @@ -51,6 +53,40 @@ public static class CheckProperties extends ApiProperties { private String inBlackListExpression; @NotBlank private String needReviewExpression; + // call back properties + private CallBackProperties callback; + + public ApiProperties onNeedReview() { + if (null != callback) { + return callback.onNeedReview; + } else { + return null; + } + } + + public ApiProperties onInBlackList() { + if (null != callback) { + return callback.onInBlackList; + } else { + return null; + } + } + + public ApiProperties onInWhiteList() { + if (null != callback) { + return callback.onInWhiteList; + } else { + return null; + } + } + } + + @Data + public static class CallBackProperties { + private ApiProperties onNeedReview; + private ApiProperties onInWhiteList; + private ApiProperties onInBlackList; + private Map responseExtractExpressions; } public static SqlInterceptorProperties from(IntegrationConfig config) { diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/TemplateVariables.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/TemplateVariables.java index ea01ca7d94..ba4a743bb0 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/TemplateVariables.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/integration/model/TemplateVariables.java @@ -73,7 +73,8 @@ public enum Variable { PROJECT_OWNER_IDS("project.owner.ids"), PROJECT_OWNER_ACCOUNTS("project.owner.accounts"), PROJECT_OWNER_NAMES("project.owner.names"), - ODC_TASK_URL("odc.task.url"); + ODC_TASK_URL("odc.task.url"), + EXTERNAL_PROPERTIES("external.response"); private final String key; diff --git a/server/odc-service/src/test/java/com/oceanbase/odc/service/intergation/ExternalSqlInterceptorTest.java b/server/odc-service/src/test/java/com/oceanbase/odc/service/intergation/ExternalSqlInterceptorTest.java new file mode 100644 index 0000000000..e6c3174557 --- /dev/null +++ b/server/odc-service/src/test/java/com/oceanbase/odc/service/intergation/ExternalSqlInterceptorTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.service.intergation; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import org.apache.commons.lang3.StringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.oceanbase.odc.common.util.YamlUtils; +import com.oceanbase.odc.core.session.ConnectionSession; +import com.oceanbase.odc.core.session.ConnectionSessionConstants; +import com.oceanbase.odc.core.shared.constant.DialectType; +import com.oceanbase.odc.core.shared.constant.OrganizationType; +import com.oceanbase.odc.core.sql.split.SqlCommentProcessor; +import com.oceanbase.odc.metadb.integration.IntegrationEntity; +import com.oceanbase.odc.service.config.SystemConfigService; +import com.oceanbase.odc.service.config.model.Configuration; +import com.oceanbase.odc.service.iam.auth.AuthenticationFacade; +import com.oceanbase.odc.service.iam.model.User; +import com.oceanbase.odc.service.integration.ExternalSqlInterceptor; +import com.oceanbase.odc.service.integration.HttpOperationService; +import com.oceanbase.odc.service.integration.HttpOperationService.IntegrationConfigProperties; +import com.oceanbase.odc.service.integration.IntegrationService; +import com.oceanbase.odc.service.integration.client.SqlInterceptorClient; +import com.oceanbase.odc.service.integration.model.Encryption; +import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties; +import com.oceanbase.odc.service.regulation.ruleset.RuleService; +import com.oceanbase.odc.service.regulation.ruleset.SqlConsoleRuleService; +import com.oceanbase.odc.service.session.model.AsyncExecuteContext; +import com.oceanbase.odc.service.session.model.SqlAsyncExecuteReq; +import com.oceanbase.odc.service.session.model.SqlAsyncExecuteResp; +import com.oceanbase.odc.service.task.net.HttpServerContainer; +import com.oceanbase.odc.service.task.net.RequestHandler; +import com.oceanbase.odc.service.task.supervisor.PortDetector; + +import io.netty.handler.codec.http.HttpMethod; +import lombok.extern.slf4j.Slf4j; + +/** + * @author longpeng.zlp + * @date 2025/6/19 17:25 + */ +@Slf4j +public class ExternalSqlInterceptorTest { + + private static final String CONFIG_TEMPLATE = "http:\n" + + " connectTimeoutSeconds: 5\n" + + " socketTimeoutSeconds: 30\n" + + "api:\n" + + " check:\n" + + " method: POST\n" + + " url: http://127.0.0.1:${port}/c/platformApi/checkControlSensitiveOdc\n" + + " headers:\n" + + " Content-Type: application/json;charset=UTF-8\n" + + " Accept: application/json\n" + + " body:\n" + + " type: RAW\n" + + " content: |-\n" + + " {\n" + + " \"sqlStatement\": \"${sql.content}\", \"accountName\":\"${connection.properties.accountName}\", \"collectionIds\":\"${connection.properties.collectionIds}\",\"dbInstance\":\"${connection.properties.dbInstance}\",\"instanceType\":\"${connection.properties.instanceType}\",\"ipAddress\":\"${connection.properties.ipAddress}\",\"operateSessionId\":\"${connection.properties.operateSessionId}\",\"reservedFields1\":\"${connection.properties.reservedFields1}\", \"reservedFields2\":\"${connection.properties.reservedFields2}\", \"reservedFields3\":\"${connection.properties.reservedFields3}\", \"serviceName\":\"${connection.properties.serviceName}\", \"ssoType\":\"${connection.properties.ssoType}\", \"userCode\":\"${connection.properties.userCode}\"\n" + + " }\n" + + " callback:\n" + + " onNeedReview:\n" + + " method: POST\n" + + " url: http://127.0.0.1:${port}/c/platformApi/cancel\n" + + " headers:\n" + + " Content-Type: application/json;charset=UTF-8\n" + + " Accept: application/json\n" + + " body:\n" + + " type: RAW\n" + + " content: |-\n" + + " {\n" + + " \"uuid\": \"${external.response.key1}\"\n" + + " }\n" + + " requestEncrypted: false\n" + + " requestSuccessExpression: '[checkResult] == \"2\"'\n" + + " responseEncrypted: false\n" + + " onInWhiteList:\n" + + " method: POST\n" + + " url: http://127.0.0.1:${port}/c/platformApi/cancel\n" + + " headers:\n" + + " Content-Type: application/json;charset=UTF-8\n" + + " Accept: application/json\n" + + " body:\n" + + " type: RAW\n" + + " content: |-\n" + + " {\n" + + " \"uuid\": \"${key1}\"\n" + + " }\n" + + " responseExtractExpressions:\n" + + " key1: '[uuid]'\n" + + " key2: '[vala]'\n" + + " requestEncrypted: false\n" + + " requestSuccessExpression: '[resultCode] == \"0\"'\n" + + " responseEncrypted: false\n" + + " inWhiteListExpression: '[checkResult] == \"1\"'\n" + + " inBlackListExpression: '[checkResult] == \"4\"'\n" + + " needReviewExpression: '[checkResult] == \"2\"'\n"; + + private int port = PortDetector.getInstance().getPort(); + private MockHttpServer mockHttpServer; + private String config; + private SqlInterceptorProperties sqlInterceptorProperties; + private ExternalSqlInterceptor externalSqlInterceptor; + private SqlInterceptorClient sqlInterceptorClient; + private HttpOperationService httpOperationService; + private ConnectionSession connectionSession; + + @Before + public void before() { + mockHttpServer = new MockHttpServer(port);; + mockHttpServer.start(); + config = CONFIG_TEMPLATE.replace("${port}", String.valueOf(port)); + sqlInterceptorProperties = YamlUtils.from(config, SqlInterceptorProperties.class); + sqlInterceptorProperties.setEncryption(Encryption.empty()); + externalSqlInterceptor = new ExternalSqlInterceptor(); + // mock httpOperationService + httpOperationService = new HttpOperationService(); + IntegrationConfigProperties integrationConfigProperties = new IntegrationConfigProperties(); + setField(httpOperationService, "configProperties", integrationConfigProperties); + // mock sql interceptor client + sqlInterceptorClient = new SqlInterceptorClient(); + setField(sqlInterceptorClient, "httpService", httpOperationService); + sqlInterceptorClient.init(); + setField(externalSqlInterceptor, "sqlInterceptorClient", sqlInterceptorClient); + // mock integrationService + IntegrationService integrationService = Mockito.mock(IntegrationService.class); + IntegrationEntity integrationEntity = new IntegrationEntity(); + integrationEntity.setId(11L); + Mockito.when(integrationService.findIntegrationById(Mockito.anyLong())) + .thenReturn(Optional.of(integrationEntity)); + Mockito.when(integrationService.getIntegrationProperties(Mockito.anyLong())) + .thenReturn(sqlInterceptorProperties); + setField(externalSqlInterceptor, "integrationService", integrationService); + // mock authenticationFacade + AuthenticationFacade authenticationFacade = Mockito.mock(AuthenticationFacade.class); + Mockito.when(authenticationFacade.currentUserId()).thenReturn(1L); + User user = new User(); + user.setId(1024L); + user.setOrganizationType(OrganizationType.TEAM); + Mockito.when(authenticationFacade.currentUser()).thenReturn(user); + Mockito.when(authenticationFacade.currentUsername()).thenReturn("test"); + Mockito.when(authenticationFacade.currentUserAccountName()).thenReturn("testAccount"); + setField(externalSqlInterceptor, "authenticationFacade", authenticationFacade); + // mock systemConfigService + SystemConfigService systemConfigService = Mockito.mock(SystemConfigService.class); + Configuration configuration = new Configuration("odc_url", "127.0.0.1:8888"); + Mockito.when(systemConfigService.queryByKeyPrefix(Mockito.anyString())) + .thenReturn(Arrays.asList(configuration)); + setField(externalSqlInterceptor, "systemConfigService", systemConfigService); + // mock ruleService + RuleService ruleService = Mockito.mock(RuleService.class); + Mockito.when(ruleService.getByRulesetIdAndName(Mockito.anyLong(), Mockito.anyString())) + .thenReturn(Optional.empty()); + setField(externalSqlInterceptor, "ruleService", ruleService); + // mock sqlConsoleRuleService + SqlConsoleRuleService sqlConsoleRuleService = Mockito.mock(SqlConsoleRuleService.class); + Mockito.when( + sqlConsoleRuleService.getProperties(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(Optional.of(1)); + setField(externalSqlInterceptor, "sqlConsoleRuleService", sqlConsoleRuleService); + // set connectionSession + connectionSession = Mockito.mock(ConnectionSession.class); + Mockito.when(connectionSession.getAttribute(ConnectionSessionConstants.RULE_SET_ID_NAME)).thenReturn(1L); + SqlCommentProcessor sqlCommentProcessor = new SqlCommentProcessor(DialectType.OB_MYSQL, ";"); + Mockito.when(connectionSession.getAttribute(ConnectionSessionConstants.SQL_COMMENT_PROCESSOR_KEY)) + .thenReturn(sqlCommentProcessor); + Mockito.when(connectionSession.getDialectType()).thenReturn(DialectType.OB_MYSQL); + + } + + @Test + public void testSQLInterceptor() { + SqlAsyncExecuteReq request = new SqlAsyncExecuteReq(); + request.setSql("select 1"); + SqlAsyncExecuteResp resp = new SqlAsyncExecuteResp(true); + externalSqlInterceptor.doPreHandle(request, resp, connectionSession, Mockito.mock(AsyncExecuteContext.class)); + List requestResult = mockHttpServer.accessInfo; + Assert.assertEquals(requestResult.size(), 2); + Assert.assertTrue(StringUtils.containsIgnoreCase(requestResult.get(1), "\"uuid\": \"myuid\"")); + } + + @After + public void clear() throws Exception { + if (null != mockHttpServer) { + mockHttpServer.stop(); + } + } + + private static void setField(Object object, String fieldName, Object value) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(object, value); + Assert.assertEquals(field.get(object), value); + } catch (Exception e) { + throw new RuntimeException("Failed to set field: " + fieldName, e); + } + } + + private static final class MockHttpServer extends HttpServerContainer> { + private final int port; + private List accessInfo = new ArrayList<>(); + + public MockHttpServer(int port) { + this.port = port; + } + + @Override + protected int getPort() { + return port; + } + + @Override + protected RequestHandler> getRequestHandler() { + return new RequestHandler>() { + @Override + public Map process(HttpMethod httpMethod, String uri, String requestData) { + log.info("receive request, uri: {}, requestData: {}", uri, requestData); + accessInfo.add(uri + "/" + requestData); + Map ret = new HashMap<>(); + ret.put("uuid", "myuid"); + ret.put("resultCode", "0"); + + if (StringUtils.containsIgnoreCase(uri, "check")) { + ret.put("checkResult", "2"); + ret.put("key1", "sssss"); + ret.put("vala", "vvvvvvv"); + ret.put("key3", "ggg"); + } + return ret; + } + + @Override + public Map processException(Throwable e) { + Map ret = new HashMap<>(); + ret.put("uuid", "myuid"); + ret.put("resultCode", "0"); + ret.put("checkResult", "4"); + return ret; + } + }; + } + + @Override + protected String getModuleName() { + return "mock http server"; + } + + @Override + protected Thread createThread(Runnable r) { + return new Thread(r); + } + + @Override + protected Consumer portConsumer() { + return (port) -> { + }; + } + } +}