From f1023e84bea4d972b915685d2c738a287c8933e6 Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Wed, 17 Jan 2024 11:28:51 +0100 Subject: [PATCH] Add Websocket samples - Add client Websockets sample - Add server Websockets sample --- README.md | 139 +++++++------- pom.xml | 1 + samples-websocket/pom.xml | 34 ++++ .../sample-websocket-client/README.md | 120 +++++++++++++ .../sample-websocket-client/pom.xml | 160 +++++++++++++++++ .../samples/websocket/ChatSocket.java | 81 +++++++++ .../src/main/resources/application.properties | 21 +++ .../samples/websocket/ChatSocketTest.java | 69 +++++++ .../sample-websocket-server/README.md | 170 ++++++++++++++++++ .../sample-websocket-server/pom.xml | 168 +++++++++++++++++ .../samples/websocket/ChatResource.java | 44 +++++ .../samples/websocket/ChatService.java | 76 ++++++++ .../src/main/resources/application.properties | 21 +++ .../samples/websocket/ChatSocketTest.java | 107 +++++++++++ 14 files changed, 1143 insertions(+), 68 deletions(-) create mode 100644 samples-websocket/pom.xml create mode 100644 samples-websocket/sample-websocket-client/README.md create mode 100644 samples-websocket/sample-websocket-client/pom.xml create mode 100644 samples-websocket/sample-websocket-client/src/main/java/org/citrusframework/samples/websocket/ChatSocket.java create mode 100644 samples-websocket/sample-websocket-client/src/main/resources/application.properties create mode 100644 samples-websocket/sample-websocket-client/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java create mode 100644 samples-websocket/sample-websocket-server/README.md create mode 100644 samples-websocket/sample-websocket-server/pom.xml create mode 100644 samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatResource.java create mode 100644 samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatService.java create mode 100644 samples-websocket/sample-websocket-server/src/main/resources/application.properties create mode 100644 samples-websocket/sample-websocket-server/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java diff --git a/README.md b/README.md index 74768d13..a3f4c39e 100644 --- a/README.md +++ b/README.md @@ -20,74 +20,77 @@ some Citrus test cases. Each sample folder demonstrates a special aspect of how to use Citrus. Most of the samples use a simple todo-list application as system under test. Please find following list of samples and their primary objective: -| Samples | Description | -|---------------------------------------|:-----------:| -| [sample-reporting](sample-reporting)| Shows how to add a custom reporter | -| [sample-docker](sample-docker)| Shows how to use Citrus within Docker infrastructure | -| [sample-kubernetes](sample-kubernetes)| Shows how to use Citrus within Kubernetes infrastructure | -| [sample-gradle](sample-gradle)| Uses Gradle build to execute tests | -| [sample-annotation-config](sample-annotation-config)| Uses annotation based endpoint configuration | -| [sample-javaconfig](sample-javaconfig)| Uses pure Java POJOs for configuration | -| [sample-groovy](sample-groovy)| Uses Groovy scripts to define Citrus test cases | -| [sample-behaviors](sample-behaviors)| Shows how to reuse test actions in test behaviors | -| [sample-dictionaries](sample-dictionaries)| Shows how to incorporate message manipulation using data dictionaries | -| [sample-message-store](sample-message-store)| Shows how to access internal message store | -| [sample-binary](sample-binary)| Shows binary message content handling in Citrus | -| [sample-hamcrest](sample-hamcrest)| Shows Hamcrest matcher support in validation and conditions | -| [sample-mail](sample-mail)| Shows mail server activities in Citrus | -| [sample-selenium](sample-selenium)| Perform UI testing with Selenium and Citrus | -| [sample-dynamic-endpoints](sample-dynamic-endpoints)| Shows dynamic endpoint component usage | -| [sample-jms](sample-jms)| Shows JMS message broker integration | -| [sample-kafka](sample-kafka)| Shows Kafka integration | -| [sample-rmi](sample-rmi)| Shows how to use RMI with Citrus as a client and server | -| [sample-camel-context](sample-camel-context)| Interact with Apache Camel context and routes | -| Samples DB | Description | -| [sample-jdbc](samples-db/sample-jdbc)| Simulates database server with JDBC | -| [sample-jdbc-callable-statements](samples-db/sample-jdbc-callable-statements)| Simulates database server communication using callable statements | -| [sample-jdbc-transactions](samples-db/sample-jdbc-transactions)| Simulates database server with transactional JDBC | -| [sample-sql](samples-db/sample-sql)| Validates stored data in relational database | -| Samples JSON | Description | -| [sample-json](samples-json/sample-json)| Shows Json payload validation feature with JsonPath validation | -| [sample-databind](samples-json/sample-databind)| Shows JSON object mapping feature when sending and receiving messages | -| Samples XML | Description | -| [sample-xml](samples-xml/sample-xml)| Shows XML validation feature with schema and Xpath validation | -| [sample-oxm](samples-xml/sample-oxm)| Shows XML object marshalling feature when sending and receiving messages | -| [sample-xhtml](samples-xml/sample-xhtml)| Shows XHTML validation feature | -| Samples FTP/SFTP | Description | -| [sample-ftp](samples-ftp/sample-ftp)| Shows FTP client and server interaction in Citrus | -| [sample-sftp](samples-ftp/sample-sftp)| Shows SFTP client and server interaction in Citrus | -| [sample-scp](samples-ftp/sample-scp)| Shows SCP client and server interaction in Citrus | -| Samples TestNG | Description | -| [sample-testng](samples-testng/sample-testng)| Shows TestNG framework support | -| [sample-dataprovider](samples-testng/sample-dataprovider)| Shows TestNG data provider usage in Citrus | -| Samples JUnit | Description | -| [sample-junit](samples-junit/sample-junit)| Shows JUnit4 framework support | -| [sample-junit5](samples-junit/sample-junit5)| Shows JUnit5 framework support | -| Samples Http | Description | -| [sample-swagger](samples-http/sample-swagger)| Auto generate tests from Swagger Open API | -| [sample-http](samples-http/sample-http)| Shows Http REST API calls as a client | -| [sample-http-loadtest](samples-http/sample-http-loadtest)| Calls REST API on Http server with multiple threads for load testing | -| [sample-http-static-response](samples-http/sample-http-static-response)| Shows how to setup a static response generating Http server component | -| [sample-http-query-param](samples-http/sample-http-form-data)| How to use Http form data with `x-www-form-urlencoded` Http POST | -| [sample-http-form-data](samples-http/sample-http-query-param)| Exchange form data via Http GET/POST | -| [sample-http-basic-auth](samples-http/sample-http-basic-auth)| Shows how to use basic authentication on client and server components | -| [sample-https](samples-http/sample-https)| Shows how to use SSL connectivity as a client and server | -| Samples SOAP | Description | -| [sample-wsdl](samples-soap/sample-wsdl)| Auto generate tests from WSDL | -| [sample-soap](samples-soap/sample-soap)| Shows basic SOAP web service support | -| [sample-soap-mtom](samples-soap/sample-soap-mtom)| Shows how to send and receive MTOM enabled SOAP attachments | -| [sample-soap-attachment](samples-soap/sample-soap-attachment)| Shows how to send SOAP attachments to server | -| [sample-soap-wssecurity](samples-soap/sample-soap-wssecurity)| Shows how to configure SOAP web service client and server with WSSecurity enabled | -| [sample-soap-wsaddressing](samples-soap/sample-soap-wsaddressing)| Shows how to configure SOAP web service client and server with WSAddressing enabled | -| [sample-soap-ssl](samples-soap/sample-soap-ssl)| Shows how to configure SOAP web service with SSL secure connectivity | -| [sample-soap-static-response](samples-soap/sample-soap-static-response)| Shows how to setup a static response generating SOAP web service server component | -| Samples Cucumber BDD | Description | -| [sample-cucumber](samples-cucumber/sample-cucumber)| Shows BDD integration with Cucumber | -| [sample-cucumber-spring](samples-cucumber/sample-cucumber-spring)| Shows BDD integration with Cucumber using Spring Framework injection | -| [sample-cucumber-spring2](samples-cucumber/sample-cucumber-spring2)| Shows BDD integration with Cucumber Spring Framework support | -| Samples - Remote | Description | -| [sample-test-jar](samples-remote/sample-test-jar)| Creates an executable test JAR to run all integration tests | -| [sample-test-war](samples-remote/sample-test-war)| Creates a deployable test WAR to run all integration tests as part of a web deployment | +| Samples | Description | +|-------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------:| +| [sample-reporting](sample-reporting) | Shows how to add a custom reporter | +| [sample-docker](sample-docker) | Shows how to use Citrus within Docker infrastructure | +| [sample-kubernetes](sample-kubernetes) | Shows how to use Citrus within Kubernetes infrastructure | +| [sample-gradle](sample-gradle) | Uses Gradle build to execute tests | +| [sample-annotation-config](sample-annotation-config) | Uses annotation based endpoint configuration | +| [sample-javaconfig](sample-javaconfig) | Uses pure Java POJOs for configuration | +| [sample-groovy](sample-groovy) | Uses Groovy scripts to define Citrus test cases | +| [sample-behaviors](sample-behaviors) | Shows how to reuse test actions in test behaviors | +| [sample-dictionaries](sample-dictionaries) | Shows how to incorporate message manipulation using data dictionaries | +| [sample-message-store](sample-message-store) | Shows how to access internal message store | +| [sample-binary](sample-binary) | Shows binary message content handling in Citrus | +| [sample-hamcrest](sample-hamcrest) | Shows Hamcrest matcher support in validation and conditions | +| [sample-mail](sample-mail) | Shows mail server activities in Citrus | +| [sample-selenium](sample-selenium) | Perform UI testing with Selenium and Citrus | +| [sample-dynamic-endpoints](sample-dynamic-endpoints) | Shows dynamic endpoint component usage | +| [sample-jms](sample-jms) | Shows JMS message broker integration | +| [sample-kafka](sample-kafka) | Shows Kafka integration | +| [sample-rmi](sample-rmi) | Shows how to use RMI with Citrus as a client and server | +| [sample-camel-context](sample-camel-context) | Interact with Apache Camel context and routes | +| Samples DB | Description | +| [sample-jdbc](samples-db/sample-jdbc) | Simulates database server with JDBC | +| [sample-jdbc-callable-statements](samples-db/sample-jdbc-callable-statements) | Simulates database server communication using callable statements | +| [sample-jdbc-transactions](samples-db/sample-jdbc-transactions) | Simulates database server with transactional JDBC | +| [sample-sql](samples-db/sample-sql) | Validates stored data in relational database | +| Samples JSON | Description | +| [sample-json](samples-json/sample-json) | Shows Json payload validation feature with JsonPath validation | +| [sample-databind](samples-json/sample-databind) | Shows JSON object mapping feature when sending and receiving messages | +| Samples XML | Description | +| [sample-xml](samples-xml/sample-xml) | Shows XML validation feature with schema and Xpath validation | +| [sample-oxm](samples-xml/sample-oxm) | Shows XML object marshalling feature when sending and receiving messages | +| [sample-xhtml](samples-xml/sample-xhtml) | Shows XHTML validation feature | +| Samples FTP/SFTP | Description | +| [sample-ftp](samples-ftp/sample-ftp) | Shows FTP client and server interaction in Citrus | +| [sample-sftp](samples-ftp/sample-sftp) | Shows SFTP client and server interaction in Citrus | +| [sample-scp](samples-ftp/sample-scp) | Shows SCP client and server interaction in Citrus | +| Samples TestNG | Description | +| [sample-testng](samples-testng/sample-testng) | Shows TestNG framework support | +| [sample-dataprovider](samples-testng/sample-dataprovider) | Shows TestNG data provider usage in Citrus | +| Samples JUnit | Description | +| [sample-junit](samples-junit/sample-junit) | Shows JUnit4 framework support | +| [sample-junit5](samples-junit/sample-junit5) | Shows JUnit5 framework support | +| Samples Http | Description | +| [sample-swagger](samples-http/sample-swagger) | Auto generate tests from Swagger Open API | +| [sample-http](samples-http/sample-http) | Shows Http REST API calls as a client | +| [sample-http-loadtest](samples-http/sample-http-loadtest) | Calls REST API on Http server with multiple threads for load testing | +| [sample-http-static-response](samples-http/sample-http-static-response) | Shows how to setup a static response generating Http server component | +| [sample-http-query-param](samples-http/sample-http-form-data) | How to use Http form data with `x-www-form-urlencoded` Http POST | +| [sample-http-form-data](samples-http/sample-http-query-param) | Exchange form data via Http GET/POST | +| [sample-http-basic-auth](samples-http/sample-http-basic-auth) | Shows how to use basic authentication on client and server components | +| [sample-https](samples-http/sample-https) | Shows how to use SSL connectivity as a client and server | +| Samples Websockets | Description | +| [sample-websocket-client](samples-websocket/sample-websocket-client) | Shows how to connect to a Websocket as a client during the test | +| [sample-websocket-server](samples-websocket/sample-websocket-server) | Shows how to provide a Websocket as a server for clients to connect | +| Samples SOAP | Description | +| [sample-wsdl](samples-soap/sample-wsdl) | Auto generate tests from WSDL | +| [sample-soap](samples-soap/sample-soap) | Shows basic SOAP web service support | +| [sample-soap-mtom](samples-soap/sample-soap-mtom) | Shows how to send and receive MTOM enabled SOAP attachments | +| [sample-soap-attachment](samples-soap/sample-soap-attachment) | Shows how to send SOAP attachments to server | +| [sample-soap-wssecurity](samples-soap/sample-soap-wssecurity) | Shows how to configure SOAP web service client and server with WSSecurity enabled | +| [sample-soap-wsaddressing](samples-soap/sample-soap-wsaddressing) | Shows how to configure SOAP web service client and server with WSAddressing enabled | +| [sample-soap-ssl](samples-soap/sample-soap-ssl) | Shows how to configure SOAP web service with SSL secure connectivity | +| [sample-soap-static-response](samples-soap/sample-soap-static-response) | Shows how to setup a static response generating SOAP web service server component | +| Samples Cucumber BDD | Description | +| [sample-cucumber](samples-cucumber/sample-cucumber) | Shows BDD integration with Cucumber | +| [sample-cucumber-spring](samples-cucumber/sample-cucumber-spring) | Shows BDD integration with Cucumber using Spring Framework injection | +| [sample-cucumber-spring2](samples-cucumber/sample-cucumber-spring2) | Shows BDD integration with Cucumber Spring Framework support | +| Samples - Remote | Description | +| [sample-test-jar](samples-remote/sample-test-jar) | Creates an executable test JAR to run all integration tests | +| [sample-test-war](samples-remote/sample-test-war) | Creates a deployable test WAR to run all integration tests as part of a web deployment | Following sample projects cover message transports and technologies. Each of these samples provides a separate system under test applicaiton that demonstrates the messaging aspect. diff --git a/pom.xml b/pom.xml index 213a4f4a..e202f8fa 100644 --- a/pom.xml +++ b/pom.xml @@ -38,5 +38,6 @@ sample-binary samples-soap samples-db + samples-websocket diff --git a/samples-websocket/pom.xml b/samples-websocket/pom.xml new file mode 100644 index 00000000..a5d4532f --- /dev/null +++ b/samples-websocket/pom.xml @@ -0,0 +1,34 @@ + + + + + 4.0.0 + + org.citrusframework.samples + citrus-samples-websocket + 4.0.0 + Citrus Samples:: Websocket Samples + pom + + + sample-websocket-client + sample-websocket-server + + diff --git a/samples-websocket/sample-websocket-client/README.md b/samples-websocket/sample-websocket-client/README.md new file mode 100644 index 00000000..9847217c --- /dev/null +++ b/samples-websocket/sample-websocket-client/README.md @@ -0,0 +1,120 @@ +Websockets sample ![Logo][1] +============== + +This sample shows how to use the Citrus Websocket client to connect to a socket on the server and send/receive data. +Citrus Websocket features are also described in detail in [reference guide][4] + +Objectives +--------- + +The sample uses a small Quarkus application that provides a server side websocket for clients to connect to. +All messages sent to the socket get pushed to the connected clients. +Citrus is able to connect to the socket as a client in order to send/receive all messages via this socket broadcast. + +In the test Citrus will connect to the socket and send some data to it. +The same message is received in a next step to verify the message broadcast. + +We need a Websocket client component in the configuration: + +```java +@BindToRegistry +public WebSocketClient chatClient() { + return new WebSocketClientBuilder() + .requestUrl("ws://localhost:8081/chat/citrus") + .build(); +} +``` + +In the test cases we can reference this client component in order to send REST calls to the server. + +```java +t.when(send("http://localhost:8081/chat/citrus-user") + .message() + .fork(true) + .body("Hello from Citrus!")); +``` + +**NOTE:** The send action uses `fork=true` option. +This is because the send operation should not block the test to proceed and verify the server side socket communication. + +The Quarkus server socket should accept the connection and process the message sent by the Citrus client. +As a result of this we are able to verify the same message on the client because of the server socket broadcast. +This time the message has been adjusted by the Quarkus server with `>> {username}:` prefix. + +```java +t.then(receive() + .endpoint(chatClient) + .message() + .body(">> citrus: Hello Quarkus chat!")); +``` + +Run +--------- + +The sample application uses QuarkusTest as a framework to run the tests with JUnit Jupiter. +So you can compile, package and test the sample with Maven to run the test. + +```shell +mvn clean verify +``` + +This executes the complete Maven build lifecycle. +The Citrus test cases are part of the unit testing lifecycle and get executed automatically. +The Quarkus application is also started automatically as part of the test. + +During the build you will see Citrus performing some integration tests. + +System under test +--------- + +The sample uses a small Quarkus application that provides the Websocket implementation. +You can read more about Quarkus websocket support in [https://quarkus.io/guides/websockets](https://quarkus.io/guides/websockets). + +Up to now we have started the Quarkus sample application as part of the unit test during the Maven build lifecycle. +This approach is fantastic when running automated tests in a continuous build. + +There may be times we want to test against a standalone application. + +You can start the sample Quarkus application in DevServices mode with this command. + +```shell +mvn quarkus:dev +``` + +Now we are ready to execute some Citrus tests in a separate JVM. + +Citrus test +--------- + +Once the sample application is deployed and running you can execute the Citrus test cases. +Open a separate command line terminal and navigate to the sample folder. + +Execute all Citrus tests by calling + +```shell +mvn verify +``` + +You can also pick a single test by calling + +```shell +mvn clean verify -Dtest= +``` + +You should see Citrus performing several tests with lots of debugging output in both terminals (sample application +and Citrus test client). +And of course green tests at the very end of the build. + +Of course, you can also start the Citrus tests from your favorite IDE. +Just start the Citrus test using the JUnit Jupiter IDE integration in IntelliJ, Eclipse or Netbeans. + +Further information +--------- + +For more information on Citrus see [www.citrusframework.org][2], including +a complete [reference manual][3]. + + [1]: https://citrusframework.org/img/brand-logo.png "Citrus" + [2]: https://citrusframework.org + [3]: https://citrusframework.org/reference/html/ + [4]: https://citrusframework.org/reference/html#websocket diff --git a/samples-websocket/sample-websocket-client/pom.xml b/samples-websocket/sample-websocket-client/pom.xml new file mode 100644 index 00000000..ba0203f6 --- /dev/null +++ b/samples-websocket/sample-websocket-client/pom.xml @@ -0,0 +1,160 @@ + + + + 4.0.0 + + org.citrusframework.samples + citrus-sample-websocket-client + Citrus Samples:: Websocket Client + 4.0.0 + + + UTF-8 + UTF-8 + + 3.11.0 + 3.1.2 + + 3.6.6 + 4.0.2 + + + + + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + + + org.citrusframework + citrus-bom + ${citrus.version} + pom + import + + + + + + + + io.quarkus + quarkus-websockets + + + + + io.quarkus + quarkus-junit5 + test + + + + + org.citrusframework + citrus-quarkus + test + + + org.citrusframework + citrus-websocket + test + + + org.citrusframework + citrus-validation-text + test + + + + + + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.platform.version} + + + 9092 + + + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${compiler-plugin.version} + + ${project.build.sourceEncoding} + + -parameters + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + diff --git a/samples-websocket/sample-websocket-client/src/main/java/org/citrusframework/samples/websocket/ChatSocket.java b/samples-websocket/sample-websocket-client/src/main/java/org/citrusframework/samples/websocket/ChatSocket.java new file mode 100644 index 00000000..7dd9f283 --- /dev/null +++ b/samples-websocket/sample-websocket-client/src/main/java/org/citrusframework/samples/websocket/ChatSocket.java @@ -0,0 +1,81 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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.citrusframework.samples.websocket; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; + +import org.jboss.logging.Logger; + +@ServerEndpoint("/chat/{username}") +@ApplicationScoped +public class ChatSocket { + + private static final Logger LOG = Logger.getLogger(ChatSocket.class); + + Map sessions = new ConcurrentHashMap<>(); + + @OnOpen + public void onOpen(Session session, @PathParam("username") String username) { + sessions.put(username, session); + } + + @OnClose + public void onClose(Session session, @PathParam("username") String username) { + sessions.remove(username); + broadcast("User " + username + " left"); + } + + @OnError + public void onError(Session session, @PathParam("username") String username, Throwable throwable) { + sessions.remove(username); + LOG.error("onError", throwable); + broadcast("User " + username + " left on error: " + throwable); + } + + @OnMessage + public void onMessage(String message, @PathParam("username") String username) { + if (message.equalsIgnoreCase("_ready_")) { + broadcast("User " + username + " joined"); + } else { + broadcast(">> " + username + ": " + message); + } + } + + private void broadcast(String message) { + sessions.values().forEach(s -> { + s.getAsyncRemote().sendObject(message, result -> { + if (result.getException() != null) { + System.out.println("Unable to send message: " + result.getException()); + } + }); + }); + } + +} diff --git a/samples-websocket/sample-websocket-client/src/main/resources/application.properties b/samples-websocket/sample-websocket-client/src/main/resources/application.properties new file mode 100644 index 00000000..c0824d2d --- /dev/null +++ b/samples-websocket/sample-websocket-client/src/main/resources/application.properties @@ -0,0 +1,21 @@ +# +# Copyright 2024 the original author or authors. +# +# 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. +# + +quarkus.log.level=INFO +quarkus.arc.ignored-split-packages=org.citrusframework.* diff --git a/samples-websocket/sample-websocket-client/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java b/samples-websocket/sample-websocket-client/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java new file mode 100644 index 00000000..d0d4f495 --- /dev/null +++ b/samples-websocket/sample-websocket-client/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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.citrusframework.samples.websocket; + +import io.quarkus.test.junit.QuarkusTest; +import org.citrusframework.TestCaseRunner; +import org.citrusframework.annotations.CitrusConfiguration; +import org.citrusframework.annotations.CitrusEndpoint; +import org.citrusframework.annotations.CitrusResource; +import org.citrusframework.quarkus.CitrusSupport; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.websocket.client.WebSocketClient; +import org.citrusframework.websocket.client.WebSocketClientBuilder; +import org.junit.jupiter.api.Test; + +import static org.citrusframework.actions.ReceiveMessageAction.Builder.receive; +import static org.citrusframework.actions.SendMessageAction.Builder.send; + +@QuarkusTest +@CitrusSupport +@CitrusConfiguration( classes = { ChatSocketTest.EndpointConfig.class } ) +class ChatSocketTest { + + @CitrusResource + TestCaseRunner t; + + @CitrusEndpoint + WebSocketClient chatClient; + + @Test + void shouldConnectAndSendMessage() { + t.given(send() + .endpoint(chatClient) + .message() + .body("Hello Quarkus chat!")); + + t.then(receive() + .endpoint(chatClient) + .message() + .body(">> citrus: Hello Quarkus chat!")); + } + + public static class EndpointConfig { + + @BindToRegistry + public WebSocketClient chatClient() { + return new WebSocketClientBuilder() + .requestUrl("ws://localhost:8081/chat/citrus") + .build(); + } + } +} diff --git a/samples-websocket/sample-websocket-server/README.md b/samples-websocket/sample-websocket-server/README.md new file mode 100644 index 00000000..8212f717 --- /dev/null +++ b/samples-websocket/sample-websocket-server/README.md @@ -0,0 +1,170 @@ +Websockets sample ![Logo][1] +============== + +This sample shows how to use the Citrus Websocket server to provide a socket on the server so that clients can connect to and send/receive data. +Citrus Websocket features are also described in detail in [reference guide][4] + +Objectives +--------- + +The sample uses a small Quarkus application that provides a websocket client to connect to the Citrus Websocket server. +All messages sent to the socket get pushed to the connected clients. +Citrus is able to start the socket as a server in order to accept client sessions and broadcast messages to all connected clients. + +In the test Citrus will verify client connections and broadcast some data to the clients. + +We need a Websocket server component in the configuration: + +```java +public static class EndpointConfig { + + private WebSocketEndpoint chatEndpoint; + + @BindToRegistry + public WebSocketEndpoint chatEndpoint() { + if (chatEndpoint == null) { + WebSocketServerEndpointConfiguration chatEndpointConfig = new WebSocketServerEndpointConfiguration(); + chatEndpointConfig.setEndpointUri("/chat"); + chatEndpoint = new WebSocketEndpoint(chatEndpointConfig); + } + + return chatEndpoint; + } + + @BindToRegistry + public WebSocketServer chatServer() { + return new WebSocketServerBuilder() + .webSockets(Collections.singletonList(chatEndpoint())) + .port(8088) + .autoStart(true) + .build(); + } +} +``` + +The server listens on port `8088` and provides a websocket endpoint on `/chat`. +So clients may connect to the socket opening sessions on `http://localhost:8088/chat`. + +In the test cases we can receive client sessions with a normal receive action that references the websocket endpoint. + +```java +t.then(receive() + .endpoint(chatEndpoint) + .message() + .body("Quarkus wants to join ...")); +``` + +**NOTE:** The message `Quarkus wants to join ...` is automatically sent by the sample Quarkus application when the session is opened. +We can respond with a server side message that is sent to all connected clients. + +```java +t.then(send() + .endpoint(chatEndpoint) + .message() + .body("Welcome Quarkus!")); +``` + +You will see this response printed to the log output of the Quarkus sample application. + +The test is able to trigger some client messages on the Quarkus application by sending a Http POST request to the REST API of the Quarkus application. + +```java +t.when(http().client("http://localhost:8081") + .send() + .post("chat/citrus-user") + .fork(true) + .message() + .body("Hello from Citrus!")); +``` + +**NOTE:** The test uses a dynamic Http endpoint URL to send the POST request. +The username is given as a path parameter and the message body represents the message that is sent to the websocket. + +Now the test is able to verify the websocket message on the server. +This time the message has been adjusted by the Quarkus client with `>> {username}:` prefix. + +```java +t.then(receive() + .endpoint(chatEndpoint) + .message() + .body(">> citrus-user: Hello from Citrus!")); +``` + +At the very end of the test we can verify the response of the Http POST request. + +```java +t.then(http().client("http://localhost:8081") + .receive() + .response(HttpStatus.CREATED)); +``` + +Run +--------- + +The sample application uses QuarkusTest as a framework to run the tests with JUnit Jupiter. +So you can compile, package and test the sample with Maven to run the test. + +```shell +mvn clean verify +``` + +This executes the complete Maven build lifecycle. +The Citrus test cases are part of the unit testing lifecycle and get executed automatically. +The Quarkus application is also started automatically as part of the test. + +During the build you will see Citrus performing some integration tests. + +System under test +--------- + +The sample uses a small Quarkus application that provides the Websocket implementation. +You can read more about Quarkus websocket support in [https://quarkus.io/guides/websockets](https://quarkus.io/guides/websockets). + +Up to now we have started the Quarkus sample application as part of the unit test during the Maven build lifecycle. +This approach is fantastic when running automated tests in a continuous build. + +There may be times we want to test against a standalone application. + +You can start the sample Quarkus application in DevServices mode with this command. + +```shell +mvn quarkus:dev +``` + +Now we are ready to execute some Citrus tests in a separate JVM. + +Citrus test +--------- + +Once the sample application is deployed and running you can execute the Citrus test cases. +Open a separate command line terminal and navigate to the sample folder. + +Execute all Citrus tests by calling + +```shell +mvn verify +``` + +You can also pick a single test by calling + +```shell +mvn clean verify -Dtest= +``` + +You should see Citrus performing several tests with lots of debugging output in both terminals (sample application +and Citrus test client). +And of course green tests at the very end of the build. + +Of course, you can also start the Citrus tests from your favorite IDE. +Just start the Citrus test using the JUnit Jupiter IDE integration in IntelliJ, Eclipse or Netbeans. + +Further information +--------- + +For more information on Citrus see [www.citrusframework.org][2], including +a complete [reference manual][3]. + + [1]: https://citrusframework.org/img/brand-logo.png "Citrus" + [2]: https://citrusframework.org + [3]: https://citrusframework.org/reference/html/ + [4]: https://citrusframework.org/reference/html#websocket diff --git a/samples-websocket/sample-websocket-server/pom.xml b/samples-websocket/sample-websocket-server/pom.xml new file mode 100644 index 00000000..2bdbf9fd --- /dev/null +++ b/samples-websocket/sample-websocket-server/pom.xml @@ -0,0 +1,168 @@ + + + + 4.0.0 + + org.citrusframework.samples + citrus-sample-websocket-server + Citrus Samples:: Websocket Server + 4.0.0 + + + UTF-8 + UTF-8 + + 3.11.0 + 3.1.2 + + 3.6.6 + 4.0.2 + + + + + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + + + org.citrusframework + citrus-bom + ${citrus.version} + pom + import + + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-websockets + + + + + io.quarkus + quarkus-junit5 + test + + + + + org.citrusframework + citrus-quarkus + test + + + org.citrusframework + citrus-websocket + test + + + org.citrusframework + citrus-validation-text + test + + + + + + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.platform.version} + + + 9092 + + + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${compiler-plugin.version} + + ${project.build.sourceEncoding} + + -parameters + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + diff --git a/samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatResource.java b/samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatResource.java new file mode 100644 index 00000000..8bbd1c70 --- /dev/null +++ b/samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatResource.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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.citrusframework.samples.websocket; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("chat/{username}") +public class ChatResource { + + @Inject + ChatService chatService; + + @POST + @Produces("text/plain") + @Consumes(MediaType.TEXT_PLAIN) + public Response add(@PathParam("username") String user, String message) { + chatService.send(user, message); + return Response.status(Response.Status.CREATED).build(); + } +} diff --git a/samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatService.java b/samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatService.java new file mode 100644 index 00000000..f4cbfc66 --- /dev/null +++ b/samples-websocket/sample-websocket-server/src/main/java/org/citrusframework/samples/websocket/ChatService.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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.citrusframework.samples.websocket; + +import java.io.IOException; +import java.net.URI; + +import jakarta.inject.Singleton; +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +@Singleton +public class ChatService { + + private static final Logger LOG = Logger.getLogger(ChatService.class); + + @ConfigProperty(name = "chat.websocket.uri", defaultValue = "http://localhost:8088/chat") + URI uri; + + private Session session; + + public void send(String user, String message) { + openSession().getAsyncRemote().sendText(">> %s: %s".formatted(user, message)); + } + + private Session openSession() { + if (session == null) { + try { + session = ContainerProvider.getWebSocketContainer().connectToServer(ChatClient.class, uri); + } catch (DeploymentException | IOException e) { + throw new RuntimeException(e); + } + } + + return session; + } + + @ClientEndpoint + public static class ChatClient { + + @OnOpen + public void open(Session session) { + LOG.info("CONNECTED!"); + session.getAsyncRemote().sendText("Quarkus wants to join ..."); + } + + @OnMessage + void message(String msg) { + LOG.info(msg); + } + + } +} diff --git a/samples-websocket/sample-websocket-server/src/main/resources/application.properties b/samples-websocket/sample-websocket-server/src/main/resources/application.properties new file mode 100644 index 00000000..c0824d2d --- /dev/null +++ b/samples-websocket/sample-websocket-server/src/main/resources/application.properties @@ -0,0 +1,21 @@ +# +# Copyright 2024 the original author or authors. +# +# 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. +# + +quarkus.log.level=INFO +quarkus.arc.ignored-split-packages=org.citrusframework.* diff --git a/samples-websocket/sample-websocket-server/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java b/samples-websocket/sample-websocket-server/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java new file mode 100644 index 00000000..0f6e397f --- /dev/null +++ b/samples-websocket/sample-websocket-server/src/test/java/org/citrusframework/samples/websocket/ChatSocketTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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.citrusframework.samples.websocket; + +import java.util.Collections; + +import io.quarkus.test.junit.QuarkusTest; +import org.citrusframework.TestCaseRunner; +import org.citrusframework.annotations.CitrusConfiguration; +import org.citrusframework.annotations.CitrusEndpoint; +import org.citrusframework.annotations.CitrusResource; +import org.citrusframework.quarkus.CitrusSupport; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.websocket.endpoint.WebSocketEndpoint; +import org.citrusframework.websocket.server.WebSocketServer; +import org.citrusframework.websocket.server.WebSocketServerBuilder; +import org.citrusframework.websocket.server.WebSocketServerEndpointConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import static org.citrusframework.actions.ReceiveMessageAction.Builder.receive; +import static org.citrusframework.actions.SendMessageAction.Builder.send; +import static org.citrusframework.http.actions.HttpActionBuilder.http; + +@QuarkusTest +@CitrusSupport +@CitrusConfiguration(classes = { ChatSocketTest.EndpointConfig.class }) +class ChatSocketTest { + + @CitrusResource + TestCaseRunner t; + + @CitrusEndpoint + WebSocketEndpoint chatEndpoint; + + @Test + void shouldBroadcastMessages() { + t.when(http() + .client("http://localhost:8081") + .send() + .post("chat/citrus-user") + .fork(true) + .message() + .body("Hello from Citrus!")); + + t.then(receive() + .endpoint(chatEndpoint) + .message() + .body("Quarkus wants to join ...")); + + t.then(send() + .endpoint(chatEndpoint) + .message() + .body("Welcome Quarkus!")); + + t.then(receive() + .endpoint(chatEndpoint) + .message() + .body(">> citrus-user: Hello from Citrus!")); + + t.then(http().client("http://localhost:8081") + .receive() + .response(HttpStatus.CREATED)); + } + + public static class EndpointConfig { + + private WebSocketEndpoint chatEndpoint; + + @BindToRegistry + public WebSocketEndpoint chatEndpoint() { + if (chatEndpoint == null) { + WebSocketServerEndpointConfiguration chatEndpointConfig = new WebSocketServerEndpointConfiguration(); + chatEndpointConfig.setEndpointUri("/chat"); + chatEndpoint = new WebSocketEndpoint(chatEndpointConfig); + } + + return chatEndpoint; + } + + @BindToRegistry + public WebSocketServer chatServer() { + return new WebSocketServerBuilder() + .webSockets(Collections.singletonList(chatEndpoint())) + .port(8088) + .autoStart(true) + .build(); + } + } +}