The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
71 lines
2.7 KiB
Python
71 lines
2.7 KiB
Python
"""The request models turn payloads that used to 500 into clean validation errors,
|
|
and PATCH models distinguish an absent field from an explicit null. Pure-logic (no DB) — it
|
|
exercises the Pydantic models the route modules define, not the endpoints."""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.routes.channels import AttachTagIn, ChannelUpdateIn
|
|
from app.routes.downloads import AdminSetQuotaIn
|
|
from app.routes.feed import VideoProgressIn
|
|
from app.routes.setup import SetupAdminIn
|
|
from app.routes.tags import TagUpdateIn
|
|
|
|
|
|
# --- The three ROADMAP-named 500-producers are now 422s (ValidationError at the boundary). ---
|
|
|
|
|
|
def test_channel_priority_rejects_non_numeric():
|
|
# Was `int(payload["priority"])` → 500 on garbage. Now a validation error.
|
|
with pytest.raises(ValidationError):
|
|
ChannelUpdateIn(priority="not-a-number")
|
|
|
|
|
|
def test_channel_priority_accepts_int_and_numeric_string():
|
|
assert ChannelUpdateIn(priority=5).priority == 5
|
|
assert ChannelUpdateIn(priority="5").priority == 5 # lax numeric-string coercion, as before
|
|
|
|
|
|
def test_channel_update_fields_all_optional():
|
|
m = ChannelUpdateIn()
|
|
assert m.priority is None and m.hidden is None and m.deep_requested is None
|
|
|
|
|
|
def test_attach_tag_rejects_non_int_id():
|
|
# Was a str tag_id into db.get(Tag, ...) — a DB cast 500. Now rejected up front.
|
|
with pytest.raises(ValidationError):
|
|
AttachTagIn(tag_id="abc")
|
|
|
|
|
|
def test_setup_admin_password_must_be_a_string():
|
|
# Was a non-string password reaching len()/argon2 → 500. `str` rejects it.
|
|
with pytest.raises(ValidationError):
|
|
SetupAdminIn(email="a@b.c", password=12345)
|
|
|
|
|
|
def test_admin_quota_rejects_non_numeric_limit():
|
|
# Was `int(payload["max_bytes"])` → 500. Now a validation error.
|
|
with pytest.raises(ValidationError):
|
|
AdminSetQuotaIn(max_bytes="lots")
|
|
|
|
|
|
def test_progress_accepts_fractional_seconds():
|
|
# The player may post a raw currentTime (a float). The field must accept it (truncated in the
|
|
# handler) — a plain `int` field would 422 on the progress hot path.
|
|
assert VideoProgressIn(position_seconds=12.7).position_seconds == 12.7
|
|
assert int(VideoProgressIn(position_seconds=12.7).position_seconds) == 12
|
|
with pytest.raises(ValidationError):
|
|
VideoProgressIn(position_seconds="not-a-number")
|
|
|
|
|
|
# --- PATCH models: model_fields_set separates "absent" from "explicit null". ---
|
|
|
|
|
|
def test_tag_update_absent_vs_null_color():
|
|
absent = TagUpdateIn()
|
|
assert "color" not in absent.model_fields_set # untouched on PATCH
|
|
|
|
cleared = TagUpdateIn(color=None)
|
|
assert "color" in cleared.model_fields_set # an explicit null CLEARS it
|
|
assert cleared.color is None
|