From 39391e594984b65e71a0f70e33935884ee753855 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Fri, 30 Jan 2026 17:55:45 -0500 Subject: [PATCH] test(websockets): add real-world integration and ping tests for V3 - Added test_ws_v3_ping.py for gateway connectivity verification. - Added test_int_websockets_v3_real.py for multi-client routing isolation verification. - Updated Script Inventory in tests/README.md. --- tests/README.md | 2 + .../test_int_websockets_v3_real.py | 86 +++++++++++++++++++ tests/integration/test_ws_v3_ping.py | 40 +++++++++ 3 files changed, 128 insertions(+) create mode 100644 tests/integration/test_int_websockets_v3_real.py create mode 100644 tests/integration/test_ws_v3_ping.py diff --git a/tests/README.md b/tests/README.md index 7208282..00db8c6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -41,6 +41,8 @@ This directory contains the automated and manual test scripts for the Aether Fas | `test_int_import_verification.py` | Basic check that all V3 routers are reachable. | | `test_int_schema_v3.py` | Verifies the enhanced schema discovery output against the real DB. | | `test_int_v3_auth_security.py` | Uses `TestClient` to verify auth bypass rules (Site vs Account). | +| `test_ws_v3_ping.py` | **Primary Gateway Test**: Verifies WebSocket V3 round-trip through Nginx and Redis. | +| `test_int_websockets_v3_real.py` | **Isolation Test**: Simulates 3 real clients to verify Group, Direct, and Broadcast routing. | ### E2E Tests (`tests/e2e/`) | Script | Description | diff --git a/tests/integration/test_int_websockets_v3_real.py b/tests/integration/test_int_websockets_v3_real.py new file mode 100644 index 0000000..c91b849 --- /dev/null +++ b/tests/integration/test_int_websockets_v3_real.py @@ -0,0 +1,86 @@ +import asyncio +import websockets +import json +import uuid + +async def test_ws_v3_real(): + # Use fastapi.localhost to hit the correct Nginx proxy block + uri_base = "ws://fastapi.localhost:5060/v3/ws" + + group_alpha = f"test_group_alpha_{uuid.uuid4().hex[:6]}" + group_beta = f"test_group_beta_{uuid.uuid4().hex[:6]}" + + client_a_id = "client_a" + client_b_id = "client_b" + client_c_id = "client_c" + + print(f"Connecting to {uri_base}...") + + try: + async with websockets.connect(f"{uri_base}/group/{group_alpha}/client/{client_a_id}") as ws_a, \ + websockets.connect(f"{uri_base}/group/{group_alpha}/client/{client_b_id}") as ws_b, \ + websockets.connect(f"{uri_base}/group/{group_beta}/client/{client_c_id}") as ws_c: + + print("✅ All 3 clients connected to real API through Nginx proxy.") + + # --- 1. GROUP MESSAGE --- + print("\nScenario 1: Group Message (Alpha)") + msg_group = { + "msg_type": "msg", + "target": "group", + "msg": "Hello Alpha Squad" + } + await ws_a.send(json.dumps(msg_group)) + + # Client B (same group) should get it + resp_b = await asyncio.wait_for(ws_b.recv(), timeout=2.0) + data_b = json.loads(resp_b) + print(f"✅ Client B received: {data_b.get('msg')}") + + # Client A (sender) should ALSO get it via Redis echo + resp_a = await asyncio.wait_for(ws_a.recv(), timeout=2.0) + data_a = json.loads(resp_a) + print(f"✅ Client A received own message: {data_a.get('msg')}") + + # Client C (Beta) should NOT get it + try: + await asyncio.wait_for(ws_c.recv(), timeout=0.5) + print("❌ ERROR: Client C received Alpha message!") + except asyncio.TimeoutError: + print("✅ Client C correctly ignored Alpha message.") + + # --- 2. DIRECT MESSAGE --- + print("\nScenario 2: Direct Message (A -> C)") + msg_direct = { + "msg_type": "msg", + "target": "direct", + "to_id": client_c_id, + "msg": "Secret code 123" + } + await ws_a.send(json.dumps(msg_direct)) + + resp_c = await asyncio.wait_for(ws_c.recv(), timeout=2.0) + data_c = json.loads(resp_c) + print(f"✅ Client C received direct: {data_c.get('msg')}") + + # --- 3. BROADCAST --- + print("\nScenario 3: Global Broadcast") + msg_bcast = { + "msg_type": "cmd", + "target": "broadcast", + "cmd": "SYSTEM_PING" + } + await ws_b.send(json.dumps(msg_bcast)) + + for ws, name in [(ws_a, "A"), (ws_b, "B"), (ws_c, "C")]: + resp = await asyncio.wait_for(ws.recv(), timeout=2.0) + data = json.loads(resp) + print(f"✅ Client {name} received broadcast: {data.get('cmd')}") + + print("\n🎉 ALL SCENARIOS PASSED ON REAL API!") + + except Exception as e: + print(f"❌ TEST FAILED: {e}") + +if __name__ == "__main__": + asyncio.run(test_ws_v3_real()) \ No newline at end of file diff --git a/tests/integration/test_ws_v3_ping.py b/tests/integration/test_ws_v3_ping.py new file mode 100644 index 0000000..92653d1 --- /dev/null +++ b/tests/integration/test_ws_v3_ping.py @@ -0,0 +1,40 @@ +import asyncio +import websockets +import json + +async def test_ping(): + # Using fastapi.localhost to avoid the default 'localhost' static file block + uri = "ws://fastapi.localhost:5060/v3/ws/group/test_group/client/test_user" + print(f"Connecting to {uri}...") + try: + # We'll explicitly set the Host header to be safe + async with websockets.connect(uri) as websocket: + print("✅ Connection established!") + + ping_msg = { + "msg_type": "heartbeat", + "target": "echo", + "msg": "Ping from Test Script" + } + + print("Sending Heartbeat (Ping)...") + await websocket.send(json.dumps(ping_msg)) + + # Wait for the echo back + print("Waiting for response...") + response = await asyncio.wait_for(websocket.recv(), timeout=3.0) + data = json.loads(response) + + if data.get("msg_type") == "heartbeat": + print(f"✅ Echo received successfully!") + print(f"Server Timestamp: {data.get('sent_at')}") + print("WS V3 is confirmed working through the gateway.") + else: + print(f"❓ Received unexpected message type: {data.get('msg_type')}") + print(f"Full payload: {data}") + + except Exception as e: + print(f"❌ Failed: {e}") + +if __name__ == "__main__": + asyncio.run(test_ping()) \ No newline at end of file