test(be): every dependency override goes through override_dep

22 test files set `app.dependency_overrides[...]` by hand and tore down with a blanket `.clear()`
in a `finally`, which wipes every override rather than the test's own and stops fixtures composing.
They now take the `override_dep` fixture (helpers get it passed in), whose teardown restores each
key's prior value; no clear() carried meaning between two phases of a test. A convention test fails
any test file that writes `dependency_overrides` directly.
This commit is contained in:
2026-09-19 17:26:07 +02:00
parent dc902d2c0e
commit 8759b5860d
23 changed files with 537 additions and 529 deletions
+79 -42
View File
@@ -96,27 +96,26 @@ def _seed(db):
return {"user": user, "movie_lib": movie_lib, "show_lib": show_lib, "show": show}
def _request(db, user, monkeypatch, method, path, json=None):
def _request(db, user, monkeypatch, override_dep, method, path, json=None):
monkeypatch.setattr(
state, "_configured_cache", True
) # the request gate 503s an unconfigured app
app.dependency_overrides[get_db] = lambda: db
app.dependency_overrides[current_user] = lambda: user
try:
client = TestClient(app) # no context manager: skip lifespan (scheduler/HLS sweep)
return client.request(method, path, json=json)
finally:
app.dependency_overrides.clear()
override_dep(get_db, lambda: db)
override_dep(current_user, lambda: user)
client = TestClient(app) # no context manager: skip lifespan (scheduler/HLS sweep)
return client.request(method, path, json=json)
def _get(db, user, monkeypatch, path):
return _request(db, user, monkeypatch, "GET", path)
def _get(db, user, monkeypatch, override_dep, path):
return _request(db, user, monkeypatch, override_dep, "GET", path)
class TestUnifiedLibrary:
def test_both_scope_unions_movies_and_shows_ordered_by_added(self, db, monkeypatch):
def test_both_scope_unions_movies_and_shows_ordered_by_added(
self, db, monkeypatch, override_dep
):
s = _seed(db)
r = _get(db, s["user"], monkeypatch, "/api/plex/library?scope=both")
r = _get(db, s["user"], monkeypatch, override_dep, "/api/plex/library?scope=both")
assert r.status_code == 200, r.text
body = r.json()
assert body["total"] == 3 # 2 movies + 1 show, unioned
@@ -127,43 +126,53 @@ class TestUnifiedLibrary:
assert show_item["season_count"] == 1
assert next(i for i in body["items"] if i["id"] == "m1")["status"] == "watched"
def test_scope_movie_only(self, db, monkeypatch):
def test_scope_movie_only(self, db, monkeypatch, override_dep):
s = _seed(db)
body = _get(db, s["user"], monkeypatch, "/api/plex/library?scope=movie").json()
body = _get(
db, s["user"], monkeypatch, override_dep, "/api/plex/library?scope=movie"
).json()
assert body["total"] == 2
assert all(i["type"] == "movie" for i in body["items"])
def test_scope_show_only(self, db, monkeypatch):
def test_scope_show_only(self, db, monkeypatch, override_dep):
s = _seed(db)
body = _get(db, s["user"], monkeypatch, "/api/plex/library?scope=show").json()
body = _get(db, s["user"], monkeypatch, override_dep, "/api/plex/library?scope=show").json()
assert [i["id"] for i in body["items"]] == ["s1"]
assert body["items"][0]["season_count"] == 1
def test_watch_state_branches(self, db, monkeypatch):
def test_watch_state_branches(self, db, monkeypatch, override_dep):
s = _seed(db)
# watched: only movie m1 (the show is in_progress, not fully watched)
watched = _get(db, s["user"], monkeypatch, "/api/plex/library?show=watched").json()
watched = _get(
db, s["user"], monkeypatch, override_dep, "/api/plex/library?show=watched"
).json()
assert [i["id"] for i in watched["items"]] == ["m1"]
# in_progress: only the show (no movie is in progress)
inprog = _get(db, s["user"], monkeypatch, "/api/plex/library?show=in_progress").json()
inprog = _get(
db, s["user"], monkeypatch, override_dep, "/api/plex/library?show=in_progress"
).json()
assert [i["id"] for i in inprog["items"]] == ["s1"]
# unwatched: only movie m2 (no state). The show is EXCLUDED — "unwatched" for a show means
# 0 watched AND 0 in-progress episodes, and e1 is in-progress. (Locks the actual branch.)
unwatched = _get(db, s["user"], monkeypatch, "/api/plex/library?show=unwatched").json()
unwatched = _get(
db, s["user"], monkeypatch, override_dep, "/api/plex/library?show=unwatched"
).json()
assert [i["id"] for i in unwatched["items"]] == ["m2"]
def test_search_matches_by_rank_and_groups_episodes(self, db, monkeypatch):
def test_search_matches_by_rank_and_groups_episodes(self, db, monkeypatch, override_dep):
s = _seed(db)
body = _get(db, s["user"], monkeypatch, "/api/plex/library?q=charlie").json()
body = _get(db, s["user"], monkeypatch, override_dep, "/api/plex/library?q=charlie").json()
# only the show title matches in the item grid; the movies don't contain "charlie"
assert [i["id"] for i in body["items"]] == ["s1"]
assert body["total"] == 1
# matching episodes come back grouped in the separate episodes list
assert sorted(e["title"] for e in body["episodes"]) == ["Charlie Finale", "Charlie Pilot"]
def test_pagination_caps_items_not_total(self, db, monkeypatch):
def test_pagination_caps_items_not_total(self, db, monkeypatch, override_dep):
s = _seed(db)
body = _get(db, s["user"], monkeypatch, "/api/plex/library?limit=2&offset=0").json()
body = _get(
db, s["user"], monkeypatch, override_dep, "/api/plex/library?limit=2&offset=0"
).json()
assert body["total"] == 3
assert [i["id"] for i in body["items"]] == [
"s1",
@@ -267,7 +276,7 @@ class TestFacetsWithData:
JSONB unnest + year/rating/duration bounds + the watch-state join). The existing smoke test only
covers the null-genre guard; this locks the aggregated numbers with real data."""
def test_facet_aggregation(self, db, monkeypatch):
def test_facet_aggregation(self, db, monkeypatch, override_dep):
movie_lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
show_lib = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True)
db.add_all([movie_lib, show_lib])
@@ -311,7 +320,7 @@ class TestFacetsWithData:
db.add(user)
db.commit()
body = _get(db, user, monkeypatch, "/api/plex/facets?scope=both").json()
body = _get(db, user, monkeypatch, override_dep, "/api/plex/facets?scope=both").json()
assert {g["value"]: g["count"] for g in body["genres"]} == {"Comedy": 2, "Drama": 2}
assert {c["value"]: c["count"] for c in body["content_ratings"]} == {
"PG": 1,
@@ -332,7 +341,7 @@ class TestPlaylistAndCollectionEndpoints:
def _movie(self, db, lib_id, rk, title):
return PlexItem(rating_key=rk, library_id=lib_id, kind="movie", title=title)
def test_playlist_crud_roundtrip(self, db, monkeypatch):
def test_playlist_crud_roundtrip(self, db, monkeypatch, override_dep):
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
db.add(lib)
db.flush()
@@ -348,6 +357,7 @@ class TestPlaylistAndCollectionEndpoints:
db,
user,
monkeypatch,
override_dep,
"POST",
"/api/plex/playlists",
{"title": "Faves", "item_rating_key": "m1"},
@@ -357,7 +367,9 @@ class TestPlaylistAndCollectionEndpoints:
assert created.json()["item_count"] == 1
# list -> the playlist with its count
listed = _get(db, user, monkeypatch, "/api/plex/playlists").json()["playlists"]
listed = _get(db, user, monkeypatch, override_dep, "/api/plex/playlists").json()[
"playlists"
]
assert [(p["id"], p["item_count"]) for p in listed] == [(pid, 1)]
# add m2, then reorder to [m2, m1]
@@ -365,6 +377,7 @@ class TestPlaylistAndCollectionEndpoints:
db,
user,
monkeypatch,
override_dep,
"POST",
f"/api/plex/playlists/{pid}/items",
{"item_rating_key": "m2"},
@@ -374,24 +387,30 @@ class TestPlaylistAndCollectionEndpoints:
db,
user,
monkeypatch,
override_dep,
"PUT",
f"/api/plex/playlists/{pid}/order",
{"item_rating_keys": ["m2", "m1"]},
)
detail = _get(db, user, monkeypatch, f"/api/plex/playlists/{pid}").json()
detail = _get(db, user, monkeypatch, override_dep, f"/api/plex/playlists/{pid}").json()
assert [it["id"] for it in detail["items"]] == ["m2", "m1"] # reorder applied, in order
# remove m1 -> one item left
rm = _request(db, user, monkeypatch, "DELETE", f"/api/plex/playlists/{pid}/items/m1")
rm = _request(
db, user, monkeypatch, override_dep, "DELETE", f"/api/plex/playlists/{pid}/items/m1"
)
assert rm.status_code == 200, rm.text
after = _get(db, user, monkeypatch, f"/api/plex/playlists/{pid}").json()
after = _get(db, user, monkeypatch, override_dep, f"/api/plex/playlists/{pid}").json()
assert [it["id"] for it in after["items"]] == ["m2"]
# delete the playlist -> gone from the list
_request(db, user, monkeypatch, "DELETE", f"/api/plex/playlists/{pid}")
assert _get(db, user, monkeypatch, "/api/plex/playlists").json()["playlists"] == []
_request(db, user, monkeypatch, override_dep, "DELETE", f"/api/plex/playlists/{pid}")
assert (
_get(db, user, monkeypatch, override_dep, "/api/plex/playlists").json()["playlists"]
== []
)
def test_playlist_bulk_add_and_remove(self, db, monkeypatch):
def test_playlist_bulk_add_and_remove(self, db, monkeypatch, override_dep):
# Covers the bulk endpoints (add-bulk / remove-bulk) and pins the remove-bulk delete's
# synchronize_session=False path, which the single add/remove roundtrip never exercises.
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
@@ -402,13 +421,16 @@ class TestPlaylistAndCollectionEndpoints:
db.add(user)
db.commit()
created = _request(db, user, monkeypatch, "POST", "/api/plex/playlists", {"title": "Bulk"})
created = _request(
db, user, monkeypatch, override_dep, "POST", "/api/plex/playlists", {"title": "Bulk"}
)
pid = created.json()["id"]
added = _request(
db,
user,
monkeypatch,
override_dep,
"POST",
f"/api/plex/playlists/{pid}/items/bulk",
{"item_rating_keys": ["m1", "m2", "m3"]},
@@ -420,6 +442,7 @@ class TestPlaylistAndCollectionEndpoints:
db,
user,
monkeypatch,
override_dep,
"POST",
f"/api/plex/playlists/{pid}/items/remove-bulk",
{"item_rating_keys": ["m2"]},
@@ -427,13 +450,13 @@ class TestPlaylistAndCollectionEndpoints:
assert removed.status_code == 200, removed.text
assert removed.json() == {"removed": 1, "item_count": 2}
detail = _get(db, user, monkeypatch, f"/api/plex/playlists/{pid}").json()
detail = _get(db, user, monkeypatch, override_dep, f"/api/plex/playlists/{pid}").json()
assert [it["id"] for it in detail["items"]] == [
"m1",
"m3",
] # input order preserved, m2 gone
def test_collections_list(self, db, monkeypatch):
def test_collections_list(self, db, monkeypatch, override_dep):
from app.models import PlexCollection
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
@@ -450,13 +473,13 @@ class TestPlaylistAndCollectionEndpoints:
db.commit()
# unified scope (no library param) -> most-populated first
body = _get(db, user, monkeypatch, "/api/plex/collections").json()
body = _get(db, user, monkeypatch, override_dep, "/api/plex/collections").json()
assert [c["title"] for c in body["collections"]] == ["Marvel", "Pixar"]
# name filter
filtered = _get(db, user, monkeypatch, "/api/plex/collections?q=pix").json()
filtered = _get(db, user, monkeypatch, override_dep, "/api/plex/collections?q=pix").json()
assert [c["title"] for c in filtered["collections"]] == ["Pixar"]
def test_item_state_watched_then_new(self, db, monkeypatch):
def test_item_state_watched_then_new(self, db, monkeypatch, override_dep):
# No PlexLink -> the background Plex push is a no-op; only the local PlexState path runs, which
# is what the conversion touched (the st lookup + the delete on ->new).
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
@@ -468,7 +491,13 @@ class TestPlaylistAndCollectionEndpoints:
db.commit()
watched = _request(
db, user, monkeypatch, "POST", "/api/plex/item/m1/state", {"status": "watched"}
db,
user,
monkeypatch,
override_dep,
"POST",
"/api/plex/item/m1/state",
{"status": "watched"},
)
assert watched.status_code == 200, watched.text
assert watched.json()["status"] == "watched"
@@ -476,7 +505,15 @@ class TestPlaylistAndCollectionEndpoints:
assert row.status == "watched"
# -> new deletes the state row
back = _request(db, user, monkeypatch, "POST", "/api/plex/item/m1/state", {"status": "new"})
back = _request(
db,
user,
monkeypatch,
override_dep,
"POST",
"/api/plex/item/m1/state",
{"status": "new"},
)
assert back.status_code == 200, back.text
assert (
db.execute(select(PlexState).where(PlexState.user_id == user.id)).scalars().first()