""" WebSocket event-stream tests. The socket is owner-gated: refused (4401) when SSO is enabled and no credential is supplied, and — in dev-owner mode (SSO disabled) — an authorized client immediately receives the synthetic trunk-status event followed by the replayed recent history. The WS `_authorize` resolves the owner via a DB session, so an in-memory SQLite database is wired in (StaticPool, shared across the TestClient thread) mirroring tests/test_data_layer.py. """ import asyncio import pytest from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.pool import StaticPool from starlette.testclient import TestClient from starlette.websockets import WebSocketDisconnect import db.database as dbmod import main from config import Settings, get_settings from core.gateway import AIPSTNGateway from db.database import Base from models.events import EventType, GatewayEvent @pytest.fixture def mem_db(monkeypatch): """Synchronous setup of an in-memory SQLite DB shared with the app.""" engine = create_async_engine( "sqlite+aiosqlite:///:memory:", poolclass=StaticPool, connect_args={"check_same_thread": False}, ) async def _create(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) asyncio.run(_create()) factory = async_sessionmaker(engine, expire_on_commit=False) monkeypatch.setattr(dbmod, "_engine", engine) monkeypatch.setattr(dbmod, "_session_factory", factory) yield asyncio.run(engine.dispose()) @pytest.fixture def ws_app(monkeypatch, mem_db): """Dev-owner mode: a tokenless WS connect resolves the owner.""" monkeypatch.setattr(get_settings().casdoor, "enabled", False) gateway = AIPSTNGateway(settings=Settings()) main.app.state.gateway = gateway yield gateway del main.app.state.gateway def _publish(gateway, call_id: str) -> None: asyncio.run(gateway.event_bus.publish(GatewayEvent( type=EventType.CALL_INITIATED, call_id=call_id, data={}, message=f"call {call_id}", ))) class TestEventStream: def test_refused_without_credential(self, monkeypatch, mem_db): """SSO enabled + no token → the socket is closed with 4401.""" monkeypatch.setattr(get_settings().casdoor, "enabled", True) monkeypatch.setattr(get_settings().casdoor, "endpoint", "https://id.example.test") monkeypatch.setattr(get_settings(), "owner_name", "owner@example.test") client = TestClient(main.app) with pytest.raises(WebSocketDisconnect) as exc: with client.websocket_connect("/ws/events"): pass assert exc.value.code == 4401 def test_trunk_status_then_replayed_history(self, ws_app): _publish(ws_app, "call_ws1") _publish(ws_app, "call_ws2") client = TestClient(main.app) with client.websocket_connect("/ws/events") as ws: first = ws.receive_json() assert first["type"] == EventType.SIP_TRUNK_REGISTRATION_FAILED.value replayed = [ws.receive_json() for _ in range(2)] assert [m["call_id"] for m in replayed] == ["call_ws1", "call_ws2"] def test_per_call_stream_filters(self, ws_app): client = TestClient(main.app) with client.websocket_connect("/ws/calls/call_target/events") as ws: _publish(ws_app, "call_other") _publish(ws_app, "call_target") msg = ws.receive_json() assert msg["call_id"] == "call_target"