Candidate rename hardening (Codex): pending-only + length cap
Two small server-side tweaks so the endpoint matches the UI policy: - Rename is refused (409) for promoted/rejected candidates — they're settled history; the UI already hides Rename for them, now the server enforces it too. - Name is capped at 160 chars before save, so an accidental pasted paragraph can't wreck the queue layout. Tests extended: 300-char name truncates to 160; renaming a promoted candidate → 409. 225 pytest + 11 vitest green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+8
-3
@@ -1205,10 +1205,15 @@ def create_app() -> FastAPI:
|
||||
def admin_candidate_rename(cid: int, body: CandidateRenameBody, request: Request) -> dict:
|
||||
with get_conn() as conn:
|
||||
_require_admin(conn, request)
|
||||
try:
|
||||
row = sources.rename_candidate(conn, cid, (body.name or "").strip() or None)
|
||||
except ValueError:
|
||||
cand = conn.execute("SELECT status FROM source_candidates WHERE id = ?", (cid,)).fetchone()
|
||||
if not cand:
|
||||
raise HTTPException(status_code=404, detail="candidate not found")
|
||||
# Match the UI policy server-side: a promoted/rejected candidate is
|
||||
# settled history — rename only while it's pending review.
|
||||
if cand["status"] in ("promoted", "rejected"):
|
||||
raise HTTPException(status_code=409, detail=f"Can't rename a {cand['status']} candidate.")
|
||||
name = (body.name or "").strip()[:160] or None # cap so a pasted paragraph can't wreck the queue
|
||||
row = sources.rename_candidate(conn, cid, name)
|
||||
return _candidate_dict(row)
|
||||
|
||||
@app.post("/api/admin/candidates/{cid}/promote")
|
||||
|
||||
@@ -179,9 +179,15 @@ def test_candidate_rename(tmp_path, monkeypatch):
|
||||
fixed = tc.post(f"/api/admin/candidates/{cid}/rename", json={"name": "Right Name"}).json()
|
||||
assert fixed["name"] == "Right Name" and fixed["preview"]["accepted"] == 2
|
||||
assert any(c["id"] == cid and c["name"] == "Right Name" for c in tc.get("/api/admin/candidates").json())
|
||||
# Length is capped so an accidental pasted paragraph can't wreck the queue.
|
||||
capped = tc.post(f"/api/admin/candidates/{cid}/rename", json={"name": "z" * 300}).json()
|
||||
assert len(capped["name"]) == 160
|
||||
tc.post(f"/api/admin/candidates/{cid}/rename", json={"name": "Right Name"}) # restore
|
||||
# The fixed name carries through to promotion.
|
||||
res = tc.post(f"/api/admin/candidates/{cid}/promote", json={}).json()
|
||||
assert res["source"]["name"] == "Right Name"
|
||||
# Settled candidates (promoted/rejected) can't be renamed server-side, not just hidden in the UI.
|
||||
assert tc.post(f"/api/admin/candidates/{cid}/rename", json={"name": "Nope"}).status_code == 409
|
||||
# Gated; unknown id → 404.
|
||||
assert TestClient(app).post(f"/api/admin/candidates/{cid}/rename", json={"name": "x"}).status_code == 401
|
||||
assert tc.post("/api/admin/candidates/99999/rename", json={"name": "x"}).status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user