"""Tests for meeting_room.server — FastAPI app, REST API, WebSocket/SSE.""" from __future__ import annotations import asyncio import pytest from fastapi import HTTPException from fastapi.testclient import TestClient from meeting_room.events import AsyncEventBridge, EventEmitter from meeting_room.server import ServerSession, SessionStore, app, store @pytest.fixture(autouse=True) def _reset_store(): """Clear the global session store before each test.""" store._sessions.clear() yield @pytest.fixture def client(): """FastAPI test client.""" return TestClient(app) # --------------------------------------------------------------------------- # SessionStore # --------------------------------------------------------------------------- class TestSessionStore: def test_create_session(self) -> None: store = SessionStore() session = store.create({"providers": {}, "roles": {}, "defaults": {}}, _minimal_discussion_config()) assert session.id assert session.status == "created" def test_get_session(self) -> None: store = SessionStore() session = store.create({"providers": {}, "roles": {}, "defaults": {}}, _minimal_discussion_config()) found = store.get(session.id) assert found is session def test_get_nonexistent_session(self) -> None: store = SessionStore() assert store.get("nonexistent") is None def test_list_sessions(self) -> None: store = SessionStore() store.create({"providers": {}, "roles": {}, "defaults": {}}, _minimal_discussion_config()) store.create({"providers": {}, "roles": {}, "defaults": {}}, _minimal_discussion_config()) assert len(store.list_sessions()) == 2 # --------------------------------------------------------------------------- # REST API endpoints # --------------------------------------------------------------------------- class TestCreateSession: def test_create_session_returns_id_and_status(self, client) -> None: response = client.post("/api/sessions") assert response.status_code == 200 data = response.json() assert "id" in data assert data["status"] == "created" class TestListSessions: def test_list_empty(self, client) -> None: response = client.get("/api/sessions") assert response.status_code == 200 assert response.json() == [] def test_list_after_create(self, client) -> None: client.post("/api/sessions") client.post("/api/sessions") response = client.get("/api/sessions") assert response.status_code == 200 assert len(response.json()) == 2 class TestGetSession: def test_get_existing_session(self, client) -> None: create_resp = client.post("/api/sessions") session_id = create_resp.json()["id"] response = client.get(f"/api/sessions/{session_id}") assert response.status_code == 200 assert response.json()["id"] == session_id def test_get_nonexistent_session(self, client) -> None: response = client.get("/api/sessions/nonexistent") assert response.status_code == 404 class TestRolesAndProviders: def test_list_roles(self, client) -> None: response = client.get("/api/roles") assert response.status_code == 200 data = response.json() assert "moderator" in data or isinstance(data, dict) def test_list_providers(self, client) -> None: response = client.get("/api/providers") assert response.status_code == 200 data = response.json() assert isinstance(data, dict) # --------------------------------------------------------------------------- # Session model # --------------------------------------------------------------------------- class TestServerSession: def test_build_engine(self) -> None: config = _minimal_config() session = ServerSession("test-id", config, _minimal_discussion_config()) engine = session.build_engine() assert engine is not None def test_cleanup_removes_collect_handler(self) -> None: from meeting_room.events import AGENT_MESSAGE, BOSS_TURN, AGENT_ERROR config = _minimal_config() session = ServerSession("test-id", config, _minimal_discussion_config()) # Simulate what start_discussion does def collect_message(event): pass session._collect_handler = collect_message session.event_bus.on(AGENT_MESSAGE, collect_message) session.event_bus.on(BOSS_TURN, collect_message) session.event_bus.on(AGENT_ERROR, collect_message) assert len(session.event_bus.handlers(AGENT_MESSAGE)) == 1 session.cleanup() assert len(session.event_bus.handlers(AGENT_MESSAGE)) == 0 class TestProviderMasking: def test_api_keys_masked(self) -> None: from meeting_room.server import _mask_api_keys providers = { "routerai": {"base_url": "https://api.example.com", "api_key": "sk-1234567890abcdef"}, } masked = _mask_api_keys(providers) assert masked["routerai"]["api_key"] == "sk-1****" assert masked["routerai"]["base_url"] == "https://api.example.com" def test_empty_key(self) -> None: from meeting_room.server import _mask_api_keys providers = {"test": {"base_url": "https://x.com", "api_key": ""}} masked = _mask_api_keys(providers) assert masked["test"]["api_key"] == "" class TestPathTraversal: def test_reject_parent_dir(self) -> None: from meeting_room.server import _resolve_scenario_path with pytest.raises(HTTPException) as exc_info: _resolve_scenario_path("../../etc/passwd") assert exc_info.value.status_code == 400 # --------------------------------------------------------------------------- # Helper # --------------------------------------------------------------------------- def _minimal_config() -> dict: return { "providers": { "test_provider": { "base_url": "https://api.example.com/v1", "api_key": "test-key", } }, "roles": { "moderator": { "name": "Moderator", "provider": "test_provider", "model": "gpt-4o-mini", "temperature": 0.7, "tools": "none", "system_prompt": "You are a moderator.", } }, "defaults": { "max_rounds": 3, "language": "en", "framework": "custom", "workdir": ".", }, } def _minimal_discussion_config(): from meeting_room.models import DefaultsConfig, DiscussionConfig return DiscussionConfig( providers={"test_provider": {"base_url": "https://api.example.com/v1", "api_key": "test-key"}}, roles={"moderator": {"name": "Moderator", "provider": "test_provider", "model": "gpt-4o-mini"}}, defaults=DefaultsConfig(max_rounds=3), )