"""Projector view (public read-only): - snapshot endpoint returns the expected shape - leaderboard never carries student_ids (privacy) - WS client receives a projector_state message on connect - state changes (open question, submit, close) push fresh snapshots """ from __future__ import annotations import pytest from starlette.websockets import WebSocketDisconnect from conftest import admin_login, join_student def test_projector_snapshot_includes_required_fields(client, sid): join_student(client, sid, "s1", "Alice") response = client.get(f"/api/session/{sid}/projector") assert response.status_code == 200 body = response.json() assert body["type"] == "projector_state" assert body["state"] == "lobby" assert body["sid"] == sid assert body["participant_count"] == 1 assert "qr_url" in body and body["qr_url"].startswith("data:image/svg+xml") assert "join_url" in body assert body["pool_meta"]["question_count"] >= 1 assert "score_distribution" in body assert "leaderboard" in body def test_projector_leaderboard_redacts_student_ids(client, sid): """The /admin board carries student_ids; the public projector leaderboard must NOT — student_id namespace is private.""" join_student(client, sid, "s1", "Alice") join_student(client, sid, "s2", "Bob") rooms = client.app.state.rooms client.portal.call(rooms.open_question, sid, 0, 5) client.portal.call(rooms.submit_answer, sid, "s1", 0, "B") client.portal.call(rooms.close_question, sid) snapshot = client.get(f"/api/session/{sid}/projector").json() for row in snapshot["leaderboard"]: assert "student_id" not in row, "projector leaderboard leaks student_ids" def test_projector_ws_pushes_snapshot_on_state_change(client, sid): join_student(client, sid, "s1", "Alice") admin_login(client) with client.websocket_connect(f"/ws/projector/{sid}") as ws: initial = ws.receive_json() assert initial["type"] == "projector_state" assert initial["state"] == "lobby" # Trigger a state change via the room manager directly. rooms = client.app.state.rooms client.portal.call(rooms.open_question, sid, 0, 5) push = ws.receive_json() assert push["type"] == "projector_state" assert push["state"] == "question_open" assert push["question"] is not None assert push["question"]["idx"] == 0 def test_projector_404_for_unknown_sid(client): assert client.get("/api/session/UNKNOWN/projector").status_code == 404 with pytest.raises(WebSocketDisconnect) as exc: with client.websocket_connect("/ws/projector/UNKNOWN"): pass assert exc.value.code == 4001 def test_projector_page_redirects_when_no_sid(client, sid): response = client.get("/projector/", follow_redirects=False) assert response.status_code == 302 assert response.headers["location"].endswith(f"sid={sid}")