Three coupled fixes from the first manual test pass:
1. Stale signed cookie no longer 500s. `rooms.me()` now raises KeyError
when the participant row is gone (the previous code deref'd None and
threw TypeError, caught by quiz.js's catch-all as 'link expired').
`/api/session/{sid}/me` translates KeyError into 401 + delete_cookie,
so the client falls back to the join form cleanly.
Returning a JSONResponse directly because `raise HTTPException`
discards Response.delete_cookie mutations (FastAPI middleware
composes a fresh response on exception).
2. Reset is now a soft restart from the student's perspective. Before
closing each student WS in `RoomManager.reset`, the server now sends
a `{"type": "session_reset"}` message. The student SPA tears down
local state and re-runs boot(); /me returns 401 (now that the
participant is gone) and the join form renders without the user
having to manually reload. The WS close handler suppresses its
"Disconnected" screen during a reset to avoid a flash.
3. "You" highlight on the student leaderboard is now matched by id, not
by name. `RoomManager.leaderboard()` accepts an optional
`you_student_id` and stamps `is_you: true` on the matching entry only.
No other students' ids leak over the wire (we still don't include
`student_id` in the public top5 payload). quiz.js's renderBoard
prefers `r.is_you` when any row is marked, falling back to name match
for backward compatibility.
41/41 tests pass. Two new tests cover (a) the 401 + cookie-clear path
after reset and (b) `is_you` marking only the requesting student.
78 lines
3.2 KiB
Python
78 lines
3.2 KiB
Python
from conftest import join_student
|
|
|
|
|
|
def test_session_metadata_join_me_and_stats(client, sid):
|
|
metadata = client.get(f"/api/session/{sid}").json()
|
|
assert metadata["title"] == "Sample Quiz"
|
|
assert metadata["state"] == "lobby"
|
|
assert metadata["current_question_idx"] is None
|
|
|
|
join = join_student(client, sid, "s1", "First Name")
|
|
assert join["ok"] is True
|
|
assert "qz_student" in client.cookies
|
|
|
|
join_student(client, sid, "s1", "Updated Name")
|
|
me = client.get(f"/api/session/{sid}/me")
|
|
assert me.status_code == 200
|
|
assert me.json()["name"] == "Updated Name"
|
|
|
|
stats = client.get(f"/api/session/{sid}/stats").json()
|
|
assert stats["question_idx"] is None
|
|
assert stats["top5"][0]["name"] == "Updated Name"
|
|
|
|
|
|
def test_root_without_sid_redirects_to_canonical(client, sid):
|
|
response = client.get("/", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == f"/?sid={sid}"
|
|
|
|
|
|
def test_me_returns_401_and_clears_cookie_when_participant_is_gone(client, sid):
|
|
"""A stale signed cookie (e.g. after admin reset wiped participants) must
|
|
return 401 with the cookie cleared, not 500. The client uses 401 to fall
|
|
back to the join form."""
|
|
join_student(client, sid, "s1", "Student One")
|
|
assert client.get(f"/api/session/{sid}/me").status_code == 200
|
|
|
|
# Simulate the post-reset state: cookie still valid by signature,
|
|
# but the participant row is gone.
|
|
rooms = client.app.state.rooms
|
|
client.portal.call(rooms.reset, sid)
|
|
|
|
response = client.get(f"/api/session/{sid}/me")
|
|
assert response.status_code == 401
|
|
# Server should send a Set-Cookie that clears the qz_student cookie.
|
|
assert any(
|
|
h.lower() == "set-cookie" and "qz_student" in v and ('Max-Age=0' in v or 'expires=' in v.lower())
|
|
for h, v in response.headers.items()
|
|
), response.headers
|
|
|
|
|
|
def test_leaderboard_marks_requesting_student_with_is_you(client, sid):
|
|
"""The student-facing top5 should mark only the requesting student's row
|
|
with `is_you: true`, never include other students' ids."""
|
|
rooms = client.app.state.rooms
|
|
join_student(client, sid, "s1", "Alice")
|
|
join_student(client, sid, "s2", "Bob")
|
|
client.portal.call(rooms.open_question, sid, 0, 5)
|
|
client.portal.call(rooms.submit_answer, sid, "s1", 0, "B")
|
|
client.portal.call(rooms.submit_answer, sid, "s2", 0, "B")
|
|
client.portal.call(rooms.close_question, sid)
|
|
|
|
# Stats endpoint reflects the requesting student's identity from cookie.
|
|
stats = client.get(f"/api/session/{sid}/stats?question_idx=0").json()
|
|
you_rows = [r for r in stats["top5"] if r.get("is_you")]
|
|
other_rows = [r for r in stats["top5"] if not r.get("is_you")]
|
|
assert len(you_rows) == 1
|
|
assert you_rows[0]["name"] in {"Alice", "Bob"}
|
|
# Other students' ids are not exposed.
|
|
assert all("student_id" not in r for r in other_rows)
|
|
|
|
|
|
def test_invalid_session_and_missing_cookie_paths(client):
|
|
response = client.get("/?sid=BAD")
|
|
assert response.status_code == 404
|
|
assert "Ask your instructor" in response.text
|
|
assert client.get("/api/session/BAD").status_code == 404
|
|
assert client.get("/api/session/BAD/me").status_code == 401
|