fix(api): a gone row and a refused playlist action are said in the reader's language, and a failure is reported once
Deep-review round 3 of bug hunt 2026-09.
- get_or_404 / owned_or_404 answer a structured {code: "not_found", reason: <what>}
(errors.notFound.* in HU+EN, generic fallback). The structured-404 fix of round 2 had gone into the
ad-hoc `_own_playlist` (CLAUDE.md's "Not" pattern) while the shared helpers stayed string-only, so
views, tags, pins, Plex playlists, invites and users still 404'd in English. `_own_playlist` now
goes through owned_or_404; admin's hand-rolled user lookups use get_or_404.
- A move into a list that is gone names the TARGET (reason "move_target") — "this playlist doesn't
exist any more" was read as the list on screen, which still exists.
- Every refusal in the playlists router is a {code: "playlist", reason} (playlists.errors.* HU+EN)
instead of a bare English string shown verbatim in the Hungarian UI.
- `req` / `uploadReq` mark the errors they already put on screen (the error dialog, the connection
notice, the 401 sign-out), and notifyActionError skips those. The watched toggles, the saved views
and a playlist delete toasted a fault the dialog was already showing; Playlists' two hand-written
status filters now go through the same check.
This commit is contained in:
@@ -139,11 +139,49 @@ def test_a_list_that_is_not_yours_answers_with_a_reason_the_ui_can_translate(
|
||||
|
||||
r = as_user(user).delete(f"/api/playlists/{theirs.id}/items/v1")
|
||||
assert r.status_code == 404
|
||||
assert r.json()["detail"] == {"code": "playlist", "reason": "not_found"}
|
||||
assert r.json()["detail"] == {"code": "not_found", "reason": "playlist"}
|
||||
db.expire_all()
|
||||
assert _ids(db, theirs.id) == ["v1"]
|
||||
|
||||
|
||||
def test_a_move_into_a_list_that_is_gone_names_the_target_not_the_list_on_screen(
|
||||
db, make_user, as_user
|
||||
):
|
||||
# Both lists 404'd with the same reason, which the UI reads as "THIS playlist doesn't exist any
|
||||
# more" — said about the source the reader is looking at, which is still there.
|
||||
user = make_user()
|
||||
_seed_videos(db, ["v1"])
|
||||
src = _playlist(db, user, "Mine", ["v1"])
|
||||
gone = _playlist(db, user, "Gone", [])
|
||||
gone_id = gone.id
|
||||
db.delete(gone)
|
||||
db.commit()
|
||||
|
||||
r = _move(as_user(user), src.id, "v1", gone_id)
|
||||
assert r.status_code == 404
|
||||
assert r.json()["detail"] == {"code": "not_found", "reason": "move_target"}
|
||||
db.expire_all()
|
||||
assert _ids(db, src.id) == ["v1"]
|
||||
|
||||
|
||||
def test_a_refused_playlist_action_answers_with_a_reason_the_ui_can_translate(
|
||||
db, make_user, as_user
|
||||
):
|
||||
# The rest of the router's refusals were bare English too ("The Watch later list can't be
|
||||
# deleted.", "YouTube delete failed.", ...), shown verbatim under the Hungarian UI.
|
||||
user = make_user()
|
||||
client = as_user(user)
|
||||
wl = client.post("/api/playlists/watch-later", json={"video_id": "nope"})
|
||||
assert wl.status_code == 404
|
||||
assert wl.json()["detail"] == {"code": "not_found", "reason": "video"}
|
||||
_seed_videos(db, ["v1"])
|
||||
saved = client.post("/api/playlists/watch-later", json={"video_id": "v1"})
|
||||
wl_id = saved.json()["playlist_id"]
|
||||
r = client.delete(f"/api/playlists/{wl_id}")
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"] == {"code": "playlist", "reason": "watch_later_undeletable"}
|
||||
|
||||
|
||||
def test_move_marks_both_linked_sides_dirty(db, make_user, as_user):
|
||||
"""A move edits two YouTube mirrors at once — the source lost an item, the target gained one.
|
||||
Marking only the source is the asymmetric fix this repo keeps re-learning."""
|
||||
|
||||
Reference in New Issue
Block a user