"""Unit: steering notes PUT endpoint (phase 27 — Global Tuning). ``PUT /api/steering/{note_id}`` is driven **via the router** with a stubbed session (FastAPI dependency override — no Postgres required): 200 with the updated note (trimmed, ``created_at`` preserved), 404 for an unknown id, 422 for empty/whitespace/over-2000 bodies, and the router-level 403 for anonymous callers (the real phase-16 ``require_admin`` gate — no auth surface added by the PUT route itself). """ from __future__ import annotations import uuid from datetime import UTC, datetime from fastapi.testclient import TestClient from app.config import get_settings from app.db import get_db from app.main import create_app from app.models import SteeringNote CREATED_AT = datetime(2026, 8, 24, 12, 0, 0, tzinfo=UTC) class _FakeSession: """Just enough of a SQLAlchemy session for the PUT route.""" def __init__(self, rows: dict[uuid.UUID, SteeringNote]) -> None: self.rows = rows self.commits = 0 def get(self, _model: object, pk: object) -> SteeringNote | None: if isinstance(pk, uuid.UUID): return self.rows.get(pk) return None def commit(self) -> None: self.commits += 1 def refresh(self, _row: object) -> None: pass def _note() -> SteeringNote: return SteeringNote(id=uuid.uuid4(), note="be concise", created_at=CREATED_AT) def _put_client(rows: dict[uuid.UUID, SteeringNote]) -> tuple[TestClient, _FakeSession]: """A fresh app whose ``get_db`` is a stub holding ``rows``.""" session = _FakeSession(rows) app = create_app() app.dependency_overrides[get_db] = lambda: session return TestClient(app), session def _sign_in(client: TestClient) -> None: """Real login route (no DB): sets the signed admin session cookie.""" r = client.post("/api/login", json={"password": get_settings().admin_password}) assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}" # ---------- 200: update in place ---------- def test_update_note_returns_200_with_new_text() -> None: note = _note() client, session = _put_client({note.id: note}) _sign_in(client) r = client.put(f"/api/steering/{note.id}", json={"note": " be MORE concise "}) assert r.status_code == 200 body = r.json() assert body["note"] == "be MORE concise" # trimmed before storage assert body["id"] == str(note.id) assert datetime.fromisoformat(body["created_at"]) == CREATED_AT # preserved, not redated assert note.note == "be MORE concise" # updated in place assert session.commits == 1 def test_update_accepts_full_length_note() -> None: note = _note() client, _session = _put_client({note.id: note}) _sign_in(client) r = client.put(f"/api/steering/{note.id}", json={"note": "x" * 2000}) assert r.status_code == 200 assert len(r.json()["note"]) == 2000 # ---------- 404 / 422 ---------- def test_update_unknown_id_returns_404() -> None: note = _note() client, _session = _put_client({note.id: note}) _sign_in(client) r = client.put(f"/api/steering/{uuid.uuid4()}", json={"note": "anything"}) assert r.status_code == 404 assert r.json() == {"detail": "steering note not found"} assert note.note == "be concise" # untouched def test_update_invalid_id_returns_422() -> None: note = _note() client, _session = _put_client({note.id: note}) _sign_in(client) assert client.put("/api/steering/not-a-uuid", json={"note": "x"}).status_code == 422 def test_update_rejects_empty_blank_and_overlong_bodies() -> None: note = _note() client, session = _put_client({note.id: note}) _sign_in(client) assert client.put(f"/api/steering/{note.id}", json={"note": ""}).status_code == 422 assert client.put(f"/api/steering/{note.id}", json={"note": " \t\n "}).status_code == 422 assert client.put(f"/api/steering/{note.id}", json={"note": "x" * 2001}).status_code == 422 assert note.note == "be concise" # rejected bodies never touch the row assert session.commits == 0 # ---------- 403: router-level require_admin (phase 16) ---------- def test_update_anonymous_returns_403_admin_only() -> None: note = _note() client, session = _put_client({note.id: note}) r = client.put(f"/api/steering/{note.id}", json={"note": "sneaky"}) assert r.status_code == 403 assert r.json() == {"detail": "admin only"} # the require_admin message assert note.note == "be concise" # anonymous callers never mutate assert session.commits == 0