All green. Final report:
**Phase 05 final verification pass — complete, 100% green.**
**Verified (all 5 tasks already in `complete/`):**
- `RoomSupervisor` import, `POST /api/rooms` → 5-char Crockford code (`VBE8Y`), `GET /api/rooms/{code}` → seat list, join/session/404 flows — all live over HTTP
- `docs/ROOMS.md` present with FSM diagram, crash-isolation and "What is lost on restart" sections
- Phase tests all pass: lifecycle 4-seat `WON`→`LOBBY` (7 tests), crash isolation, GC/idle via `FakeClock` (no sleeps), FSM, tick cadence, supervisor, sessions
**Results:**
- `uv run pytest` → **1506 passed, 0 failed**; coverage **98.89%** (>90% floor; `room.py` 99%, `rooms.py` 97%, `sessions.py` 99% — all ≥95% per-module gate)
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors; `uv run undercooked-levels validate` → content OK; server boots, `/healthz` 200, client at `/` 200
- e2e US-01 suite re-run after fixes: 10 passed
**Documentation — inconsistencies found and fixed:**
- `README.md`: status now lists phases 01–05 complete, next pending `06_realtime_websocket_protocol`; dropped "(phase 05, in progress)"; 3 stale "Not implemented until phase 05." references corrected
- `app/static/js/app.js` + `tests/e2e/test_us01_landing_surface.py`: placeholder string updated in lockstep to "Not implemented until phase 07." (phase 07 task 02 wires the buttons), comments updated to reflect phase-05 completion
- `AGENTS.md`: reviewed in full — §6 already documents all phase-05 modules accurately; no changes needed
**Decisions:** no code behavior changes beyond the user-visible placeholder string; no live-infrastructure touch (only a local loopback server, stopped).
**Next pending phase:** `06_realtime_websocket_protocol`.
145 lines
5.1 KiB
Python
145 lines
5.1 KiB
Python
"""Unit: the session's privacy surface (phase 05, task 04, US-07).
|
|
|
|
The store is anonymous by design (A16): the identity is a 24-byte
|
|
random token and *nothing else*. These tests lock the surface so a PII
|
|
field can never join without a test failing first:
|
|
|
|
* **the `SessionRecord` field set** — exactly the task's nine fields
|
|
(identity, name, progression, counters, stamps),
|
|
* **a `repr()` scan** — a representative record (and a representative
|
|
store) never carries `ip`, `user_agent`, `authorization` or `cookie`
|
|
(case-insensitive),
|
|
* **`SessionStore` has no attribute holding raw headers** — neither the
|
|
record's fields nor the store's attributes name a `header` of any
|
|
kind, and no dict-valued store attribute holds a header-like key.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
import pytest
|
|
|
|
from undercooked.server.clock import FakeClock
|
|
from undercooked.server.sessions import BestRun, SessionRecord, SessionStore
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
#: The only fields a session record may hold (task 04's list).
|
|
ALLOWED_RECORD_FIELDS = frozenset(
|
|
{
|
|
"token",
|
|
"display_name",
|
|
"created_at",
|
|
"unlocked_level",
|
|
"best",
|
|
"rounds_played",
|
|
"dishes_served",
|
|
"last_seen",
|
|
"endless_best",
|
|
}
|
|
)
|
|
|
|
#: The strings a session's repr must never contain (the PII markers).
|
|
FORBIDDEN_REPR_SUBSTRINGS = ("ip", "user_agent", "authorization", "cookie")
|
|
|
|
#: A request-header key that must never appear in the store's own data.
|
|
HEADER_LIKE_KEYS = frozenset(
|
|
{"host", "user-agent", "authorization", "cookie", "x-forwarded-for"}
|
|
)
|
|
|
|
|
|
def _benign_record() -> SessionRecord:
|
|
"""A representative record with fixed, benign values (a random
|
|
base64url token could incidentally contain a marker; the test needs
|
|
a *guaranteed* shape, so the token is fixed too)."""
|
|
return SessionRecord(
|
|
token="t" * 32,
|
|
display_name="Ada",
|
|
created_at=1.0,
|
|
unlocked_level=2,
|
|
best={"01_kitchen": BestRun(score=100, stars=3, completed_at=2.0)},
|
|
rounds_played=4,
|
|
dishes_served=9,
|
|
last_seen=3.0,
|
|
endless_best=None,
|
|
)
|
|
|
|
|
|
def _benign_store() -> SessionStore:
|
|
"""A representative store: a `FakeClock`, one campaign entry, and one
|
|
record inside (so a walk of its data covers the record's shape)."""
|
|
store = SessionStore(
|
|
max_entries=10, clock=FakeClock(start=1.0), campaign={"01_kitchen": 1}
|
|
)
|
|
record = _benign_record()
|
|
store.put(record)
|
|
return store
|
|
|
|
|
|
def test_the_record_field_set_is_exactly_the_task_list() -> None:
|
|
fields = {field.name for field in dataclasses.fields(SessionRecord)}
|
|
assert fields == ALLOWED_RECORD_FIELDS
|
|
|
|
|
|
def test_the_record_repr_carrys_no_pii_markers() -> None:
|
|
text = repr(_benign_record()).lower()
|
|
for marker in FORBIDDEN_REPR_SUBSTRINGS:
|
|
assert marker not in text, f"the record's repr carries {marker!r}"
|
|
|
|
|
|
def test_the_store_repr_carrys_no_pii_markers() -> None:
|
|
text = repr(_benign_store()).lower()
|
|
for marker in FORBIDDEN_REPR_SUBSTRINGS:
|
|
assert marker not in text, f"the store's repr carries {marker!r}"
|
|
|
|
|
|
def test_the_record_and_the_best_run_are_slots_dataclasses() -> None:
|
|
"""Slots (not a `__dict__` bag) is the shape that keeps the field
|
|
set auditable: a new field is a named, visible change."""
|
|
assert hasattr(SessionRecord, "__slots__")
|
|
assert hasattr(BestRun, "__slots__")
|
|
|
|
|
|
def test_neither_record_nor_store_names_a_header_attribute() -> None:
|
|
record_fields = {field.name for field in dataclasses.fields(SessionRecord)}
|
|
assert not any("header" in name.lower() for name in record_fields)
|
|
store = _benign_store()
|
|
assert not any("header" in name.lower() for name in dir(store))
|
|
assert not any("header" in name.lower() for name in vars(store))
|
|
|
|
|
|
def test_the_store_holds_no_request_header_mapping() -> None:
|
|
"""Every dict-valued attribute of the store is either `token ->
|
|
record` or `level id -> order`: neither may hold a header-like key."""
|
|
store = _benign_store()
|
|
for name, value in vars(store).items():
|
|
if isinstance(value, dict):
|
|
assert not (
|
|
set(value) & HEADER_LIKE_KEYS
|
|
), f"store attribute {name!r} holds header-like keys"
|
|
record = _benign_record()
|
|
for field in dataclasses.fields(record):
|
|
assert field.name.lower() not in HEADER_LIKE_KEYS, f"record field {field.name!r}"
|
|
value = getattr(record, field.name)
|
|
if isinstance(value, dict):
|
|
assert not (
|
|
set(value) & HEADER_LIKE_KEYS
|
|
), f"record field {field.name!r} holds header-like keys"
|
|
|
|
|
|
def test_the_store_never_minted_with_headers_keeps_none() -> None:
|
|
"""A store built the way the supervisor builds it (a clock and the
|
|
campaign mapping only) exposes exactly its own five attributes — no
|
|
header bag can hide in a constructor that does not take one."""
|
|
store = SessionStore(
|
|
clock=FakeClock(), campaign={"01_kitchen": 1}
|
|
)
|
|
assert set(vars(store)) == {
|
|
"_max_entries",
|
|
"_clock",
|
|
"_campaign",
|
|
"_records",
|
|
"_evictions",
|
|
}
|