Files
hold-slayer/tests/test_websocket.py
Robert Helewka 4a3c14d4af
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 45s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s
docs: add Claude AI assistant rules and configuration
Add comprehensive rule documentation for AI-assisted development covering
authentication surfaces, outbound-call safety invariants, and other project
conventions to guide Claude's understanding of critical system behaviors.
2026-07-28 19:01:38 -04:00

100 lines
3.5 KiB
Python

"""
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"