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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user