140 lines
4.5 KiB
Python
140 lines
4.5 KiB
Python
"""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
|