Files
siftlode/backend/tests/test_saved_views_source.py
T
peter fa7a09a420 feat(saved-views): scope saved views by module source (backend)
Saved "smart views" were feed-only; the Plex module is getting them too. Add a
`source` discriminator to saved_views ("feed" | "plex"), default "feed" so every
existing row and any source-less client stays feed. Widen the per-user unique
name constraint to (user_id, source, name) so a feed view and a Plex view can
share a name, and scope create's position numbering + the is_default "clear the
others" to (user, source).

Migration 0073 (ADD source NOT NULL server_default 'feed' + swap the unique
constraint; reversible). Endpoints: GET/POST take `source` (default feed).
Tests: source isolation, per-source default, per-source name uniqueness.
2026-08-22 18:56:52 +02:00

76 lines
3.1 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):
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 # 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):
user = _user(db)
try:
c = _client(db, user, monkeypatch)
# 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]
finally:
app.dependency_overrides.clear()
def test_default_is_per_source(db, monkeypatch):
user = _user(db)
try:
c = _client(db, user, monkeypatch)
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"]
c.patch(f"/api/saved-views/{fid}", json={"is_default": True})
c.patch(f"/api/saved-views/{pid}", json={"is_default": True})
# 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
finally:
app.dependency_overrides.clear()
def test_duplicate_name_within_a_source_conflicts(db, monkeypatch):
user = _user(db)
try:
c = _client(db, user, monkeypatch)
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)
finally:
app.dependency_overrides.clear()