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.
This commit is contained in:
@@ -1,27 +1,57 @@
|
||||
"""
|
||||
WebSocket event-stream tests.
|
||||
|
||||
The socket is refused (4401) without the bearer token, and an
|
||||
authorized client immediately receives the synthetic trunk-status
|
||||
event followed by the replayed recent history.
|
||||
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 pydantic import SecretStr
|
||||
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 ws_app(monkeypatch):
|
||||
monkeypatch.setattr(get_settings(), "api_token", SecretStr("tok"))
|
||||
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
|
||||
@@ -38,7 +68,11 @@ def _publish(gateway, call_id: str) -> None:
|
||||
|
||||
|
||||
class TestEventStream:
|
||||
def test_refused_without_token(self, ws_app):
|
||||
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"):
|
||||
@@ -50,7 +84,7 @@ class TestEventStream:
|
||||
_publish(ws_app, "call_ws2")
|
||||
|
||||
client = TestClient(main.app)
|
||||
with client.websocket_connect("/ws/events?token=tok") as ws:
|
||||
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)]
|
||||
@@ -58,9 +92,7 @@ class TestEventStream:
|
||||
|
||||
def test_per_call_stream_filters(self, ws_app):
|
||||
client = TestClient(main.app)
|
||||
with client.websocket_connect(
|
||||
"/ws/calls/call_target/events?token=tok"
|
||||
) as ws:
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user