diff --git a/.github/workflows/e2e-k8s.yml b/.github/workflows/e2e-k8s.yml index 6072527b0aaf..f0fa44b76164 100644 --- a/.github/workflows/e2e-k8s.yml +++ b/.github/workflows/e2e-k8s.yml @@ -209,6 +209,8 @@ jobs: script: e2e-sofa-sync - case: shenyu-e2e-case-grpc script: e2e-grpc-sync + - case: shenyu-e2e-case-websocket + script: e2e-websocket-sync steps: - uses: actions/checkout@v2 with: diff --git a/shenyu-e2e/pom.xml b/shenyu-e2e/pom.xml index 2e38489c72a9..737308092550 100644 --- a/shenyu-e2e/pom.xml +++ b/shenyu-e2e/pom.xml @@ -57,6 +57,7 @@ 3.1.0 31.1-jre 4.4 + 1.5.1 @@ -67,6 +68,12 @@ + + org.java-websocket + Java-WebSocket + ${websocket.version} + + com.google.guava guava diff --git a/shenyu-e2e/shenyu-e2e-case/pom.xml b/shenyu-e2e/shenyu-e2e-case/pom.xml index a3e3e11a529b..e72a646df72e 100644 --- a/shenyu-e2e/shenyu-e2e-case/pom.xml +++ b/shenyu-e2e/shenyu-e2e-case/pom.xml @@ -37,6 +37,7 @@ shenyu-e2e-case-motan shenyu-e2e-case-grpc shenyu-e2e-case-brpc + shenyu-e2e-case-websocket diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/e2e-websocket-sync.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/e2e-websocket-sync.sh new file mode 100644 index 000000000000..5fb198dc384c --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/e2e-websocket-sync.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +docker shenyu-examples-websocket:latest | sudo k3s ctr images import - + +# init kubernetes for mysql +SHENYU_TESTCASE_DIR=$(dirname "$(dirname "$(dirname "$(dirname "$0")")")") +bash "${SHENYU_TESTCASE_DIR}"/k8s/script/storage/storage_init_mysql.sh + +# init register center +CUR_PATH=$(readlink -f "$(dirname "$0")") +PRGDIR=$(dirname "$CUR_PATH") +echo "$PRGDIR" +kubectl apply -f "${PRGDIR}"/shenyu-cm.yml + +# init shenyu sync +SYNC_ARRAY=("websocket" "http" "zookeeper" "etcd") +#SYNC_ARRAY=("websocket" "nacos") +MIDDLEWARE_SYNC_ARRAY=("zookeeper" "etcd" "nacos") +for sync in ${SYNC_ARRAY[@]}; do + echo -e "------------------\n" + kubectl apply -f "$SHENYU_TESTCASE_DIR"/k8s/shenyu-mysql.yml + sleep 30s + echo "[Start ${sync} synchronous] create shenyu-admin-${sync}.yml shenyu-bootstrap-${sync}.yml shenyu-examples-websocket.yml" + # shellcheck disable=SC2199 + # shellcheck disable=SC2076 + if [[ "${MIDDLEWARE_SYNC_ARRAY[@]}" =~ "${sync}" ]]; then + kubectl apply -f "${SHENYU_TESTCASE_DIR}"/k8s/shenyu-"${sync}".yml + sleep 10s + fi + kubectl apply -f "${SHENYU_TESTCASE_DIR}"/k8s/sync/shenyu-admin-"${sync}".yml + sh "${CUR_PATH}"/healthcheck.sh http://localhost:31095/actuator/health + sh "${CUR_PATH}"/healthcheck.sh http://localhost:30761/actuator/health + kubectl apply -f "${SHENYU_TESTCASE_DIR}"/k8s/sync/shenyu-bootstrap-"${sync}".yml + sh "${CUR_PATH}"/healthcheck.sh http://localhost:31195/actuator/health + kubectl apply -f "${PRGDIR}"/shenyu-examples-springcloud.yml + sh "${CUR_PATH}"/healthcheck.sh http://localhost:30884/actuator/health + sleep 10s + kubectl get pod -o wide + + ## run e2e-test + ./mvnw -B -f ./shenyu-e2e/pom.xml -pl shenyu-e2e-case/shenyu-e2e-case-websocket -am test + # shellcheck disable=SC2181 + if (($?)); then + echo "${sync}-sync-e2e-test failed" + echo "shenyu-admin log:" + echo "------------------" + kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')" + echo "shenyu-bootstrap log:" + echo "------------------" + kubectl logs "$(kubectl get pod -o wide | grep shenyu-bootstrap | awk '{print $1}')" + exit 1 + fi + kubectl delete -f "${SHENYU_TESTCASE_DIR}"/k8s/shenyu-mysql.yml + kubectl delete -f "${SHENYU_TESTCASE_DIR}"/k8s/sync/shenyu-admin-"${sync}".yml + kubectl delete -f "${SHENYU_TESTCASE_DIR}"/k8s/sync/shenyu-bootstrap-"${sync}".yml + kubectl delete -f "${PRGDIR}"/shenyu-examples-websocket.yml + # shellcheck disable=SC2199 + # shellcheck disable=SC2076 + if [[ "${MIDDLEWARE_SYNC_ARRAY[@]}" =~ "${sync}" ]]; then + kubectl delete -f "${SHENYU_TESTCASE_DIR}"/k8s/shenyu-"${sync}".yml + fi + echo "[Remove ${sync} synchronous] delete shenyu-admin-${sync}.yml shenyu-bootstrap-${sync}.yml shenyu-examples-websocket.yml" +done diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/healthcheck.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/healthcheck.sh new file mode 100644 index 000000000000..1159a0f41797 --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/healthcheck.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +for loop in $(seq 1 30); do + status=$(curl -s -o /dev/null -w %{http_code} -X GET "${1}" -H "accept: */*") + echo -e "${loop} curl ${1} response $status" + if [ "$status" -eq 200 ]; then + break + fi + sleep 2s +done + +status=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${1}" -H "accept: */*") + +if [ "$status" -eq 200 ]; then + echo -e "\n-------------------" + echo -e "Success to ${1} send request: $status" + echo -e "\n-------------------" + exit 0 +fi +echo -e "\n-------------------" +echo -e "Failed to send request from shenyu-admin : $status" +echo -e "\n-------------------" +exit 1 diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/shenyu-examples-websocket.yml b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/shenyu-examples-websocket.yml new file mode 100644 index 000000000000..ba28ae014c02 --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/shenyu-examples-websocket.yml @@ -0,0 +1,73 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shenyu-examples-websocket + labels: + app: shenyu-examples-websocket +spec: + replicas: 1 + selector: + matchLabels: + app: shenyu-examples-websocket + strategy: {} + template: + metadata: + labels: + app: shenyu-examples-websocket + spec: + containers: + - image: shenyu-examples-websocket:latest + name: shenyu-examples-websocket + livenessProbe: + exec: + command: + - wget -q -O - http://localhost:8001/actuator/health | grep UP || exit 1 + initialDelaySeconds: 30 + failureThreshold: 3 + timeoutSeconds: 2 + env: + - name: shenyu.register.serverLists + value: http://shenyu-admin:9095 + - name: websocket.registry.address + value: zookeeper://shenyu-zookeeper:2181 + ports: + - containerPort: 8848 + - containerPort: 31188 + imagePullPolicy: IfNotPresent + restartPolicy: Always +status: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: shenyu-examples-websocket + labels: + app: shenyu-examples-websocket +spec: + selector: + app: shenyu-examples-websocket + type: NodePort + ports: + - name: "8848" + port: 8848 + targetPort: 8848 + nodePort: 31191 +status: + loadBalancer: {} diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/pom.xml b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/pom.xml new file mode 100644 index 000000000000..024fd51c2370 --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/pom.xml @@ -0,0 +1,43 @@ + + + + + org.apache.shenyu + shenyu-e2e-case + 0.0.1-SNAPSHOT + + + 4.0.0 + + shenyu-e2e-case-websocket + + + 1.5.1 + + + + + org.java-websocket + Java-WebSocket + 1.5.3 + + + + \ No newline at end of file diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/DataSynTest.java b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/DataSynTest.java new file mode 100644 index 000000000000..70564adf4685 --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/DataSynTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyue.e2e.testcase.websocket; + +import org.apache.shenyu.e2e.client.WaitDataSync; +import org.apache.shenyu.e2e.client.admin.AdminClient; +import org.apache.shenyu.e2e.client.gateway.GatewayClient; +import org.apache.shenyu.e2e.engine.annotation.ShenYuTest; +import org.apache.shenyu.e2e.enums.ServiceTypeEnum; +import org.junit.jupiter.api.Test; + +/** + * Testing the correctness of etcd data synchronization method. + */ +@ShenYuTest(environments = { + @ShenYuTest.Environment( + serviceName = "shenyu-e2e-admin", + service = @ShenYuTest.ServiceConfigure(moduleName = "shenyu-e2e", + baseUrl = "http://localhost:9095", + type = ServiceTypeEnum.SHENYU_ADMIN, + parameters = { + @ShenYuTest.Parameter(key = "username", value = "admin"), + @ShenYuTest.Parameter(key = "password", value = "123456") + } + ) + ), + @ShenYuTest.Environment( + serviceName = "shenyu-e2e-gateway", + service = @ShenYuTest.ServiceConfigure(moduleName = "shenyu-e2e", + baseUrl = "http://localhost:9195", + type = ServiceTypeEnum.SHENYU_GATEWAY + ) + ) +}) +public class DataSynTest { + + @Test + void testDataSyn(final AdminClient adminClient, final GatewayClient gatewayClient) throws Exception { + adminClient.login(); + WaitDataSync.waitAdmin2GatewayDataSyncEquals(adminClient::listAllSelectors, gatewayClient::getSelectorCache, adminClient); + WaitDataSync.waitAdmin2GatewayDataSyncEquals(adminClient::listAllMetaData, gatewayClient::getMetaDataCache, adminClient); + WaitDataSync.waitAdmin2GatewayDataSyncEquals(adminClient::listAllRules, gatewayClient::getRuleCache, adminClient); + } +} diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/WebSocketPluginCases.java b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/WebSocketPluginCases.java new file mode 100644 index 000000000000..00afdaec1bf0 --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/WebSocketPluginCases.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyue.e2e.testcase.websocket; + +import com.google.common.collect.Lists; +import org.apache.shenyu.e2e.engine.scenario.ShenYuScenarioProvider; +import org.apache.shenyu.e2e.engine.scenario.specification.ScenarioSpec; +import org.apache.shenyu.e2e.engine.scenario.specification.ShenYuBeforeEachSpec; +import org.apache.shenyu.e2e.engine.scenario.specification.ShenYuCaseSpec; +import org.apache.shenyu.e2e.engine.scenario.specification.ShenYuScenarioSpec; + +import java.util.List; + +import static org.apache.shenyu.e2e.engine.scenario.function.WebSocketCheckers.exists; + +public class WebSocketPluginCases implements ShenYuScenarioProvider { + + private static final String WEBSOCKET_URI = "ws://localhost:9195/ws-annotation/myWs?token=Jack"; + + @Override + public List get() { + return Lists.newArrayList( + testWebSocket() + ); + } + + /** + * test websocket + * + * @return ShenYuScenarioSpec + */ + public ShenYuScenarioSpec testWebSocket() { + return ShenYuScenarioSpec.builder() + .name("single-websocket uri starts_with]") + .beforeEachSpec(ShenYuBeforeEachSpec.builder() + .checker(exists("/ws-annotation/myWs", "Hello ShenYu", "server send message:Hello ShenYu")) + .build()) + .caseSpec(ShenYuCaseSpec.builder() + .addExists("/ws-annotation/myWs", "Hello ShenYu", "server send message:Hello ShenYu") + .addNotExists("/ws-native/myWs", "Hello ShenYu") + .build()) + .build(); + } +} diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/WebSocketPluginTest.java b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/WebSocketPluginTest.java new file mode 100644 index 000000000000..bb4d3a82979d --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/src/test/java/org/apache/shenyue/e2e/testcase/websocket/WebSocketPluginTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyue.e2e.testcase.websocket; + +import org.apache.shenyu.e2e.client.WaitDataSync; +import org.apache.shenyu.e2e.client.admin.AdminClient; +import org.apache.shenyu.e2e.client.gateway.GatewayClient; +import org.apache.shenyu.e2e.engine.annotation.ShenYuScenario; +import org.apache.shenyu.e2e.engine.annotation.ShenYuTest; +import org.apache.shenyu.e2e.engine.scenario.specification.CaseSpec; +import org.apache.shenyu.e2e.enums.ServiceTypeEnum; +import org.junit.jupiter.api.BeforeAll; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@ShenYuTest(environments = { + @ShenYuTest.Environment( + serviceName = "shenyu-e2e-admin", + service = @ShenYuTest.ServiceConfigure(moduleName = "shenyu-e2e", + baseUrl = "http://localhost:9095", + type = ServiceTypeEnum.SHENYU_ADMIN, + parameters = { + @ShenYuTest.Parameter(key = "username", value = "admin"), + @ShenYuTest.Parameter(key = "password", value = "123456") + } + ) + ), + @ShenYuTest.Environment( + serviceName = "shenyu-e2e-gateway", + service = @ShenYuTest.ServiceConfigure(moduleName = "shenyu-e2e", + baseUrl = "http://localhost:9195", + type = ServiceTypeEnum.SHENYU_GATEWAY + ) + ) +}) +/** + * Testing websocket plugin. + */ +public class WebSocketPluginTest { + + private static final Logger LOG = LoggerFactory.getLogger(WebSocketPluginTest.class); + + @BeforeAll + static void setup(final AdminClient adminClient, final GatewayClient gatewayClient) throws Exception { + adminClient.login(); + WaitDataSync.waitAdmin2GatewayDataSyncEquals(adminClient::listAllRules, gatewayClient::getRuleCache, adminClient); + WaitDataSync.waitAdmin2GatewayDataSyncEquals(adminClient::listAllSelectors, gatewayClient::getSelectorCache, adminClient); + WaitDataSync.waitAdmin2GatewayDataSyncEquals(adminClient::listAllMetaData, gatewayClient::getMetaDataCache, adminClient); + WaitDataSync.waitAdmin2GatewayDataSyncEquals(adminClient::listAllRules, gatewayClient::getRuleCache, adminClient); + LOG.info("start websocket plugin"); + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("id", "1"); + formData.add("name", "websocket"); + formData.add("enabled", "true"); + formData.add("role", "Proxy"); + formData.add("sort", "140"); + adminClient.changePluginStatus("8", formData); + WaitDataSync.waitGatewayPluginUse(gatewayClient, "org.apache.shenyu.plugin.websocket.WebSocketPlugin"); + } + + @ShenYuScenario(provider = WebSocketPluginCases.class) + void testWebSocket(final GatewayClient gateway, final CaseSpec spec) { + spec.getWebSocketVerifiers().forEach(webSocketVerifier -> webSocketVerifier.verify(gateway.getWebSocketClientSupplier().get(), gateway)); + } +} diff --git a/shenyu-e2e/shenyu-e2e-client/src/main/java/org/apache/shenyu/e2e/client/gateway/GatewayClient.java b/shenyu-e2e/shenyu-e2e-client/src/main/java/org/apache/shenyu/e2e/client/gateway/GatewayClient.java index 8966e3b05cc6..308efb83d25b 100644 --- a/shenyu-e2e/shenyu-e2e-client/src/main/java/org/apache/shenyu/e2e/client/gateway/GatewayClient.java +++ b/shenyu-e2e/shenyu-e2e-client/src/main/java/org/apache/shenyu/e2e/client/gateway/GatewayClient.java @@ -27,6 +27,8 @@ import org.apache.shenyu.e2e.model.data.MetaData; import org.apache.shenyu.e2e.model.data.RuleCacheData; import org.apache.shenyu.e2e.model.data.SelectorCacheData; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; @@ -34,10 +36,14 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import static io.restassured.RestAssured.given; @@ -53,7 +59,9 @@ public class GatewayClient extends BaseClient { private static final RestTemplate TEMPLATE = new RestTemplate(); private static final ObjectMapper MAPPER = new ObjectMapper(); - + + private static final ArrayBlockingQueue BLOCKING_QUEUE = new ArrayBlockingQueue<>(1); + private final String scenarioId; private final String baseUrl; @@ -104,6 +112,41 @@ public Supplier getHttpRequesterSupplier() { }) .when(); } + + /** + * get websocket client. + * @return Supplier + */ + public Supplier getWebSocketClientSupplier() { + return () -> { + try { + return new WebSocketClient(new URI(getBaseUrl().replaceAll("http", "ws"))) { + @Override + public void onOpen(ServerHandshake handshakeData) { + log.info("Open websocket connection successfully"); + } + + @Override + public void onMessage(String message) { + BLOCKING_QUEUE.add(message); + log.info("Receive Message: " + message); + } + + @Override + public void onClose(int code, String reason, boolean remote) { + } + + @Override + public void onError(Exception ex) { + ex.printStackTrace(); + } + }; + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid WebSocket URI", e); + } + }; + + } /** * get meta data cache. @@ -172,4 +215,12 @@ public Map getPlugins() { List body = response.getBody(); return (Map) body.get(0); } + + public String getWebSocketMessage() { + try { + return BLOCKING_QUEUE.poll(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } } diff --git a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketChecker.java b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketChecker.java new file mode 100644 index 000000000000..0e56f223ba6e --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketChecker.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.e2e.engine.scenario.function; + +import org.apache.shenyu.e2e.client.gateway.GatewayClient; +import org.java_websocket.client.WebSocketClient; +import org.slf4j.MDC; + +import java.util.function.Supplier; + +/** + * WebSocket Checker interface. + */ +@FunctionalInterface +public interface WebSocketChecker extends Checker, WebSocketVerifier { + + default void check(GatewayClient client) { + check(client.getWebSocketClientSupplier(), client); + } + + /** + * check request specification. + * @param supplier supplier + */ + default void check(Supplier supplier, GatewayClient client) { + try { + verify(supplier.get(), client); + } catch (AssertionError e) { + throw new AssertionError("failed to request " + MDC.get("endpoint"), e); + } + } +} diff --git a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketCheckers.java b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketCheckers.java new file mode 100644 index 000000000000..e4ab9dea40cb --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketCheckers.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.e2e.engine.scenario.function; + +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.exceptions.WebsocketNotConnectedException; +import org.junit.jupiter.api.Assertions; + +import java.lang.reflect.Field; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; + +/** + * Check if the endpoint exists. + */ +public class WebSocketCheckers { + + public static final ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(1); + + public static WebSocketChecker exists(final String endpoint, final String sendMessage, final String receiveMessage) { + return (websocketClient, gatewayClient) -> { + try { + updateWebSocketClientURI(websocketClient, endpoint); + + websocketClient.connectBlocking(); + websocketClient.send(sendMessage); + Optional.of(websocketClient) + .filter(WebSocketClient::isOpen) + .ifPresent(client -> Assertions.assertEquals(receiveMessage, gatewayClient.getWebSocketMessage())); + } catch (AssertionError | InterruptedException | RuntimeException error) { + Assertions.fail("websocket endpoint '" + endpoint + "' not exists", error); + } catch (NoSuchFieldException | IllegalAccessException | URISyntaxException e) { + throw new RuntimeException(e); + } + }; + } + + public static WebSocketChecker notExists(final String endpoint, final String message) { + return (websocketClient, gatewayClient) -> { + try { + updateWebSocketClientURI(websocketClient, endpoint); + + websocketClient.connectBlocking(); + Optional.of(websocketClient) + .filter(WebSocketClient::isOpen) + .ifPresent(client -> Assertions.fail("websocket endpoint '" + endpoint + "' exists")); + } catch (AssertionError | InterruptedException | WebsocketNotConnectedException error) { + Assertions.fail("websocket endpoint '" + endpoint + "' not exists", error); + } catch (URISyntaxException | NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }; + } + + private static void updateWebSocketClientURI(WebSocketClient client, String endpoint) + throws NoSuchFieldException, IllegalAccessException, URISyntaxException { + Field uriField = WebSocketClient.class.getDeclaredField("uri"); + uriField.setAccessible(true); + URI originalUri = client.getURI(); + URI updatedUri = new URI(originalUri + endpoint); + uriField.set(client, updatedUri); + } +} diff --git a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketVerifier.java b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketVerifier.java new file mode 100644 index 000000000000..957f5471fc08 --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/function/WebSocketVerifier.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.e2e.engine.scenario.function; + +import org.apache.shenyu.e2e.client.gateway.GatewayClient; +import org.java_websocket.client.WebSocketClient; + +/** + * WebSocket Verifier interface. + */ +public interface WebSocketVerifier { + + WebSocketVerifier DEFAULT = (webSocketClient, gatewayClient) -> { + }; + + /** + * Verify WebSocketClient. + * @param client WebSocketClient + */ + void verify(WebSocketClient client, GatewayClient gatewayClient); +} diff --git a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/CaseSpec.java b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/CaseSpec.java index cb30193e5d89..8e1314d52eab 100644 --- a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/CaseSpec.java +++ b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/CaseSpec.java @@ -19,6 +19,7 @@ import org.apache.shenyu.e2e.engine.annotation.ShenYuScenarioParameter; import org.apache.shenyu.e2e.engine.scenario.function.Verifier; +import org.apache.shenyu.e2e.engine.scenario.function.WebSocketVerifier; import java.util.List; @@ -37,4 +38,9 @@ public interface CaseSpec { */ List getVerifiers(); + /** + * get case spec websocket verifiers. + * @return List + */ + List getWebSocketVerifiers(); } diff --git a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ScenarioSpecLogProxy.java b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ScenarioSpecLogProxy.java index 0bbc75721db3..fb044c189b52 100644 --- a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ScenarioSpecLogProxy.java +++ b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ScenarioSpecLogProxy.java @@ -17,11 +17,8 @@ package org.apache.shenyu.e2e.engine.scenario.specification; +import org.apache.shenyu.e2e.engine.scenario.function.*; import org.apache.shenyu.e2e.model.ResourcesData; -import org.apache.shenyu.e2e.engine.scenario.function.Checker; -import org.apache.shenyu.e2e.engine.scenario.function.Deleter; -import org.apache.shenyu.e2e.engine.scenario.function.Verifier; -import org.apache.shenyu.e2e.engine.scenario.function.Waiting; import org.slf4j.MDC; import java.util.List; @@ -74,6 +71,12 @@ public List getVerifiers() { MDC.put("operate", "verify"); return spec.getVerifiers(); } + + @Override + public List getWebSocketVerifiers() { + MDC.put("operate", "websocketVerify"); + return spec.getWebSocketVerifiers(); + } }; } diff --git a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ShenYuCaseSpec.java b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ShenYuCaseSpec.java index 89d138183dad..94ea122d1961 100644 --- a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ShenYuCaseSpec.java +++ b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/scenario/specification/ShenYuCaseSpec.java @@ -21,6 +21,8 @@ import com.google.common.collect.ImmutableList.Builder; import io.restassured.http.Method; import org.apache.shenyu.e2e.engine.scenario.function.Verifier; +import org.apache.shenyu.e2e.engine.scenario.function.WebSocketCheckers; +import org.apache.shenyu.e2e.engine.scenario.function.WebSocketVerifier; import org.hamcrest.Matcher; import java.util.List; @@ -38,9 +40,12 @@ public class ShenYuCaseSpec implements CaseSpec { private final List verifiers; - public ShenYuCaseSpec(final String name, final List verifiers) { + private final List webSocketVerifiers; + + public ShenYuCaseSpec(final String name, final List verifiers, final List webSocketVerifiers) { this.name = name; this.verifiers = verifiers; + this.webSocketVerifiers = webSocketVerifiers; } /** @@ -62,6 +67,11 @@ public String getName() { public List getVerifiers() { return verifiers; } + + @Override + public List getWebSocketVerifiers() { + return webSocketVerifiers; + } /** * builder. @@ -86,6 +96,8 @@ public static class ShenYuTestCaseSpecBuilder { private final Builder builder = ImmutableList.builder(); + private final Builder webSocketBuilder = ImmutableList.builder(); + public ShenYuTestCaseSpecBuilder() { } @@ -112,6 +124,16 @@ public ShenYuTestCaseSpecBuilder add(final Verifier verifier) { builder.add(verifier); return this; } + + /** + * websocket builder add verifier. + * @param webSocketVerifier webSocketVerifier + * @return ShenYuTestCaseSpecBuilder + */ + public ShenYuTestCaseSpecBuilder add(final WebSocketVerifier webSocketVerifier) { + webSocketBuilder.add(webSocketVerifier); + return this; + } /** * add verifier case spec. @@ -133,7 +155,7 @@ public ShenYuTestCaseSpecBuilder addVerifier(final String endpoint, final Matche * @return ShenYuTestCaseSpecBuilder */ public ShenYuTestCaseSpecBuilder addVerifier(final Method method, final String endpoint, final Matcher matcher, final Matcher... matchers) { - return add(supplier -> supplier.when().request(method, endpoint).then().assertThat().body(matcher, matchers)); + return add((Verifier) supplier -> supplier.when().request(method, endpoint).then().assertThat().body(matcher, matchers)); } /** @@ -155,6 +177,17 @@ public ShenYuTestCaseSpecBuilder addExists(final Method method, final String end return add(exists(method, endpoint)); } + /** + * add exist method endpoint case spec. + * @param endpoint endpoint + * @param sendMessage sendMessage + * @param receiveMessage receiveMessage + * @return ShenYuTestCaseSpecBuilder + */ + public ShenYuTestCaseSpecBuilder addExists(final String endpoint, final String sendMessage, final String receiveMessage) { + return add(WebSocketCheckers.exists(endpoint, sendMessage, receiveMessage)); + } + /** * add exist method endpoint case spec. @@ -186,13 +219,23 @@ public ShenYuTestCaseSpecBuilder addNotExists(final String endpoint) { public ShenYuTestCaseSpecBuilder addNotExists(final Method method, final String endpoint) { return add(notExists(method, endpoint)); } - + + /** + * add not exists case spec. + * @param endpoint endpoint + * @param endpoint endpoint + * @return ShenYuTestCaseSpecBuilder + */ + public ShenYuTestCaseSpecBuilder addNotExists(final String endpoint, final String message) { + return add(WebSocketCheckers.notExists(endpoint, message)); + } + /** * build. * @return ShenYuCaseSpec */ public ShenYuCaseSpec build() { - return new ShenYuCaseSpec(name, builder.build()); + return new ShenYuCaseSpec(name, builder.build(), webSocketBuilder.build()); } } }