diff --git a/.gitignore b/.gitignore
index ba58fd4ff..7d3e23014 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,6 @@ coverage
/.idea
temp
-__pycache__
\ No newline at end of file
+__pycache__
+
+microcks-data
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index bb105cbbf..f6927edb8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10313,6 +10313,10 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/javascript": {
+ "resolved": "packages/templates/clients/websocket/test/javascript",
+ "link": true
+ },
"node_modules/jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
@@ -18159,6 +18163,19 @@
"jest-esm-transformer": "^1.0.0",
"rimraf": "^3.0.2"
}
+ },
+ "packages/templates/clients/websocket/test/javascript": {
+ "devDependencies": {
+ "@babel/cli": "^7.25.9",
+ "@babel/core": "^7.26.0",
+ "@babel/preset-env": "^7.26.0",
+ "@babel/preset-react": "^7.25.9",
+ "eslint": "^6.8.0",
+ "eslint-plugin-jest": "^23.8.2",
+ "eslint-plugin-react": "^7.34.1",
+ "eslint-plugin-sonarjs": "^0.5.0",
+ "jest-esm-transformer": "^1.0.0"
+ }
}
}
}
diff --git a/packages/templates/clients/websocket/javascript/components/SendEchoMessage.js b/packages/templates/clients/websocket/javascript/components/SendEchoMessage.js
index 68aff90c9..64cee369f 100644
--- a/packages/templates/clients/websocket/javascript/components/SendEchoMessage.js
+++ b/packages/templates/clients/websocket/javascript/components/SendEchoMessage.js
@@ -4,10 +4,17 @@ export function SendEchoMessage() {
return (
{
- `// Method to send an echo message to the server
-sendEchoMessage(message) {
- this.websocket.send(JSON.stringify(message));
- console.log('Sent message to echo server:', message);
+ `/**
+ * By default sends a message over a provided WebSocket connection.
+ * Useful when you already have an established WebSocket connection
+ * and want to send a message without creating a class instance.
+ *
+ * @param {Object} message - The message to send. It will be stringified to JSON.
+ * @param {WebSocket} socket - An existing WebSocket connection to use for sending the message.
+*/
+static sendEchoMessage(message, socket) {
+ const websocket = socket || this.websocket;
+ websocket.send(JSON.stringify(message));
}`
}
diff --git a/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-hoppscotch-echo.yml b/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-hoppscotch-echo.yml
index 83cf22220..74a3a11de 100644
--- a/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-hoppscotch-echo.yml
+++ b/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-hoppscotch-echo.yml
@@ -44,6 +44,9 @@ components:
summary: A message sent from server with current timestamp.
payload:
$ref: '#/components/schemas/currentTime'
+ examples:
+ - name: someRandomDate
+ payload: 11:13:24 GMT+0000 (Coordinated Universal Time)
echo:
summary: A message exchanged with the echo server.
payload: {}
diff --git a/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-postman-echo.yml b/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-postman-echo.yml
index 2ff9b7020..a9b9d0bc7 100644
--- a/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-postman-echo.yml
+++ b/packages/templates/clients/websocket/javascript/test/__fixtures__/asyncapi-postman-echo.yml
@@ -26,7 +26,8 @@ operations:
action: send
channel:
$ref: '#/channels/echo'
-
+ messages:
+ - $ref: '#/channels/echo/messages/echo'
reply:
channel:
$ref: '#/channels/echo'
diff --git a/packages/templates/clients/websocket/javascript/test/__snapshots__/integration.test.js.snap b/packages/templates/clients/websocket/javascript/test/__snapshots__/integration.test.js.snap
index 5da478e49..70e3caa5b 100644
--- a/packages/templates/clients/websocket/javascript/test/__snapshots__/integration.test.js.snap
+++ b/packages/templates/clients/websocket/javascript/test/__snapshots__/integration.test.js.snap
@@ -92,10 +92,17 @@ class HoppscotchEchoWebSocketClient {
if (cb) cb(message);
}
- // Method to send an echo message to the server
- sendEchoMessage(message) {
- this.websocket.send(JSON.stringify(message));
- console.log('Sent message to echo server:', message);
+ /**
+ * By default sends a message over a provided WebSocket connection.
+ * Useful when you already have an established WebSocket connection
+ * and want to send a message without creating a class instance.
+ *
+ * @param {Object} message - The message to send. It will be stringified to JSON.
+ * @param {WebSocket} socket - An existing WebSocket connection to use for sending the message.
+ */
+ static sendEchoMessage(message, socket) {
+ const websocket = socket || this.websocket;
+ websocket.send(JSON.stringify(message));
}
// Method to close the WebSocket connection
@@ -204,10 +211,17 @@ class PostmanEchoWebSocketClientClient {
if (cb) cb(message);
}
- // Method to send an echo message to the server
- sendEchoMessage(message) {
- this.websocket.send(JSON.stringify(message));
- console.log('Sent message to echo server:', message);
+ /**
+ * By default sends a message over a provided WebSocket connection.
+ * Useful when you already have an established WebSocket connection
+ * and want to send a message without creating a class instance.
+ *
+ * @param {Object} message - The message to send. It will be stringified to JSON.
+ * @param {WebSocket} socket - An existing WebSocket connection to use for sending the message.
+ */
+ static sendEchoMessage(message, socket) {
+ const websocket = socket || this.websocket;
+ websocket.send(JSON.stringify(message));
}
// Method to close the WebSocket connection
diff --git a/packages/templates/clients/websocket/python/components/SendEchoMessage.js b/packages/templates/clients/websocket/python/components/SendEchoMessage.js
index 13dee6c08..b6d639ea8 100644
--- a/packages/templates/clients/websocket/python/components/SendEchoMessage.js
+++ b/packages/templates/clients/websocket/python/components/SendEchoMessage.js
@@ -4,23 +4,50 @@ export function SendEchoMessage() {
return (
{
- `def send_message(self, message):
+ `async def send_message(self, message):
"""
- Automatically process the outgoing message with registered processors and send it
- using the active WebSocket connection.
+ Send a message using the WebSocket connection attached to this instance.
+
+ Args:
+ message (dict or str): The message to send. Will be serialized to JSON if it's a dictionary.
+
+ Raises:
+ Exception: If sending fails or the socket is not connected.
+ """
+ await self._send(message, self.ws_app)
+
+@staticmethod
+async def send_message_static(message, socket):
+ """
+ Send a message using a provided WebSocket connection, without needing an instance.
+
+ Args:
+ message (dict or str): The message to send.
+ socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
+
+ Raises:
+ Exception: If sending fails or the socket is not connected.
+ """
+ await HoppscotchEchoWebSocketClient._send(message, socket)
+
+@staticmethod
+async def _send(message, socket):
+ """
+ Internal helper to handle the actual sending logic.
+
+ Args:
+ message (dict or str): The message to send.
+ socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
+
+ Notes:
+ If message is a dictionary, it will be automatically converted to JSON.
"""
- # Apply outgoing processors sequentially.
- for processor in self.outgoing_processors:
- message = processor(message)
-
- if self.ws_app and self.ws_app.sock and self.ws_app.sock.connected:
- try:
- self.ws_app.send(json.dumps(message))
- print("\\033[92mSent:\\033[0m", message)
- except Exception as error:
- self.handle_error(error)
- else:
- print("Error: WebSocket is not connected.")`
+ try:
+ if isinstance(message, dict):
+ message = json.dumps(message)
+ await socket.send(message)
+ except Exception as e:
+ print("Error sending:", e)`
}
);
diff --git a/packages/templates/clients/websocket/python/test/__snapshots__/integration.test.js.snap b/packages/templates/clients/websocket/python/test/__snapshots__/integration.test.js.snap
index e4a27149b..6340510b8 100644
--- a/packages/templates/clients/websocket/python/test/__snapshots__/integration.test.js.snap
+++ b/packages/templates/clients/websocket/python/test/__snapshots__/integration.test.js.snap
@@ -107,23 +107,50 @@ class HoppscotchEchoWebSocketClient:
for handler in self.error_handlers:
handler(error)
- def send_message(self, message):
+ async def send_message(self, message):
\\"\\"\\"
- Automatically process the outgoing message with registered processors and send it
- using the active WebSocket connection.
+ Send a message using the WebSocket connection attached to this instance.
+
+ Args:
+ message (dict or str): The message to send. Will be serialized to JSON if it's a dictionary.
+
+ Raises:
+ Exception: If sending fails or the socket is not connected.
\\"\\"\\"
- # Apply outgoing processors sequentially.
- for processor in self.outgoing_processors:
- message = processor(message)
-
- if self.ws_app and self.ws_app.sock and self.ws_app.sock.connected:
- try:
- self.ws_app.send(json.dumps(message))
- print(\\"\\\\033[92mSent:\\\\033[0m\\", message)
- except Exception as error:
- self.handle_error(error)
- else:
- print(\\"Error: WebSocket is not connected.\\")
+ await self._send(message, self.ws_app)
+
+ @staticmethod
+ async def send_message_static(message, socket):
+ \\"\\"\\"
+ Send a message using a provided WebSocket connection, without needing an instance.
+
+ Args:
+ message (dict or str): The message to send.
+ socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
+
+ Raises:
+ Exception: If sending fails or the socket is not connected.
+ \\"\\"\\"
+ await HoppscotchEchoWebSocketClient._send(message, socket)
+
+ @staticmethod
+ async def _send(message, socket):
+ \\"\\"\\"
+ Internal helper to handle the actual sending logic.
+
+ Args:
+ message (dict or str): The message to send.
+ socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
+
+ Notes:
+ If message is a dictionary, it will be automatically converted to JSON.
+ \\"\\"\\"
+ try:
+ if isinstance(message, dict):
+ message = json.dumps(message)
+ await socket.send(message)
+ except Exception as e:
+ print(\\"Error sending:\\", e)
def close(self):
\\"\\"\\"Cleanly close the WebSocket connection.\\"\\"\\"
@@ -250,23 +277,50 @@ class PostmanEchoWebSocketClientClient:
for handler in self.error_handlers:
handler(error)
- def send_message(self, message):
+ async def send_message(self, message):
\\"\\"\\"
- Automatically process the outgoing message with registered processors and send it
- using the active WebSocket connection.
+ Send a message using the WebSocket connection attached to this instance.
+
+ Args:
+ message (dict or str): The message to send. Will be serialized to JSON if it's a dictionary.
+
+ Raises:
+ Exception: If sending fails or the socket is not connected.
\\"\\"\\"
- # Apply outgoing processors sequentially.
- for processor in self.outgoing_processors:
- message = processor(message)
-
- if self.ws_app and self.ws_app.sock and self.ws_app.sock.connected:
- try:
- self.ws_app.send(json.dumps(message))
- print(\\"\\\\033[92mSent:\\\\033[0m\\", message)
- except Exception as error:
- self.handle_error(error)
- else:
- print(\\"Error: WebSocket is not connected.\\")
+ await self._send(message, self.ws_app)
+
+ @staticmethod
+ async def send_message_static(message, socket):
+ \\"\\"\\"
+ Send a message using a provided WebSocket connection, without needing an instance.
+
+ Args:
+ message (dict or str): The message to send.
+ socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
+
+ Raises:
+ Exception: If sending fails or the socket is not connected.
+ \\"\\"\\"
+ await HoppscotchEchoWebSocketClient._send(message, socket)
+
+ @staticmethod
+ async def _send(message, socket):
+ \\"\\"\\"
+ Internal helper to handle the actual sending logic.
+
+ Args:
+ message (dict or str): The message to send.
+ socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
+
+ Notes:
+ If message is a dictionary, it will be automatically converted to JSON.
+ \\"\\"\\"
+ try:
+ if isinstance(message, dict):
+ message = json.dumps(message)
+ await socket.send(message)
+ except Exception as e:
+ print(\\"Error sending:\\", e)
def close(self):
\\"\\"\\"Cleanly close the WebSocket connection.\\"\\"\\"
diff --git a/packages/templates/clients/websocket/test/README.md b/packages/templates/clients/websocket/test/README.md
new file mode 100644
index 000000000..152d50170
--- /dev/null
+++ b/packages/templates/clients/websocket/test/README.md
@@ -0,0 +1,91 @@
+## Prerequisites
+
+Install [Podman](https://podman.io/docs/installation).
+
+> Microcks needs some services running to simulate the infrastructure and this requires multiple resources. Docker is not the best solution for this, thus it's better to use Podman that manages resources better.
+
+You can also install [docker-compose](https://docs.docker.com/compose/install/) that `podman compose` will later use.
+
+## Test Project
+
+This `test` directory contains acceptance tests that check different clients with tests written in their respective languages. So JavaScript client is tested with JavaScript test, and Python with Python tests, and so on.
+
+To run tests: `podman compose -f ./microcks-setup/microcks-podman.yml --profile tests up -d`
+
+> You need to remember about `--profile tests` to run whole setup with tests. This way you ensure that proper importer container imports `__fixtures__/asyncapi-hoppscotch-server.yml` into Microcks and tests run against it.
+
+## Testing Clients with Microcks
+
+This instruction is just a set of notes about how to play locally with Microcks that we already use for automated testing.
+
+Tests also run in containers, although with provided configuration you can start also testing environment without running tests, just to play with Microcks and understand more how it works.
+
+### Concept
+
+Microcks is a tool for mocking. To test our generated clients, we need to mock the server. In other words, if we want to check if the generated client for Postman works well, we need to mock the server that the client will communicate with during testing.
+
+### Using Microcks locally
+
+> This instruction assumes you are located in directory where `microcks-setup` directory is located
+
+#### Start Microcks
+
+1. Start Microcks infrastructure: `podman compose -f ./microcks-setup/microcks-podman.yml up -d`.
+
+1. Check with `podman ps` command if all services are running. It may take few minutes to start all containers. You can also run a special script that will confirm that services are ready: `bash ./microcks-setup/checkMicrocksReady.sh`.
+
+1. Access Microcks UI with `open http://localhost:8080`.
+
+#### Load AsyncAPI documents
+
+To test clients, we need to mock the server. Remember to load AsyncAPI documents that represent the server.
+
+1. Install [Microcks CLI](https://microcks.io/documentation/guides/automation/cli/)
+
+1. Import AsyncAPI document
+ ```bash
+ microcks-cli import __fixtures__/asyncapi-hoppscotch-server.yml \
+ --microcksURL=http://localhost:8080/api/ \
+ --keycloakClientId=microcks-serviceaccount \
+ --keycloakClientSecret="ab54d329-e435-41ae-a900-ec6b3fe15c54"
+ ```
+
+1. See the mock in the Microcks UI with `open http://localhost:8080/#/services`
+
+#### Invoke Mocks tests
+
+Start simple testing of the mock to see if it was created properly. Tests should turn green.
+
+You should run tests only on one operation at a time.
+
+```bash
+# the higher timeout the more test samples will run
+microcks-cli test 'Hoppscotch WebSocket Server:1.0.0' ws://localhost:8081/api/ws/Hoppscotch+WebSocket+Server/1.0.0/sendTimeStampMessage ASYNC_API_SCHEMA \
+ --microcksURL=http://localhost:8080/api/ \
+ --insecure \
+ --waitFor=15sec \
+ --keycloakClientId=microcks-serviceaccount \
+ --keycloakClientSecret="ab54d329-e435-41ae-a900-ec6b3fe15c54" \
+ --filteredOperations="[\"SEND sendTimeStampMessage\"]"
+```
+
+You can also check the status of tests in the Microcks UI.
+
+### Debugging
+
+File `microcks-podman.yml` contains commented out configuration for debbuging container you can start with the entire testing environment.
+
+It's also useful to connect to Microcks containers and checkout logs: `podman logs -f microcks-async-minion`. Minion is the service that handles API calls.
+
+### Cleanup
+
+Run `podman compose -f ./microcks-setup/microcks-podman.yml down --remove-orphans` to clean everything.
+
+## Playing with Microcks Mock Server
+
+You can also check the websocket endpoints generated by Microcks in the terminal using [websocat](https://github.com/vi/websocat).
+
+Do below to start receiving example messages in the terminal:
+```
+websocat ws://localhost:8081/api/ws/Hoppscotch+WebSocket+Server/1.0.0/sendTimeStampMessage
+```
\ No newline at end of file
diff --git a/packages/templates/clients/websocket/test/__fixtures__/asyncapi-hoppscotch-server.yml b/packages/templates/clients/websocket/test/__fixtures__/asyncapi-hoppscotch-server.yml
new file mode 100644
index 000000000..068ab3453
--- /dev/null
+++ b/packages/templates/clients/websocket/test/__fixtures__/asyncapi-hoppscotch-server.yml
@@ -0,0 +1,71 @@
+asyncapi: 3.0.0
+defaultContentType: text/plain
+info:
+ title: Hoppscotch WebSocket Server
+ version: 1.0.0
+ description: >
+ This document is purely for mocking the official Hoppscotch Websocket Echo server.
+
+servers:
+ echoServer:
+ host: echo-websocket.hoppscotch.io
+ protocol: wss
+
+channels:
+ echo:
+ description: >
+ The single channel where messages are sent to and echoed back. Echo server also regularly drops a timestampl to that channel.
+ address: /
+ messages:
+ echo:
+ $ref: '#/components/messages/echo'
+ timestamp:
+ $ref: '#/components/messages/timestamp'
+ bindings:
+ ws:
+ method: POST
+
+operations:
+ sendTimeStampMessage:
+ action: send
+ channel:
+ $ref: '#/channels/echo'
+ summary: Receive the timestamp message sent from server every second.
+ messages:
+ - $ref: '#/channels/echo/messages/timestamp'
+ handleEchoMessage:
+ action: receive
+ channel:
+ $ref: '#/channels/echo'
+ summary: Send a message to the echo server.
+ messages:
+ - $ref: '#/channels/echo/messages/echo'
+
+components:
+ messages:
+ timestamp:
+ summary: A message sent from server with current timestamp.
+ payload:
+ $ref: '#/components/schemas/currentTime'
+ examples:
+ - name: someRandomDate
+ payload: 11:13:24 GMT+0000 (Coordinated Universal Time)
+ echo:
+ summary: A message exchanged with the echo server.
+ payload: {}
+ examples:
+ - name: string
+ payload: "test"
+ - name: boolean
+ payload: true
+ - name: number
+ payload: 123
+ - name: object
+ payload:
+ test: test text
+
+ schemas:
+ currentTime:
+ type: string
+ description: A timestamp with GMT timezone.
+ example: 11:13:24 GMT+0000 (Coordinated Universal Time)
\ No newline at end of file
diff --git a/packages/templates/clients/websocket/test/javascript/acceptance.test.js b/packages/templates/clients/websocket/test/javascript/acceptance.test.js
new file mode 100644
index 000000000..1553431a3
--- /dev/null
+++ b/packages/templates/clients/websocket/test/javascript/acceptance.test.js
@@ -0,0 +1,105 @@
+/**
+ * @jest-environment node
+ */
+
+const WSClient = require('../../javascript/test/temp/snapshotTestResult/client-hoppscotch');
+const { waitForMessage, delay, waitForTestSuccess } = require('./utils');
+const fetch = require('node-fetch');
+const WebSocket = require('ws');
+const microcksTestEndpoint = 'http://microcks:8080/api/tests';
+
+describe('client - receiver tests', () => {
+ jest.setTimeout(10000);
+ /*
+ * TEST: we test if the generated client can be instantiated with custom URL
+ */
+ const wsClient = new WSClient(
+ 'ws://microcks-async-minion:8081/api/ws/Hoppscotch+WebSocket+Server/1.0.0/sendTimeStampMessage'
+ );
+
+ it('javascript client should receive a message', async () => {
+ const received_messages = [];
+ const expectedMessage = '11:13:24 GMT+0000 (Coordinated Universal Time)';
+
+ /*
+ * TEST: we test if generated message handler can be registered and later records messages
+ */
+ // registering message handler that adds incomming messages to an array
+ // later messages from array are evaluated to make sure the message sent from server is received by the client
+ wsClient.registerMessageHandler((message) => {
+ received_messages.push(message);
+ });
+
+ /*
+ * TEST: we test if we can connect to server using genrated client
+ */
+ await wsClient.connect();
+
+ // wait for the incomming message to be logged
+ await waitForMessage(received_messages, expectedMessage);
+
+ const isReceived = received_messages.some((message) =>
+ message.includes(expectedMessage)
+ );
+
+ // checking if microcks mock send proper message and they were recorded by registered handler for incomming messages
+ expect(isReceived).toEqual(true);
+ });
+
+ afterAll(async () => {
+ wsClient.close();
+
+ //jest doesn't like that on client closure some logs are printed
+ // so we need to wait second or 2 until all logs are printed
+ await delay();
+ });
+});
+
+describe('client - sender tests', () => {
+ jest.setTimeout(100000);
+ const port = 8082;
+ const payload = JSON.stringify({
+ serviceId: 'Hoppscotch WebSocket Server:1.0.0',
+ testEndpoint: 'ws://websocket-acceptance-tester-js:8082/ws',
+ runnerType: 'ASYNC_API_SCHEMA',
+ timeout: 30000,
+ filteredOperations: ['RECEIVE handleEchoMessage'],
+ });
+
+ //this is a custom server that needs to be used to expose endpoint to which microcks will connect and await for messages comming from generated function
+ const wsServer = new WebSocket.Server({ port, path: '/ws' }, () => {
+ console.log(`Test WS server running on port ${port}`);
+ });
+
+ it('javascript client should send a valid message', async () => {
+ //await delay(100000);
+ const expectedMessage = 'Sending acceptance test message';
+
+ wsServer.on('connection', (socket) => {
+ /*
+ * TEST: sending message with generated function to microcks test client that connected to our custom server
+ */
+ WSClient.sendEchoMessage(expectedMessage, socket);
+ });
+
+ // Start test in Microcks
+ const response = await fetch(microcksTestEndpoint, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: payload,
+ });
+
+ //checking test result
+ const responseBody = await response.json();
+ const status = await waitForTestSuccess(`${microcksTestEndpoint}/${responseBody.id}`);
+
+ expect(status).toEqual(true);
+ });
+
+ afterAll(async () => {
+ wsServer.close();
+ //jest doesn't like that on client closure some logs are printed
+ // so we need to wait second or 2 until all logs are printed
+ await delay();
+ });
+});
diff --git a/packages/templates/clients/websocket/test/javascript/package.json b/packages/templates/clients/websocket/test/javascript/package.json
new file mode 100644
index 000000000..37e85303b
--- /dev/null
+++ b/packages/templates/clients/websocket/test/javascript/package.json
@@ -0,0 +1,32 @@
+{
+ "scripts": {
+ "test": "jest --coverage",
+ "lint": "eslint --max-warnings 0 --config ../../../../../../.eslintrc --ignore-path ../../../../../../.eslintignore .",
+ "lint:fix": "eslint --fix --max-warnings 0 --config ../../../../../../.eslintrc --ignore-path ../../../../../../.eslintignore ."
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.25.9",
+ "@babel/core": "^7.26.0",
+ "@babel/preset-env": "^7.26.0",
+ "@babel/preset-react": "^7.25.9",
+ "eslint": "^6.8.0",
+ "eslint-plugin-jest": "^23.8.2",
+ "eslint-plugin-react": "^7.34.1",
+ "eslint-plugin-sonarjs": "^0.5.0",
+ "jest-esm-transformer": "^1.0.0"
+ },
+ "jest": {
+ "moduleFileExtensions": [
+ "js",
+ "json",
+ "jsx"
+ ],
+ "transform": {
+ "^.+\\.jsx?$": "babel-jest"
+ },
+ "moduleNameMapper": {
+ "^nimma/legacy$": "/../../../../../node_modules/nimma/dist/legacy/cjs/index.js",
+ "^nimma/(.*)": "/../../../../../node_modules/nimma/dist/cjs/$1"
+ }
+ }
+}
diff --git a/packages/templates/clients/websocket/test/javascript/utils.js b/packages/templates/clients/websocket/test/javascript/utils.js
new file mode 100644
index 000000000..636ebba93
--- /dev/null
+++ b/packages/templates/clients/websocket/test/javascript/utils.js
@@ -0,0 +1,70 @@
+/* istanbul ignore file */
+const fetch = require('node-fetch');
+
+/**
+ * Polls the Microcks API every 2 seconds until a test with the given ID reports success.
+ *
+ * @param {string} url - link to endpoint providing info on particular test
+ * @returns {Promise} Resolves with `true` when the test is marked as successful.
+ */
+async function waitForTestSuccess(url) {
+ while (true) {
+ try {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' }
+ });
+
+ if (!response.ok) {
+ console.error(`Request failed with status: ${response.status}`);
+ break;
+ }
+
+ const data = await response.json();
+
+ if (data.success) {
+ return true;
+ }
+ } catch (err) {
+ break;
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
+ }
+
+ return false;
+}
+
+/**
+ * Wait for a specific message to appear in the spy's log calls.
+ * @param {array} a - array with messages
+ * @param {string} expectedMessage - The message to look for in logs.
+ * @param {number} timeout - Maximum time (in ms) to wait. Default is 3000ms.
+ * @param {number} interval - How long to wait (in ms) until next check. Default is 10ms.
+ * @returns {Promise} Resolves if the message is found, otherwise rejects.
+ */
+async function waitForMessage(a, expectedMessage, timeout = 3000, interval = 10) {
+ const start = Date.now();
+ while (Date.now() - start < timeout) {
+ if (a.some(message => message.includes(expectedMessage))) {
+ return;
+ }
+ await delay(interval);
+ }
+ throw new Error(`Expected message "${expectedMessage}" not found within timeout`);
+}
+
+/**
+ * Delay execution by a specified time.
+ * @param {number} time - Delay duration in milliseconds. Default is 1000ms.
+ * @returns {Promise} Resolves after the specified delay.
+ */
+async function delay(time = 1000) {
+ return new Promise(resolve => setTimeout(resolve, time));
+}
+
+module.exports = {
+ waitForMessage,
+ delay,
+ waitForTestSuccess
+};
\ No newline at end of file
diff --git a/packages/templates/clients/websocket/test/microcks-setup/checkMicrocksReady.sh b/packages/templates/clients/websocket/test/microcks-setup/checkMicrocksReady.sh
new file mode 100644
index 000000000..5228e30c3
--- /dev/null
+++ b/packages/templates/clients/websocket/test/microcks-setup/checkMicrocksReady.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# List the container names as defined in your Compose file
+containers=("microcks-db" "microcks-kafka" "microcks" "microcks-async-minion")
+
+# Function to check if all containers are healthy
+all_healthy() {
+ for container in "${containers[@]}"; do
+ # Use podman inspect to retrieve the health status
+ status=$(podman inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null)
+ if [[ "$status" != "healthy" ]]; then
+ echo "Container $container is not healthy yet (current status: $status)"
+ return 1
+ fi
+ done
+ return 0
+}
+
+echo "Waiting for all services to become healthy..."
+# Loop until all containers report healthy
+until all_healthy; do
+ echo "One or more services are not healthy. Waiting 5 seconds before checking again..."
+ sleep 5
+done
+
+echo "All services are healthy. Proceeding with the next steps..."
diff --git a/packages/templates/clients/websocket/test/microcks-setup/config/application.properties b/packages/templates/clients/websocket/test/microcks-setup/config/application.properties
new file mode 100644
index 000000000..dcf21313a
--- /dev/null
+++ b/packages/templates/clients/websocket/test/microcks-setup/config/application.properties
@@ -0,0 +1,28 @@
+# Async mocking support.
+async-api.enabled=true
+
+# Access to Microcks API server.
+%docker-compose.io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url=http://microcks:8080
+
+# Access to Keycloak through docker network
+%docker-compose.keycloak.auth.url=http://keycloak:8080
+
+# Access to Kafka broker.
+%docker-compose.kafka.bootstrap.servers=kafka:19092
+
+# Do not save any consumer-offset on the broker as there's a re-sync on each minion startup.
+%docker-compose.mp.messaging.incoming.microcks-services-updates.enable.auto.commit=false
+%docker-compose.mp.messaging.incoming.microcks-services-updates.bootstrap.servers=kafka:19092
+
+# Explicitly telling the minion the protocols we want to support
+%docker-compose.minion.supported-bindings=KAFKA,WS
+
+# %docker-compose.minion.supported-bindings=KAFKA,WS,MQTT,AMQP,NATS
+
+# %docker-compose.mqtt.server=localhost:1883
+# %docker-compose.mqtt.username=microcks
+# %docker-compose.mqtt.password=microcks
+
+# %docker-compose.amqp.server=localhost:5672
+# %docker-compose.amqp.username=microcks
+# %docker-compose.amqp.password=microcks
\ No newline at end of file
diff --git a/packages/templates/clients/websocket/test/microcks-setup/config/features.properties b/packages/templates/clients/websocket/test/microcks-setup/config/features.properties
new file mode 100644
index 000000000..b088c9c8f
--- /dev/null
+++ b/packages/templates/clients/websocket/test/microcks-setup/config/features.properties
@@ -0,0 +1,8 @@
+features.feature.async-api.enabled=true
+features.feature.async-api.frequencies=3,10,30
+features.feature.async-api.default-binding=KAFKA
+features.feature.async-api.endpoint-KAFKA=localhost:9092
+features.feature.async-api.endpoint-MQTT=my-mqtt-broker.apps.try.microcks.io:1883
+features.feature.async-api.endpoint-AMQP=my-amqp-broker.apps.try.microcks.io:5672
+features.feature.async-api.endpoint-WS=localhost:8081
+features.feature.async-api.endpoint-NATS=localhost:4222
\ No newline at end of file
diff --git a/packages/templates/clients/websocket/test/microcks-setup/microcks-podman.yml b/packages/templates/clients/websocket/test/microcks-setup/microcks-podman.yml
new file mode 100644
index 000000000..e93a306a0
--- /dev/null
+++ b/packages/templates/clients/websocket/test/microcks-setup/microcks-podman.yml
@@ -0,0 +1,151 @@
+volumes:
+ microcks-data:
+
+services:
+ mongo:
+ image: docker.io/mongo:4.4.29
+ container_name: microcks-db
+ volumes:
+ - microcks-data:/data/db
+ healthcheck:
+ test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]
+ interval: 30s
+ timeout: 1s
+ retries: 3
+
+ kafka:
+ image: docker.io/redpandadata/redpanda:v24.3.1
+ container_name: microcks-kafka
+ command:
+ - redpanda
+ - start
+ - --overprovisioned
+ - --smp
+ - "1"
+ - --memory
+ - 1G
+ - --reserve-memory
+ - 0M
+ - --node-id
+ - "0"
+ - --check=false
+ - --kafka-addr
+ - PLAINTEXT://0.0.0.0:19092,EXTERNAL://0.0.0.0:9092
+ - --advertise-kafka-addr
+ - PLAINTEXT://kafka:19092,EXTERNAL://localhost:9092
+ ports:
+ - 9092:9092
+ - 19092:19092
+ healthcheck:
+ test: ["CMD-SHELL", "timeout 5 bash -c 'echo > /dev/tcp/localhost/19092'"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+
+ app:
+ depends_on:
+ - mongo
+ image: quay.io/microcks/microcks:1.11.2
+ container_name: microcks
+ volumes:
+ - ./config:/deployments/config:Z
+ ports:
+ - 8080:8080
+ - 9090:9090
+ environment:
+ SPRING_PROFILES_ACTIVE: prod
+ SPRING_DATA_MONGODB_URI: mongodb://mongo:27017
+ SPRING_DATA_MONGODB_DATABASE: microcks
+ POSTMAN_RUNNER_URL: http://postman:3000
+ TEST_CALLBACK_URL: http://microcks:8080
+ SERVICES_UPDATE_INTERVAL: "0 0 0/2 * * *"
+ KEYCLOAK_ENABLED: "false"
+ #MAX_UPLOAD_FILE_SIZE: 3MB
+ ASYNC_MINION_URL: http://microcks-async-minion:8081
+ KAFKA_BOOTSTRAP_SERVER: kafka:19092
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
+ start_period: 35s
+ interval: 10s
+ timeout: 3s
+ retries: 3
+
+ async-minion:
+ depends_on:
+ - app
+ image: quay.io/microcks/microcks-async-minion:1.11.2
+ container_name: microcks-async-minion
+ restart: on-failure
+ volumes:
+ - ./config:/deployments/config:Z
+ ports:
+ - 8081:8081
+ environment:
+ QUARKUS_PROFILE: docker-compose
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8081/q/health/ready"]
+ start_period: 10s
+ interval: 10s
+ timeout: 2s
+ retries: 3
+
+ importer:
+ profiles:
+ - tests
+ image: quay.io/microcks/microcks-cli:latest
+ container_name: microcks-cli-importer
+ working_dir: /usr/src/app
+ volumes:
+ - ../:/usr/src/app
+ depends_on:
+ app:
+ condition: service_healthy
+ command: >
+ sh -c "microcks-cli import __fixtures__/asyncapi-hoppscotch-server.yml
+ --microcksURL=http://microcks:8080/api/
+ --keycloakClientId=microcks-serviceaccount
+ --keycloakClientSecret=ab54d329-e435-41ae-a900-ec6b3fe15c54"
+
+ #TODO:1: make these tester containers work in way that if test fails, they fails as well. So also good to do starting of podman in a way that it is awaiting status
+ #TODO:2: add CI/CD pipeline for these tests
+ tester-js:
+ profiles:
+ - tests
+ image: node:18
+ container_name: websocket-acceptance-tester-js
+ ports:
+ - 8082:8082
+ working_dir: /usr/src/app
+ volumes:
+ - ../../../../../../:/usr/src/app
+ depends_on:
+ app:
+ condition: service_healthy
+ command: ["sh", "-c", "npm install && cd packages/templates/clients/websocket/test/javascript && npm test"]
+
+ tester-py:
+ profiles:
+ - tests
+ image: python:3.11
+ container_name: websocket-acceptance-tester-py
+ ports:
+ - "8083:8083"
+ working_dir: /usr/src/app
+ volumes:
+ - ../../../../../../:/usr/src/app
+ depends_on:
+ app:
+ condition: service_healthy
+ command: ["sh", "-c", "cd packages/templates/clients/websocket/python/test/temp/snapshotTestResult && pip install -r requirements.txt && cd ../../../../test/python && pip install -r requirements.txt && pytest"]
+
+ # Below for debugging purposes.
+ # Just uncomment below and after starting environment you can enter the constainer with "podman exec -it net-debug sh" and use websocat to test the websocket connection
+
+ # net-debug:
+ # image: nicolaka/netshoot
+ # container_name: net-debug
+ # stdin_open: true
+ # tty: true
+ # command: >
+ # sh -c "apk add --no-cache websocat && sh"
\ No newline at end of file
diff --git a/packages/templates/clients/websocket/test/python/requirements.txt b/packages/templates/clients/websocket/test/python/requirements.txt
new file mode 100644
index 000000000..ca03253b5
--- /dev/null
+++ b/packages/templates/clients/websocket/test/python/requirements.txt
@@ -0,0 +1,5 @@
+pytest==7.4.4
+pytest-asyncio==0.23.3
+websockets==12.0
+requests==2.31.0
+certifi==2023.11.17
\ No newline at end of file
diff --git a/packages/templates/clients/websocket/test/python/test_acceptance.py b/packages/templates/clients/websocket/test/python/test_acceptance.py
new file mode 100644
index 000000000..580541bce
--- /dev/null
+++ b/packages/templates/clients/websocket/test/python/test_acceptance.py
@@ -0,0 +1,90 @@
+import time
+import sys
+import os
+import pytest
+import asyncio
+import websockets
+import requests
+
+module_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../python/test/temp/snapshotTestResult'))
+
+microcks_test_endpoint = 'http://microcks:8080/api/tests'
+
+# Add this directory to sys.path if it isn’t already there.
+if module_path not in sys.path:
+ sys.path.insert(0, module_path)
+
+from client_hoppscotch import HoppscotchEchoWebSocketClient
+
+def test_hoppscotch_client_receives_message():
+ # Instantiate the client using the default URL.
+ client = HoppscotchEchoWebSocketClient('ws://microcks-async-minion:8081/api/ws/Hoppscotch+WebSocket+Server/1.0.0/sendTimeStampMessage')
+
+ received_messages = []
+ expected_message = "GMT+0000 (Coordinated Universal Time)"
+
+ def message_handler(message):
+ received_messages.append(message)
+
+ client.register_message_handler(message_handler)
+
+ client.connect()
+
+ # Wait up to 10 seconds for a message to be received.
+ timeout = 10 # seconds
+ start_time = time.time()
+ while not received_messages and (time.time() - start_time < timeout):
+ time.sleep(0.1)
+
+ # Close the connection.
+ client.close()
+
+ assert expected_message in received_messages[0], (
+ f"Expected message '{expected_message}' but got '{received_messages[0]}'"
+ )
+
+@pytest.mark.asyncio
+async def test_hoppscotch_client_sends_message():
+ port = 8083
+ expected_outgoing_message = "Sending acceptance test message to Microcks."
+
+ # payload for trigger Microcks test
+ payload = {
+ "serviceId": "Hoppscotch WebSocket Server:1.0.0",
+ "testEndpoint": "ws://websocket-acceptance-tester-py:8083/ws",
+ "runnerType": "ASYNC_API_SCHEMA",
+ "timeout": 30000,
+ "filteredOperations": ["RECEIVE handleEchoMessage"]
+ }
+ # Create WebSocket server handler
+ async def handler(websocket):
+ ###############
+ #
+ # Most improtant part of test where we test clients send message
+ #
+ ###############
+ await HoppscotchEchoWebSocketClient.send_message_static(expected_outgoing_message, websocket)
+
+ # Start the WebSocket server
+ server = await websockets.serve(handler, port=port)
+ await asyncio.sleep(1) # Give server time to start
+
+ # Start test in Microcks
+ response = requests.post(microcks_test_endpoint, json=payload)
+ response.raise_for_status()
+ response_data = response.json()
+ test_id = response_data.get('id')
+
+ # Poll Microcks for result
+ success = False
+ for _ in range(30):
+ result = requests.get(f"{microcks_test_endpoint}/{test_id}").json()
+ print("Polling Microcks:", result)
+ if result.get("success") is True:
+ success = True
+ break
+ await asyncio.sleep(2)
+ #await asyncio.sleep(1000)
+ server.close()
+
+ assert success, f"Microcks test {test_id} did not succeed"
\ No newline at end of file