From 14897c06248e4cf2f94ba8aa9e64dfac613863a0 Mon Sep 17 00:00:00 2001 From: Mahad Munir Date: Tue, 4 Nov 2025 19:59:25 +0500 Subject: [PATCH 1/5] add rpc integration tests --- .github/workflows/main.yaml | 6 +++ Makefile | 10 +++++ tests/.nxt/config.yaml | 64 ++++++++++++++++++++++++++++++ tests/integration/__init__.py | 0 tests/integration/rpc_test.py | 75 +++++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+) create mode 100644 tests/.nxt/config.yaml create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/rpc_test.py diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f2dc99a..3c4131e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -33,6 +33,12 @@ jobs: - name: Run tests run: make test + - name: Run Integration tests + run: | + make install-nxt + nxt start -c tests/ & + make integration + ruff: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 30e1e06..87dab38 100644 --- a/Makefile +++ b/Makefile @@ -37,3 +37,13 @@ build-docs: clean-docs: rm -rf site/ + +install-nxt: + @if ! command -v nxt >/dev/null 2>&1; then \ + sudo snap install nxt-router --classic --edge; \ + fi + + +integration: + make install-nxt + ./.venv/bin/pytest -s -v tests/integration/ diff --git a/tests/.nxt/config.yaml b/tests/.nxt/config.yaml new file mode 100644 index 0000000..033c2cf --- /dev/null +++ b/tests/.nxt/config.yaml @@ -0,0 +1,64 @@ +version: '1' + +config: + loglevel: debug + management: true + +realms: + - name: realm1 + roles: + - name: anonymous + permissions: + - uri: "" + match: prefix + allow_call: true + allow_register: true + allow_publish: true + allow_subscribe: true + + - name: io.xconn.mgmt + roles: + - name: anonymous + permissions: + - uri: "io.xconn.mgmt." + match: prefix + allow_call: true + allow_subscribe: true + +transports: + - type: websocket + listener: tcp + address: localhost:8079 + serializers: + - json + - cbor + - msgpack + +authenticators: + cryptosign: + - authid: john + realm: realm1 + role: anonymous + authorized_keys: + - 20e6ff0eb2552204fac19a15a61da586e437abd64a545bedce61a89b48184fcb + + wampcra: + - authid: john + realm: realm1 + role: anonymous + secret: hello + + ticket: + - authid: john + realm: realm1 + role: anonymous + ticket: hello + + anonymous: + - authid: john + realm: realm1 + role: anonymous + + - authid: john + realm: io.xconn.mgmt + role: anonymous diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/rpc_test.py b/tests/integration/rpc_test.py new file mode 100644 index 0000000..dbd6366 --- /dev/null +++ b/tests/integration/rpc_test.py @@ -0,0 +1,75 @@ +import os +import hashlib +from concurrent.futures import ThreadPoolExecutor + +import pytest +from wampproto import serializers + +from xconn import Client +from xconn.types import Invocation +from xconn.client import connect_anonymous +from xconn.exception import ApplicationError + + +def test_rpc(): + client1 = connect_anonymous("ws://localhost:8079/ws", "realm1") + client2 = connect_anonymous("ws://localhost:8079/ws", "realm1") + + args = ["client1", "client2"] + + def inv_handler_with_args(inv: Invocation): + assert inv.args == args + assert inv.kwargs is None + + args_registration = client1.register("io.xconn.rpc.args", inv_handler_with_args) + client2.call("io.xconn.rpc.args", args) + args_registration.unregister() + + with pytest.raises(ApplicationError, match="wamp.error.no_such_procedure"): + client2.call("io.xconn.rpc.args", args) + + kwargs = {"foo": "bar", "baz": {"k": "v"}} + + def inv_handler_with_kwargs(inv: Invocation): + assert inv.args == [] + assert inv.kwargs == kwargs + + registration = client1.register("io.xconn.rpc.kwargs", inv_handler_with_kwargs) + client2.call("io.xconn.rpc.kwargs", kwargs=kwargs) + + registration.unregister() + + client2.leave() + client1.leave() + + +@pytest.mark.parametrize( + "serializer", [serializers.CBORSerializer(), serializers.MsgPackSerializer(), serializers.JSONSerializer()] +) +def test_rpc_with_various_data(serializer: serializers.Serializer): + client1 = Client(serializer=serializer).connect("ws://localhost:8079/ws", "realm1") + client2 = Client(serializer=serializer).connect("ws://localhost:8079/ws", "realm1") + + def inv_handler(inv: Invocation): + payload: bytes = inv.kwargs["payload"] + checksum: bytes = inv.kwargs["checksum"] + + calculated_checksum = hashlib.sha256(payload).digest() + assert calculated_checksum == checksum, f"Checksum mismatch! got {calculated_checksum}, expected {checksum}" + + client1.register("io.xconn.rpc.inv_handler", inv_handler) + + def send_payload(size_bytes: int): + payload = os.urandom(size_bytes) + checksum = hashlib.sha256(payload).digest() + + client2.call("io.xconn.rpc.inv_handler", kwargs={"payload": payload, "checksum": checksum}) + + # test call with different payload sizes + sizes = [1024 * n for n in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1023]] + + with ThreadPoolExecutor(max_workers=8) as executor: + executor.map(send_payload, sizes) + + client1.leave() + client2.leave() From 9db1ed88b9c458626ca0c2224465127edbbd9239 Mon Sep 17 00:00:00 2001 From: Mahad Munir Date: Tue, 4 Nov 2025 20:07:50 +0500 Subject: [PATCH 2/5] separate unit, integration & aat tests --- .github/workflows/main.yaml | 24 +++++++++++++++--------- Makefile | 5 ++++- tests/aat/__init__.py | 0 tests/{ => aat}/client_test.py | 0 tests/{ => aat}/interop_test.py | 0 tests/integration/rpc_test.py | 2 +- tests/unit/__init__.py | 0 tests/{ => unit}/acceptor_test.py | 0 tests/{ => unit}/router_test.py | 0 9 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 tests/aat/__init__.py rename tests/{ => aat}/client_test.py (100%) rename tests/{ => aat}/interop_test.py (100%) create mode 100644 tests/unit/__init__.py rename tests/{ => unit}/acceptor_test.py (100%) rename tests/{ => unit}/router_test.py (100%) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3c4131e..c343ebb 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -22,23 +22,29 @@ jobs: - name: Setup environment & install dependencies run: make setup - - name: Setup AAT - run: | - git clone https://github.com/xconnio/xconn-aat-setup.git - cd xconn-aat-setup - make up - sudo snap install wick --classic - timeout 30 bash -c 'until wick --url ws://localhost:8081/ws publish test; do sleep 1; done' - - - name: Run tests + - name: Run unit tests run: make test + - name: Install wick + run: sudo snap install wick --classic + - name: Run Integration tests run: | make install-nxt nxt start -c tests/ & + timeout 30 bash -c 'until wick --url ws://localhost:8079/ws publish test; do sleep 1; done' make integration + - name: Setup AAT + run: | + git clone https://github.com/xconnio/xconn-aat-setup.git + cd xconn-aat-setup + make up + timeout 30 bash -c 'until wick --url ws://localhost:8081/ws publish test; do sleep 1; done' + + - name: Run AAT tests + run: make aat + ruff: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 87dab38..b5ea7d4 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ lint: ./.venv/bin/ruff check . test: - ./.venv/bin/pytest -s -v + ./.venv/bin/pytest -s -v tests/unit run: ./.venv/bin/xconn example:app --directory examples/simple @@ -47,3 +47,6 @@ install-nxt: integration: make install-nxt ./.venv/bin/pytest -s -v tests/integration/ + +aat: + ./.venv/bin/pytest -s -v tests/aat/ diff --git a/tests/aat/__init__.py b/tests/aat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/client_test.py b/tests/aat/client_test.py similarity index 100% rename from tests/client_test.py rename to tests/aat/client_test.py diff --git a/tests/interop_test.py b/tests/aat/interop_test.py similarity index 100% rename from tests/interop_test.py rename to tests/aat/interop_test.py diff --git a/tests/integration/rpc_test.py b/tests/integration/rpc_test.py index dbd6366..f5b1e31 100644 --- a/tests/integration/rpc_test.py +++ b/tests/integration/rpc_test.py @@ -44,7 +44,7 @@ def inv_handler_with_kwargs(inv: Invocation): @pytest.mark.parametrize( - "serializer", [serializers.CBORSerializer(), serializers.MsgPackSerializer(), serializers.JSONSerializer()] + "serializer", [serializers.CBORSerializer(), serializers.MsgPackSerializer()] ) def test_rpc_with_various_data(serializer: serializers.Serializer): client1 = Client(serializer=serializer).connect("ws://localhost:8079/ws", "realm1") diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptor_test.py b/tests/unit/acceptor_test.py similarity index 100% rename from tests/acceptor_test.py rename to tests/unit/acceptor_test.py diff --git a/tests/router_test.py b/tests/unit/router_test.py similarity index 100% rename from tests/router_test.py rename to tests/unit/router_test.py From 48f7614374896d6c8d2ef044eaf42a0be16ca842 Mon Sep 17 00:00:00 2001 From: Mahad Munir Date: Wed, 5 Nov 2025 14:47:22 +0500 Subject: [PATCH 3/5] add async rpc test --- tests/integration/async_rpc_test.py | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/integration/async_rpc_test.py diff --git a/tests/integration/async_rpc_test.py b/tests/integration/async_rpc_test.py new file mode 100644 index 0000000..570867c --- /dev/null +++ b/tests/integration/async_rpc_test.py @@ -0,0 +1,76 @@ +import os +import asyncio +import hashlib + +import pytest +from wampproto import serializers + +from xconn import AsyncClient +from xconn.types import Invocation +from xconn.async_client import connect_anonymous +from xconn.exception import ApplicationError + + +async def test_rpc(): + client1 = await connect_anonymous("ws://localhost:8079/ws", "realm1") + client2 = await connect_anonymous("ws://localhost:8079/ws", "realm1") + + args = ["client1", "client2"] + + async def inv_handler_with_args(inv: Invocation): + assert inv.args == args + assert inv.kwargs is None + + args_registration = await client1.register("io.xconn.rpc.args", inv_handler_with_args) + await client2.call("io.xconn.rpc.args", args) + await args_registration.unregister() + + with pytest.raises(ApplicationError, match="wamp.error.no_such_procedure"): + await client2.call("io.xconn.rpc.args", args) + + kwargs = {"foo": "bar", "baz": {"k": "v"}} + + async def inv_handler_with_kwargs(inv: Invocation): + assert inv.args == [] + assert inv.kwargs == kwargs + + registration = await client1.register("io.xconn.rpc.kwargs", inv_handler_with_kwargs) + await client2.call("io.xconn.rpc.kwargs", kwargs=kwargs) + + await registration.unregister() + + await client2.leave() + await client1.leave() + + +@pytest.mark.parametrize( + "serializer", [serializers.CBORSerializer(), serializers.MsgPackSerializer()] +) +async def test_rpc_with_various_data(serializer: serializers.Serializer): + async_client = AsyncClient(serializer=serializer) + client1 = await async_client.connect("ws://localhost:8079/ws", "realm1") + async_client2 = AsyncClient(serializer=serializer) + client2 = await async_client2.connect("ws://localhost:8079/ws", "realm1") + + async def inv_handler(inv: Invocation): + payload: bytes = inv.kwargs["payload"] + checksum: bytes = inv.kwargs["checksum"] + + calculated_checksum = hashlib.sha256(payload).digest() + assert calculated_checksum == checksum, f"Checksum mismatch! got {calculated_checksum}, expected {checksum}" + + await client1.register("io.xconn.rpc.inv_handler", inv_handler) + + async def send_payload(size_bytes: int): + payload = os.urandom(size_bytes) + checksum = hashlib.sha256(payload).digest() + + await client2.call("io.xconn.rpc.inv_handler", kwargs={"payload": payload, "checksum": checksum}) + + # test call with different payload sizes + sizes = [1024 * n for n in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1023]] + + await asyncio.gather(*(send_payload(size) for size in sizes)) + + await client1.leave() + await client2.leave() From 35c2317e5fd35f5b59408d9da60fb8746f2e12db Mon Sep 17 00:00:00 2001 From: Mahad Munir Date: Wed, 5 Nov 2025 16:44:40 +0500 Subject: [PATCH 4/5] add async pubsub test --- tests/integration/async_pubsub_test.py | 70 ++++++++++++++++++++++++ tests/integration/pubsub_test.py | 73 ++++++++++++++++++++++++++ tests/integration/rpc_test.py | 6 ++- 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 tests/integration/async_pubsub_test.py create mode 100644 tests/integration/pubsub_test.py diff --git a/tests/integration/async_pubsub_test.py b/tests/integration/async_pubsub_test.py new file mode 100644 index 0000000..eb54f63 --- /dev/null +++ b/tests/integration/async_pubsub_test.py @@ -0,0 +1,70 @@ +import os +import asyncio +import hashlib + +import pytest +from wampproto import serializers + +from xconn import AsyncClient +from xconn.types import Event +from xconn.async_client import connect_anonymous + + +async def test_pubsub(): + client1 = await connect_anonymous("ws://localhost:8079/ws", "realm1") + client2 = await connect_anonymous("ws://localhost:8079/ws", "realm1") + + args = ["client1", "client2"] + + async def event_handler_with_args(event: Event): + assert event.args == args + assert event.kwargs is None + + args_subscription = await client1.subscribe("io.xconn.pubsub.args", event_handler_with_args) + await client2.publish("io.xconn.pubsub.args", args, options={"acknowledge": True}) + await args_subscription.unsubscribe() + + kwargs = {"foo": "bar", "baz": {"k": "v"}} + + async def event_handler_with_kwargs(event: Event): + assert event.args == [] + assert event.kwargs == kwargs + + subscription = await client1.subscribe("io.xconn.pubsub.kwargs", event_handler_with_kwargs) + await client2.publish("io.xconn.pubsub.kwargs", kwargs=kwargs, options={"acknowledge": True}) + + await subscription.unsubscribe() + + await client2.leave() + await client1.leave() + + +@pytest.mark.parametrize("serializer", [serializers.CBORSerializer(), serializers.MsgPackSerializer()]) +async def test_pubsub_with_various_data(serializer: serializers.Serializer): + async_client = AsyncClient(serializer=serializer) + client1 = await async_client.connect("ws://localhost:8079/ws", "realm1") + async_client2 = AsyncClient(serializer=serializer) + client2 = await async_client2.connect("ws://localhost:8079/ws", "realm1") + + async def event_handler(inv: Event): + payload: bytes = inv.kwargs["payload"] + checksum: bytes = inv.kwargs["checksum"] + + calculated_checksum = hashlib.sha256(payload).digest() + assert calculated_checksum == checksum, f"Checksum mismatch! got {calculated_checksum}, expected {checksum}" + + await client1.subscribe("io.xconn.pubsub.event_handler", event_handler) + + async def send_payload(size_bytes: int): + payload = os.urandom(size_bytes) + checksum = hashlib.sha256(payload).digest() + + await client2.publish("io.xconn.pubsub.event_handler", kwargs={"payload": payload, "checksum": checksum}) + + # test call with different payload sizes + sizes = [1024 * n for n in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1023]] + + await asyncio.gather(*(send_payload(size) for size in sizes)) + + await client1.leave() + await client2.leave() diff --git a/tests/integration/pubsub_test.py b/tests/integration/pubsub_test.py new file mode 100644 index 0000000..045336c --- /dev/null +++ b/tests/integration/pubsub_test.py @@ -0,0 +1,73 @@ +import concurrent +import os +import hashlib +from concurrent.futures import ThreadPoolExecutor + +import pytest +from wampproto import serializers + +from xconn import Client +from xconn.types import Event +from xconn.client import connect_anonymous + + +def test_pubsub(): + client1 = connect_anonymous("ws://localhost:8079/ws", "realm1") + client2 = connect_anonymous("ws://localhost:8079/ws", "realm1") + + args = ["client1", "client2"] + + def event_handler_with_args(event: Event): + assert event.args == args + assert event.kwargs is None + + args_subscription = client1.subscribe("io.xconn.pubsub.args", event_handler_with_args) + client2.publish("io.xconn.pubsub.args", args, options={"acknowledge": True}) + args_subscription.unsubscribe() + + kwargs = {"foo": "bar", "baz": {"k": "v"}} + + def event_handler_with_kwargs(event: Event): + assert event.args == [] + assert event.kwargs == kwargs + + registration = client1.subscribe("io.xconn.pubsub.kwargs", event_handler_with_kwargs) + client2.publish("io.xconn.pubsub.kwargs", kwargs=kwargs, options={"acknowledge": True}) + + registration.unsubscribe() + + client2.leave() + client1.leave() + + +@pytest.mark.parametrize("serializer", [serializers.CBORSerializer(), serializers.MsgPackSerializer()]) +def test_pubsub_with_various_data(serializer: serializers.Serializer): + client1 = Client(serializer=serializer).connect("ws://localhost:8079/ws", "realm1") + client2 = Client(serializer=serializer).connect("ws://localhost:8079/ws", "realm1") + + def event_handler(inv: Event): + payload: bytes = inv.kwargs["payload"] + checksum: bytes = inv.kwargs["checksum"] + + calculated_checksum = hashlib.sha256(payload).digest() + assert calculated_checksum == checksum, f"Checksum mismatch! got {calculated_checksum}, expected {checksum}" + + client1.subscribe("io.xconn.pubsub.event_handler", event_handler) + + def send_payload(size_bytes: int): + payload = os.urandom(size_bytes) + checksum = hashlib.sha256(payload).digest() + + client2.publish("io.xconn.pubsub.event_handler", kwargs={"payload": payload, "checksum": checksum}) + + # test call with different payload sizes + sizes = [1024 * n for n in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1023]] + + with ThreadPoolExecutor(max_workers=8) as executor: + futures = [executor.submit(send_payload, size) for size in sizes] + + for future in concurrent.futures.as_completed(futures): + future.result() + + client1.leave() + client2.leave() diff --git a/tests/integration/rpc_test.py b/tests/integration/rpc_test.py index f5b1e31..8e5fb95 100644 --- a/tests/integration/rpc_test.py +++ b/tests/integration/rpc_test.py @@ -1,3 +1,4 @@ +import concurrent import os import hashlib from concurrent.futures import ThreadPoolExecutor @@ -69,7 +70,10 @@ def send_payload(size_bytes: int): sizes = [1024 * n for n in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1023]] with ThreadPoolExecutor(max_workers=8) as executor: - executor.map(send_payload, sizes) + futures = [executor.submit(send_payload, size) for size in sizes] + + for future in concurrent.futures.as_completed(futures): + future.result() client1.leave() client2.leave() From 83052cac9107531746cc2560e7ea0267c86b5731 Mon Sep 17 00:00:00 2001 From: Mahad Munir Date: Thu, 6 Nov 2025 13:10:54 +0500 Subject: [PATCH 5/5] remove management realm in nxt config --- tests/.nxt/config.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/.nxt/config.yaml b/tests/.nxt/config.yaml index 033c2cf..9670b52 100644 --- a/tests/.nxt/config.yaml +++ b/tests/.nxt/config.yaml @@ -16,15 +16,6 @@ realms: allow_publish: true allow_subscribe: true - - name: io.xconn.mgmt - roles: - - name: anonymous - permissions: - - uri: "io.xconn.mgmt." - match: prefix - allow_call: true - allow_subscribe: true - transports: - type: websocket listener: tcp