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());
}
}
}