E32 review round 1 (B5, B6).
- The autotag orphan cleanup reads every user's feed views. A view whose tag pick was not a list
("tags": 5) raised for everyone's job, True passed as tag id 1, and ~66k ids pushed the NOT IN
past psycopg's 65535 bind parameters. The views are now read defensively and the kept ids
filtered in Python; the saved-views route holds filters.tags to a list of at most 500 ids, on
create and on update.
- tags_by_category resolved any tag id, so another user's private tag landed in its real category
and the feed summary answered differently than for an id that does not exist, which told the
caller which foreign ids exist. It is owner-scoped now (pre-existing; S2's missing-tag branch
made the difference visible).
85 lines
3.8 KiB
Python
85 lines
3.8 KiB
Python
"""ask 4: saved views are per-module (`source`). Feed and Plex views are separate lists, may share a
|
|
name, and each has its own single default. DB lane (`db` fixture) — skipped without Postgres.
|
|
"""
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.state as state
|
|
from app.auth import current_user
|
|
from app.db import get_db
|
|
from app.main import app
|
|
from app.models import User
|
|
|
|
|
|
def _client(db, user, monkeypatch, override_dep):
|
|
monkeypatch.setattr(
|
|
state, "_configured_cache", True
|
|
) # the request gate 503s an unconfigured app
|
|
override_dep(get_db, db.info["request"])
|
|
override_dep(current_user, lambda: user) # require_human resolves through this
|
|
return TestClient(app)
|
|
|
|
|
|
def _user(db):
|
|
u = User(email="views@example.com")
|
|
db.add(u)
|
|
db.commit()
|
|
return u
|
|
|
|
|
|
def test_views_are_scoped_by_source(db, monkeypatch, override_dep):
|
|
user = _user(db)
|
|
c = _client(db, user, monkeypatch, override_dep)
|
|
# Same NAME under two sources is allowed (unique is per user+source now).
|
|
f = c.post("/api/saved-views", json={"name": "Fresh", "filters": {"a": 1}, "source": "feed"})
|
|
p = c.post("/api/saved-views", json={"name": "Fresh", "filters": {"b": 2}, "source": "plex"})
|
|
assert f.status_code == 200 and p.status_code == 200, (f.text, p.text)
|
|
|
|
feed = c.get("/api/saved-views?source=feed").json()
|
|
plex = c.get("/api/saved-views?source=plex").json()
|
|
assert [v["name"] for v in feed] == ["Fresh"]
|
|
assert [v["name"] for v in plex] == ["Fresh"]
|
|
assert feed[0]["filters"] == {"a": 1} and plex[0]["filters"] == {"b": 2}
|
|
# A source-less GET keeps the old feed-only behaviour.
|
|
assert [v["id"] for v in c.get("/api/saved-views").json()] == [v["id"] for v in feed]
|
|
|
|
|
|
def test_default_is_per_source(db, monkeypatch, override_dep):
|
|
user = _user(db)
|
|
c = _client(db, user, monkeypatch, override_dep)
|
|
fid = c.post("/api/saved-views", json={"name": "F", "filters": {}, "source": "feed"}).json()[
|
|
"id"
|
|
]
|
|
pid = c.post("/api/saved-views", json={"name": "P", "filters": {}, "source": "plex"}).json()[
|
|
"id"
|
|
]
|
|
assert c.patch(f"/api/saved-views/{fid}", json={"is_default": True}).status_code == 200
|
|
assert c.patch(f"/api/saved-views/{pid}", json={"is_default": True}).status_code == 200
|
|
# Making the Plex view default must NOT clear the feed view's default (different source).
|
|
assert c.get("/api/saved-views?source=feed").json()[0]["is_default"] is True
|
|
assert c.get("/api/saved-views?source=plex").json()[0]["is_default"] is True
|
|
|
|
|
|
def test_duplicate_name_within_a_source_conflicts(db, monkeypatch, override_dep):
|
|
user = _user(db)
|
|
c = _client(db, user, monkeypatch, override_dep)
|
|
a = c.post("/api/saved-views", json={"name": "Dup", "filters": {}, "source": "plex"})
|
|
b = c.post("/api/saved-views", json={"name": "Dup", "filters": {}, "source": "plex"})
|
|
assert a.status_code == 200
|
|
assert b.status_code == 409 # same (user, source, name)
|
|
|
|
|
|
def test_a_feed_views_tag_pick_must_be_a_short_list_of_ids(db, monkeypatch, override_dep):
|
|
"""The autotag job reads every view's tag pick (E32 review): the route holds it to the shape the
|
|
feed sends, on create and on update."""
|
|
user = _user(db)
|
|
c = _client(db, user, monkeypatch, override_dep)
|
|
for bad in (5, "1", [1, "2"], [True], list(range(501))):
|
|
r = c.post("/api/saved-views", json={"name": "Bad", "filters": {"tags": bad}})
|
|
assert r.status_code == 422, (bad, r.text)
|
|
ok = c.post("/api/saved-views", json={"name": "Ok", "filters": {"tags": [1, 2]}})
|
|
assert ok.status_code == 200, ok.text
|
|
vid = ok.json()["id"]
|
|
assert c.patch(f"/api/saved-views/{vid}", json={"filters": {"tags": 5}}).status_code == 422
|
|
assert c.patch(f"/api/saved-views/{vid}", json={"filters": {"tags": []}}).status_code == 200
|