251 lines
9.4 KiB
Python
251 lines
9.4 KiB
Python
"""
|
|
Comprehensive WebSocket Test Suite (Section 37, 38, 40).
|
|
Tests connection lifecycle, handshake, message protocol, typing/processing events,
|
|
deduplication, safety alerts, error protocol, multilingual handling, and REST backward compatibility.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
def create_test_session(patient_id: str = "PAT-WS-100", language: str = "en") -> str:
|
|
"""Helper to initialize an active onboarding session via REST API."""
|
|
response = client.post(
|
|
"/v1/onboarding/start",
|
|
json={
|
|
"patient": {"id": patient_id, "name": "WebSocket Test Patient", "age": 32, "gender": "female"},
|
|
"category": {"type": "general", "code": "general_medicine", "name": "General Medicine"},
|
|
"language": language
|
|
},
|
|
headers={"Authorization": f"Bearer test_token_{patient_id}"}
|
|
)
|
|
assert response.status_code == 200
|
|
return response.json()["session_id"]
|
|
|
|
|
|
def test_websocket_successful_connection_ack():
|
|
"""Test WS connection lifecycle and connection.ack event (Section 3 & 4)."""
|
|
patient_id = "PAT-WS-ACK"
|
|
session_id = create_test_session(patient_id=patient_id)
|
|
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_{patient_id}"
|
|
with client.websocket_connect(url) as websocket:
|
|
ack_data = websocket.receive_json()
|
|
assert ack_data["type"] == "connection.ack"
|
|
assert ack_data["session_id"] == session_id
|
|
assert ack_data["status"] == "connected"
|
|
|
|
|
|
def test_websocket_invalid_session_connection():
|
|
"""Test connection to non-existent session ID is rejected (Section 24)."""
|
|
url = "/api/v1/ws/onboarding/ONB-INVALID-9999?token=test_token_PAT-WS-1"
|
|
with client.websocket_connect(url) as websocket:
|
|
err = websocket.receive_json()
|
|
assert err["type"] == "error"
|
|
assert err["code"] == "SESSION_NOT_FOUND"
|
|
|
|
|
|
def test_websocket_unauthorized_session_access():
|
|
"""Test connecting to Session A with User B token is rejected (Section 25)."""
|
|
session_id = create_test_session(patient_id="PAT-USER-A")
|
|
|
|
# Connect with token for PAT-USER-B
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_PAT-USER-B"
|
|
with client.websocket_connect(url) as websocket:
|
|
err = websocket.receive_json()
|
|
assert err["type"] == "error"
|
|
assert err["code"] == "UNAUTHORIZED"
|
|
|
|
|
|
def test_websocket_end_to_end_message_flow():
|
|
"""Required End-to-End Test (Section 38): verify complete event sequence."""
|
|
patient_id = "PAT-WS-E2E"
|
|
session_id = create_test_session(patient_id=patient_id)
|
|
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_{patient_id}"
|
|
with client.websocket_connect(url) as websocket:
|
|
# Receive ACK
|
|
ack = websocket.receive_json()
|
|
assert ack["type"] == "connection.ack"
|
|
|
|
# Send patient.message
|
|
websocket.send_json({
|
|
"type": "patient.message",
|
|
"message_id": "msg_e2e_1",
|
|
"text": "I have severe headache for 3 days"
|
|
})
|
|
|
|
events = []
|
|
# Expect: processing.started, typing.started, state.updated, assistant.message, typing.stopped, processing.completed
|
|
for _ in range(6):
|
|
event = websocket.receive_json()
|
|
events.append(event)
|
|
|
|
event_types = [e["type"] for e in events]
|
|
assert "processing.started" in event_types
|
|
assert "typing.started" in event_types
|
|
assert "state.updated" in event_types
|
|
assert "assistant.message" in event_types
|
|
assert "typing.stopped" in event_types
|
|
assert "processing.completed" in event_types
|
|
|
|
# Verify assistant response properties (Section 5 & 9)
|
|
asst_msg = next(e for e in events if e["type"] == "assistant.message")
|
|
assert asst_msg["message_id"] == "resp_msg_e2e_1"
|
|
assert len(asst_msg["reply"].split()) <= 20
|
|
assert asst_msg["reply"].count("?") <= 1
|
|
|
|
|
|
def test_websocket_duplicate_message_prevention():
|
|
"""Test duplicate message_id protection returns cached response (Section 17)."""
|
|
patient_id = "PAT-WS-DEDUP"
|
|
session_id = create_test_session(patient_id=patient_id)
|
|
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_{patient_id}"
|
|
with client.websocket_connect(url) as websocket:
|
|
websocket.receive_json() # ACK
|
|
|
|
msg_payload = {
|
|
"type": "patient.message",
|
|
"message_id": "msg_dedup_101",
|
|
"text": "Fever for two days"
|
|
}
|
|
|
|
# First send
|
|
websocket.send_json(msg_payload)
|
|
events_1 = [websocket.receive_json() for _ in range(6)]
|
|
asst_1 = next(e for e in events_1 if e["type"] == "assistant.message")
|
|
|
|
# Duplicate send with SAME message_id
|
|
websocket.send_json(msg_payload)
|
|
cached_resp = websocket.receive_json()
|
|
assert cached_resp["type"] == "assistant.message"
|
|
assert cached_resp["reply"] == asst_1["reply"]
|
|
|
|
|
|
def test_websocket_emergency_safety_alert():
|
|
"""Test emergency message triggers safety.alert event (Section 12)."""
|
|
patient_id = "PAT-WS-EMERGENCY"
|
|
session_id = create_test_session(patient_id=patient_id)
|
|
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_{patient_id}"
|
|
with client.websocket_connect(url) as websocket:
|
|
websocket.receive_json() # ACK
|
|
|
|
websocket.send_json({
|
|
"type": "patient.message",
|
|
"message_id": "msg_emerg_1",
|
|
"text": "I have severe crushing chest pain radiating to my arm and shortness of breath"
|
|
})
|
|
|
|
received = []
|
|
for _ in range(8): # Includes safety alert and onboarding completed
|
|
received.append(websocket.receive_json())
|
|
|
|
event_types = [e["type"] for e in received]
|
|
assert "safety.alert" in event_types
|
|
|
|
safety_event = next(e for e in received if e["type"] == "safety.alert")
|
|
assert safety_event["level"] in ["emergency", "crisis"]
|
|
|
|
|
|
def test_websocket_invalid_json_handling():
|
|
"""Test malformed non-JSON text returns error event without closing socket (Section 15)."""
|
|
patient_id = "PAT-WS-BADJSON"
|
|
session_id = create_test_session(patient_id=patient_id)
|
|
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_{patient_id}"
|
|
with client.websocket_connect(url) as websocket:
|
|
websocket.receive_json() # ACK
|
|
|
|
# Send raw non-JSON text
|
|
websocket.send_text("THIS_IS_NOT_JSON")
|
|
err_resp = websocket.receive_json()
|
|
assert err_resp["type"] == "error"
|
|
assert err_resp["code"] == "INVALID_MESSAGE"
|
|
|
|
# Verify socket is still active by sending valid message after
|
|
websocket.send_json({
|
|
"type": "patient.message",
|
|
"message_id": "msg_valid_after_error",
|
|
"text": "I am fine now"
|
|
})
|
|
events = [websocket.receive_json() for _ in range(6)]
|
|
assert any(e["type"] == "assistant.message" for e in events)
|
|
|
|
|
|
def test_websocket_oversized_message():
|
|
"""Test message > 5000 chars returns MESSAGE_TOO_LARGE error (Section 16)."""
|
|
patient_id = "PAT-WS-LARGE"
|
|
session_id = create_test_session(patient_id=patient_id)
|
|
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_{patient_id}"
|
|
with client.websocket_connect(url) as websocket:
|
|
websocket.receive_json() # ACK
|
|
|
|
oversized_text = "A" * 5050
|
|
websocket.send_json({
|
|
"type": "patient.message",
|
|
"message_id": "msg_large_1",
|
|
"text": oversized_text
|
|
})
|
|
|
|
err_resp = websocket.receive_json()
|
|
assert err_resp["type"] == "error"
|
|
assert err_resp["code"] == "MESSAGE_TOO_LARGE"
|
|
|
|
|
|
def test_websocket_multilingual_unicode():
|
|
"""Test Tamil/Hindi multilingual messages process seamlessly over WebSocket (Section 16)."""
|
|
patient_id = "PAT-WS-TAMIL"
|
|
session_id = create_test_session(patient_id=patient_id, language="ta")
|
|
|
|
url = f"/api/v1/ws/onboarding/{session_id}?token=test_token_{patient_id}"
|
|
with client.websocket_connect(url) as websocket:
|
|
websocket.receive_json() # ACK
|
|
|
|
websocket.send_json({
|
|
"type": "patient.message",
|
|
"message_id": "msg_ta_1",
|
|
"text": "எனக்கு 3 நாளா தலைவலி இருக்கு"
|
|
})
|
|
|
|
events = [websocket.receive_json() for _ in range(6)]
|
|
asst = next(e for e in events if e["type"] == "assistant.message")
|
|
assert asst["reply"] is not None
|
|
assert len(asst["reply"]) > 0
|
|
|
|
|
|
def test_rest_api_backward_compatibility():
|
|
"""Verify Section 36 requirement: REST APIs continue working untouched."""
|
|
session_id = create_test_session(patient_id="PAT-REST-BACKWARD")
|
|
|
|
# Send REST message
|
|
res = client.post(
|
|
f"/v1/onboarding/{session_id}/message",
|
|
json={"message": "Headache for two days"},
|
|
headers={"Authorization": "Bearer test_token_PAT-REST-BACKWARD"}
|
|
)
|
|
assert res.status_code == 200
|
|
assert res.json()["success"] is True
|
|
assert "message" in res.json()
|
|
|
|
|
|
def test_websocket_tester_ui_endpoint():
|
|
"""Verify that the sample UI page for WebSocket testing returns 200 OK HTML."""
|
|
res = client.get("/ws-tester")
|
|
assert res.status_code == 200
|
|
assert "text/html" in res.headers["content-type"]
|
|
assert "AI Patient Onboarding" in res.text
|
|
assert "WebSocket" in res.text
|
|
|