add: chat service
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go-wst/ go-wst/
|
||||
COPY juwan-backend/go.mod juwan-backend/go.sum juwan-backend/
|
||||
WORKDIR /build/juwan-backend
|
||||
RUN go mod download
|
||||
|
||||
COPY juwan-backend/ /build/juwan-backend/
|
||||
RUN CGO_ENABLED=0 go build -o /chat-api ./app/chat/api/
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=builder /chat-api /chat-api
|
||||
COPY juwan-backend/app/chat/test/chat-api-test.yaml /etc/chat-api.yaml
|
||||
CMD ["/chat-api", "-f", "/etc/chat-api.yaml"]
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go-wst/ go-wst/
|
||||
COPY juwan-backend/go.mod juwan-backend/go.sum juwan-backend/
|
||||
WORKDIR /build/juwan-backend
|
||||
RUN go mod download
|
||||
|
||||
COPY juwan-backend/ /build/juwan-backend/
|
||||
RUN CGO_ENABLED=0 go build -o /chat-rpc ./app/chat/rpc/
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=builder /chat-rpc /chat-rpc
|
||||
COPY juwan-backend/app/chat/rpc/etc/pb.yaml /etc/pb.yaml
|
||||
CMD ["/chat-rpc", "-f", "/etc/pb.yaml"]
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEmjCCAwKgAwIBAgIQP64kUTHSRYb6YJNRMMkJ2DANBgkqhkiG9w0BAQsFADCB
|
||||
gzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSwwKgYDVQQLDCNhc2Fk
|
||||
ekBBc2FkemRlTWFjLW1pbmkubG9jYWwgKEFzYWR6KTEzMDEGA1UEAwwqbWtjZXJ0
|
||||
IGFzYWR6QEFzYWR6ZGVNYWMtbWluaS5sb2NhbCAoQXNhZHopMB4XDTI2MDQyNDA3
|
||||
NDk0NVoXDTI4MDcyNDA3NDk0NVowVzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3Bt
|
||||
ZW50IGNlcnRpZmljYXRlMSwwKgYDVQQLDCNhc2FkekBBc2FkemRlTWFjLW1pbmku
|
||||
bG9jYWwgKEFzYWR6KTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK96
|
||||
emv0wyPAnxxMVLMzp7iSOlRtq4ay68xTFOCCMSvgDTek91lyA1AL/zZ558C86dio
|
||||
9HI43VIy70BwQEzVHdPdg1bPWn02ic4197po+k/xbKUdAxSElM2JdRkr1D6OeTz2
|
||||
y4jAqL2YZu/ZWR2PZ41TSYEnSc3UKc/ZsdOanF21w5OHpL5cNzJHcQ+8KP4vMEHd
|
||||
odUwxGbp4D0/Wnd57hSO6M1XywiQRDlJq+atqiPSAG1AlI30T39KNkcfYwv90WgD
|
||||
t1S8KQhYS5ddP81TUoMymQLczxoQkv4DjG3K4UhnscRNXa3IaVWAkXX1x4eakd3X
|
||||
jKh9uNCxTtPM+iFKmbUCAwEAAaOBtDCBsTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0l
|
||||
BAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUYOQEJcqeAhlfeIBUbdPBAFGj9Ogw
|
||||
aQYDVR0RBGIwYIIJbG9jYWxob3N0gghjaGF0LWFwaYIiY2hhdC1hcGkuZGVmYXVs
|
||||
dC5zdmMuY2x1c3Rlci5sb2NhbIINKi5qdXdhbi5sb2NhbIcEfwAAAYcQAAAAAAAA
|
||||
AAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAYEABQeqvTqcNpT9cnTdz0kwNlHW
|
||||
f6GGfYQ39ZZ1XTwKbFKKner+0Oe+WkoQnMt0sTx/ImOMpC4LAaq08pU0k85d4lQA
|
||||
yGSv8mnWLyEVFnU02cfeIcMhV6qrl5Od/g4Ow2JRRlMQxg/FRzNtzIIcPwi46K5V
|
||||
mozMXIf6QOUGa4wPrh7AdybYnA2YPmJJrNCwI2ycHtapmo3T5oO1dm+KWSWbYrx7
|
||||
yiN6ZBTxaxESJfjPYCrSNXnzRuXrseDIlKYyU0j3GMmbaSOYHVWSTnsB/Mei9Tff
|
||||
uLHOalyawbsgjqT4xVd7MFXni/mk2FDwJcPH8WAg0KgHZ+M7j8oWkqj5RS8skBFc
|
||||
EK6Y4PbYRjKUWESQPGBbUwjkjSPYz2KiWz4cnXyL2MnAg1BPUskNrBfPUEWIkpWe
|
||||
XyljMEOofQBw7G9QFIrQwWD3I8ps+KicskgcUoY62AkGh4Ky0X84tJCIrS3bwkCi
|
||||
OR681vZWZqPpG3sj7zpmcnAibA0Y7Jpj+9RoR1D9
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvenpr9MMjwJ8c
|
||||
TFSzM6e4kjpUbauGsuvMUxTggjEr4A03pPdZcgNQC/82eefAvOnYqPRyON1SMu9A
|
||||
cEBM1R3T3YNWz1p9NonONfe6aPpP8WylHQMUhJTNiXUZK9Q+jnk89suIwKi9mGbv
|
||||
2Vkdj2eNU0mBJ0nN1CnP2bHTmpxdtcOTh6S+XDcyR3EPvCj+LzBB3aHVMMRm6eA9
|
||||
P1p3ee4UjujNV8sIkEQ5Savmraoj0gBtQJSN9E9/SjZHH2ML/dFoA7dUvCkIWEuX
|
||||
XT/NU1KDMpkC3M8aEJL+A4xtyuFIZ7HETV2tyGlVgJF19ceHmpHd14yofbjQsU7T
|
||||
zPohSpm1AgMBAAECggEAAiAq25343e/WkWN/ObzISJlBoUvz2S5dt93vlimvGWua
|
||||
1oa+mPoLCnfJl8V2UQMGiqGCRFojLkvMLknEq7pbKsj3gXKU3Ii42z7KQbN2Rh01
|
||||
cosPsBX2xqGRCuBTTcsqjf7boC24IiIbH5ZvBng9K5OX61PQAmsGEZdsknxez4kz
|
||||
nnc20PbQ2HlZZ8oOTgmGFoYil2q40Lfj3VOVwaIidFfy8MAZNA6T3tmh6NFvupBy
|
||||
GPNTLPDqu4b5MUX5UxX/QV+cusK6h5rcyoLdmFY8jAEstIISmtx5HaJXw5oB0+6w
|
||||
+r5F+LCNVqmS5DUfvfAHiQ5TnACyy8QKsrSnbC1SAwKBgQDBvYgeEOhI5CvLmRe9
|
||||
JCgHO/Pu14Gg63oQN3uHakjKrUOo8fFXSUMpGJZy7PPeAZJ0ssFJLarEcn+9df25
|
||||
ksluZbinwJs71rWHp1jZNVHvvHuuPxV+exrwZO7FrIUUjmZCKH8thKcGcNfAT74q
|
||||
V6WHDeQLzdAnEzibmuZnKHe7OwKBgQDn3pbyxuCgA3aZBOiVvgdVuRoCRyTRV3I8
|
||||
LbabdpDvr3UvnZy/uPQFaFcmxaasHAy8ZJulbWrLGcDxK8CZRu8xLQY6I53djd95
|
||||
8v/YIvFmWmTnkMz7qibNqKJjKVWN4WbZcd1b+Wu6kMzkkVsIU36bHoYiReQXuJwq
|
||||
7lvbV+lPzwKBgQCPEXNPIJUoHrbopqkNF4IntXIxUht7xehhyVcDbM1MPh7Ux7W9
|
||||
C3D5DBstyyVbMDYCz25Ep+CPKS6Drnora+YsDBoMZwM7cRakkkPeQq27J6j9x8AL
|
||||
osUF+MMKXpf30iBZgqZH6smcy//HGBwKEKc/0FYzEU1BTcRjxEOYsh2YuQKBgGOw
|
||||
pvOwoAkMFCSMILeo4RxxHgaWsfSzhTDscpN6savroxWazTb8/SWKC9ZmqldbI/qn
|
||||
wueoGH9EDllid0cvYU2iTwgWIhyMj+WtnWQ++c0I1lNdRVR6fn5zn4XE0rzSiVa6
|
||||
BvMxVKj88qre9+WniEqHICKCLCQqwjIPEz1GGdCvAoGAP8fzIc/1esph4FT94SxR
|
||||
CWYGKskH2/iv7LeB9xI+uS4/oz1hZ9lLhZYfFzYzyGKQjDLLDAI7mFdS30VHjYBu
|
||||
/lYZOqvs9awQjCXQ0BftU0P2wU+ANBLEZPKxyquZqItQzRavOV3a5/1iI7//vDeN
|
||||
OWMzztsAP0sRb2ns95zWpiQ=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,29 @@
|
||||
Name: chat-api
|
||||
Host: 0.0.0.0
|
||||
Port: 28888
|
||||
|
||||
Hybrid:
|
||||
Name: chat-hybrid
|
||||
Protocol: auto
|
||||
Ws:
|
||||
Name: chat-ws
|
||||
Addr: :28889
|
||||
Path: /ws/chat
|
||||
MaxConnections: 10000
|
||||
Auth:
|
||||
Enabled: true
|
||||
Source: envoy-header
|
||||
HeaderName: X-User-ID
|
||||
FallbackStrategy: auto
|
||||
MaxRetries: 3
|
||||
MaxConnections: 10000
|
||||
Auth:
|
||||
Enabled: true
|
||||
WsHeaderName: X-User-ID
|
||||
|
||||
Stateless:
|
||||
PollInterval: 100ms
|
||||
BatchSize: 100
|
||||
|
||||
Log:
|
||||
Level: debug
|
||||
@@ -0,0 +1,37 @@
|
||||
Name: chat-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
|
||||
Hybrid:
|
||||
Name: chat-hybrid
|
||||
Protocol: auto
|
||||
Ws:
|
||||
Name: chat-ws
|
||||
Addr: :8889
|
||||
Path: /ws/chat
|
||||
MaxConnections: 10000
|
||||
Auth:
|
||||
Enabled: true
|
||||
Source: envoy-header
|
||||
HeaderName: X-User-ID
|
||||
Wt:
|
||||
Addr: :8443
|
||||
Path: /wt/chat
|
||||
CertFile: /etc/certs/tls.crt
|
||||
KeyFile: /etc/certs/tls.key
|
||||
FallbackStrategy: auto
|
||||
MaxRetries: 3
|
||||
MaxConnections: 10000
|
||||
Auth:
|
||||
Enabled: true
|
||||
WsHeaderName: X-User-ID
|
||||
WtTokenSource: query
|
||||
WtTokenName: token
|
||||
WtJWTSecret: test-secret
|
||||
|
||||
Stateless:
|
||||
PollInterval: 100ms
|
||||
BatchSize: 100
|
||||
|
||||
Log:
|
||||
Level: debug
|
||||
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
chat-api:
|
||||
build:
|
||||
context: ../../../../
|
||||
dockerfile: juwan-backend/app/chat/test/Dockerfile.api
|
||||
container_name: chat-api-test
|
||||
ports:
|
||||
- "28888:8888"
|
||||
- "28889:8889"
|
||||
- "28443:8443/udp"
|
||||
volumes:
|
||||
- ./certs:/etc/certs:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "true"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 5
|
||||
@@ -0,0 +1,5 @@
|
||||
Name: pb.rpc
|
||||
ListenOn: 0.0.0.0:28080
|
||||
|
||||
Log:
|
||||
Level: debug
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
echo "=== Chat Service Test Runner ==="
|
||||
echo "Log directory: $LOG_DIR"
|
||||
echo ""
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "=== Collecting container logs ==="
|
||||
docker compose -f "$SCRIPT_DIR/docker-compose.yml" logs chat-rpc > "$LOG_DIR/chat-rpc.log" 2>&1 || true
|
||||
docker compose -f "$SCRIPT_DIR/docker-compose.yml" logs chat-api > "$LOG_DIR/chat-api.log" 2>&1 || true
|
||||
echo "=== Stopping containers ==="
|
||||
docker compose -f "$SCRIPT_DIR/docker-compose.yml" down --remove-orphans 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== Step 1: Building Docker images ==="
|
||||
docker compose -f "$SCRIPT_DIR/docker-compose.yml" build 2>&1 | tee "$LOG_DIR/build.log"
|
||||
echo ""
|
||||
|
||||
echo "=== Step 2: Starting services ==="
|
||||
docker compose -f "$SCRIPT_DIR/docker-compose.yml" up -d 2>&1 | tee -a "$LOG_DIR/build.log"
|
||||
echo ""
|
||||
|
||||
echo "=== Step 3: Waiting for services to be ready ==="
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s http://localhost:28888 > /dev/null 2>&1 || [ $i -eq 30 ]; then
|
||||
break
|
||||
fi
|
||||
echo " waiting... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
sleep 3
|
||||
echo "Services should be ready."
|
||||
echo ""
|
||||
|
||||
echo "=== Step 4: Running WebSocket tests ==="
|
||||
cd "$SCRIPT_DIR"
|
||||
python3 test_ws.py 2>&1 | tee "$LOG_DIR/ws_test_stdout.log"
|
||||
WS_RC=${PIPESTATUS[0]}
|
||||
echo ""
|
||||
|
||||
echo "=== Step 5: Running WebTransport fallback tests ==="
|
||||
python3 test_wt.py 2>&1 | tee "$LOG_DIR/wt_test_stdout.log"
|
||||
WT_RC=${PIPESTATUS[0]}
|
||||
echo ""
|
||||
|
||||
echo "=== Test Summary ==="
|
||||
echo "WebSocket test: $([ $WS_RC -eq 0 ] && echo 'PASSED' || echo 'FAILED')"
|
||||
echo "WebTransport test: $([ $WT_RC -eq 0 ] && echo 'PASSED' || echo 'FAILED')"
|
||||
echo ""
|
||||
echo "Logs saved to: $LOG_DIR/"
|
||||
ls -la "$LOG_DIR/"
|
||||
|
||||
if [ $WS_RC -ne 0 ] || [ $WT_RC -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""WebSocket chat test — group chat + DM flows."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
print("installing websockets...")
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "websockets", "-q"])
|
||||
import websockets
|
||||
|
||||
WS_URL = "ws://localhost:28889/ws/chat"
|
||||
RESULTS = []
|
||||
|
||||
def log(tag, msg):
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
line = f"[{ts}] [{tag}] {msg}"
|
||||
print(line)
|
||||
RESULTS.append(line)
|
||||
|
||||
async def recv_json(ws, timeout=5):
|
||||
raw = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
||||
return json.loads(raw)
|
||||
|
||||
async def send_json(ws, data):
|
||||
await ws.send(json.dumps(data))
|
||||
|
||||
async def test_ws():
|
||||
log("TEST", "=== WebSocket Chat Test Start ===")
|
||||
|
||||
log("WS", "connecting user1...")
|
||||
user1 = await websockets.connect(WS_URL, additional_headers={"X-User-ID": "1001"})
|
||||
resp = await recv_json(user1)
|
||||
log("WS", f"user1 connected: {resp}")
|
||||
assert resp["type"] == "connected", f"expected connected, got {resp['type']}"
|
||||
|
||||
log("WS", "connecting user2...")
|
||||
user2 = await websockets.connect(WS_URL, additional_headers={"X-User-ID": "1002"})
|
||||
resp = await recv_json(user2)
|
||||
log("WS", f"user2 connected: {resp}")
|
||||
assert resp["type"] == "connected"
|
||||
|
||||
log("TEST", "--- Test 1: Create Group ---")
|
||||
await send_json(user1, {"type": "create_group", "name": "test-room"})
|
||||
resp = await recv_json(user1)
|
||||
log("WS", f"create_group response: {resp}")
|
||||
assert resp["type"] == "group_created", f"expected group_created, got {resp['type']}"
|
||||
group_id = resp["sessionId"]
|
||||
log("TEST", f"group created with id={group_id}")
|
||||
|
||||
log("TEST", "--- Test 2: Create DM ---")
|
||||
await send_json(user1, {"type": "create_dm", "targetId": 1002})
|
||||
resp = await recv_json(user1)
|
||||
log("WS", f"create_dm response: {resp}")
|
||||
assert resp["type"] == "dm_created", f"expected dm_created, got {resp['type']}"
|
||||
dm_id = resp["sessionId"]
|
||||
log("TEST", f"DM created with id={dm_id}")
|
||||
|
||||
log("TEST", "--- Test 3: Join Group ---")
|
||||
await send_json(user1, {"type": "join", "sessionId": group_id})
|
||||
msgs = []
|
||||
for _ in range(2):
|
||||
try:
|
||||
r = await recv_json(user1, timeout=3)
|
||||
msgs.append(r)
|
||||
log("WS", f"join msg: {r}")
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
types = {m["type"] for m in msgs}
|
||||
assert "joined" in types, f"expected 'joined' in {types}"
|
||||
log("TEST", f"join received types: {types}")
|
||||
|
||||
log("TEST", "--- Test 4: Send Message in Group ---")
|
||||
await send_json(user1, {"type": "message", "sessionId": group_id, "content": "hello group!"})
|
||||
resp = await recv_json(user1)
|
||||
log("WS", f"message broadcast: {resp}")
|
||||
assert resp["type"] == "message", f"expected message, got {resp['type']}"
|
||||
assert resp["content"] == "hello group!"
|
||||
|
||||
log("TEST", "--- Test 5: Send DM ---")
|
||||
await send_json(user1, {"type": "message", "sessionId": dm_id, "content": "hello DM!"})
|
||||
resp = await recv_json(user1)
|
||||
log("WS", f"DM message: {resp}")
|
||||
assert resp["type"] == "message"
|
||||
assert resp["content"] == "hello DM!"
|
||||
|
||||
log("TEST", "--- Test 6: Message History ---")
|
||||
await send_json(user1, {"type": "history", "sessionId": group_id})
|
||||
resp = await recv_json(user1)
|
||||
log("WS", f"history response: type={resp['type']} data_len={len(resp.get('data', []))}")
|
||||
assert resp["type"] == "history"
|
||||
|
||||
log("TEST", "--- Test 7: Invalid Message ---")
|
||||
await send_json(user1, {"type": "unknown_action"})
|
||||
resp = await recv_json(user1)
|
||||
log("WS", f"error response: {resp}")
|
||||
assert resp["type"] == "error"
|
||||
|
||||
log("TEST", "--- Test 8: Leave Group ---")
|
||||
await send_json(user1, {"type": "leave", "sessionId": group_id})
|
||||
|
||||
await user1.close()
|
||||
await user2.close()
|
||||
log("TEST", "=== WebSocket Chat Test PASSED ===")
|
||||
|
||||
async def main():
|
||||
try:
|
||||
await test_ws()
|
||||
return 0
|
||||
except Exception as e:
|
||||
log("FAIL", f"Test failed: {e}")
|
||||
import traceback
|
||||
log("FAIL", traceback.format_exc())
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
rc = asyncio.run(main())
|
||||
with open("logs/ws_test.log", "w") as f:
|
||||
f.write("\n".join(RESULTS) + "\n")
|
||||
sys.exit(rc)
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""WebTransport fallback test — verifies hybrid mode falls back to WS when WT is unavailable."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "websockets", "-q"])
|
||||
import websockets
|
||||
|
||||
WS_URL = "ws://localhost:28889/ws/chat"
|
||||
API_BASE = "http://localhost:28888"
|
||||
RESULTS = []
|
||||
|
||||
def log(tag, msg):
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
line = f"[{ts}] [{tag}] {msg}"
|
||||
print(line)
|
||||
RESULTS.append(line)
|
||||
|
||||
async def recv_json(ws, timeout=5):
|
||||
raw = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
||||
return json.loads(raw)
|
||||
|
||||
async def send_json(ws, data):
|
||||
await ws.send(json.dumps(data))
|
||||
|
||||
async def test_wt_fallback():
|
||||
log("TEST", "=== WebTransport Fallback Test Start ===")
|
||||
|
||||
log("WT", "--- Test 1: WT not configured, WS fallback should work ---")
|
||||
log("WT", "connecting via WS (fallback path)...")
|
||||
ws = await websockets.connect(WS_URL, additional_headers={"X-User-ID": "2001"})
|
||||
resp = await recv_json(ws)
|
||||
log("WT", f"fallback WS connected: {resp}")
|
||||
assert resp["type"] == "connected", f"expected connected, got {resp['type']}"
|
||||
|
||||
log("WT", "--- Test 2: Full chat flow over fallback WS ---")
|
||||
await send_json(ws, {"type": "create_group", "name": "wt-fallback-room"})
|
||||
resp = await recv_json(ws)
|
||||
log("WT", f"create_group via fallback: {resp}")
|
||||
assert resp["type"] == "group_created"
|
||||
group_id = resp["sessionId"]
|
||||
|
||||
await send_json(ws, {"type": "join", "sessionId": group_id})
|
||||
resp1 = await recv_json(ws)
|
||||
log("WT", f"join broadcast: {resp1}")
|
||||
resp2 = await recv_json(ws)
|
||||
log("WT", f"join confirm: {resp2}")
|
||||
|
||||
await send_json(ws, {"type": "message", "sessionId": group_id, "content": "hello from WT fallback!"})
|
||||
resp = await recv_json(ws)
|
||||
log("WT", f"message via fallback: {resp}")
|
||||
assert resp["type"] == "message"
|
||||
assert resp["content"] == "hello from WT fallback!"
|
||||
|
||||
log("WT", "--- Test 3: DM over fallback ---")
|
||||
await send_json(ws, {"type": "create_dm", "targetId": 2002})
|
||||
resp = await recv_json(ws)
|
||||
log("WT", f"DM created via fallback: {resp}")
|
||||
assert resp["type"] == "dm_created"
|
||||
dm_id = resp["sessionId"]
|
||||
|
||||
await send_json(ws, {"type": "message", "sessionId": dm_id, "content": "DM via fallback"})
|
||||
resp = await recv_json(ws)
|
||||
log("WT", f"DM message via fallback: {resp}")
|
||||
assert resp["type"] == "message"
|
||||
|
||||
log("WT", "--- Test 4: History over fallback ---")
|
||||
await send_json(ws, {"type": "history", "sessionId": group_id})
|
||||
resp = await recv_json(ws)
|
||||
log("WT", f"history via fallback: type={resp['type']}")
|
||||
assert resp["type"] == "history"
|
||||
|
||||
log("WT", "--- Test 5: Multi-user over fallback ---")
|
||||
ws2 = await websockets.connect(WS_URL, additional_headers={"X-User-ID": "2002"})
|
||||
resp = await recv_json(ws2)
|
||||
assert resp["type"] == "connected"
|
||||
log("WT", "user2 connected via fallback WS")
|
||||
|
||||
await send_json(ws, {"type": "message", "sessionId": dm_id, "content": "cross-user DM"})
|
||||
resp = await recv_json(ws)
|
||||
log("WT", f"sender got broadcast: {resp}")
|
||||
assert resp["type"] == "message"
|
||||
|
||||
try:
|
||||
resp2 = await recv_json(ws2, timeout=2)
|
||||
log("WT", f"user2 got message: {resp2}")
|
||||
except asyncio.TimeoutError:
|
||||
log("WT", "user2 did not receive (not joined to session, expected)")
|
||||
|
||||
await ws.close()
|
||||
await ws2.close()
|
||||
|
||||
log("WT", "--- Test 6: Verify WT port is not serving (no TLS configured) ---")
|
||||
try:
|
||||
wt_ws = await asyncio.wait_for(
|
||||
websockets.connect("ws://localhost:28443/wt/chat"),
|
||||
timeout=2
|
||||
)
|
||||
await wt_ws.close()
|
||||
log("WT", "WT port unexpectedly open (might be OK if hybrid exposes it)")
|
||||
except (ConnectionRefusedError, asyncio.TimeoutError, OSError):
|
||||
log("WT", "WT port not available (expected — no TLS cert configured)")
|
||||
|
||||
log("TEST", "=== WebTransport Fallback Test PASSED ===")
|
||||
|
||||
async def main():
|
||||
try:
|
||||
await test_wt_fallback()
|
||||
return 0
|
||||
except Exception as e:
|
||||
log("FAIL", f"Test failed: {e}")
|
||||
import traceback
|
||||
log("FAIL", traceback.format_exc())
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
rc = asyncio.run(main())
|
||||
with open("logs/wt_test.log", "w") as f:
|
||||
f.write("\n".join(RESULTS) + "\n")
|
||||
sys.exit(rc)
|
||||
Reference in New Issue
Block a user