diff --git a/backend/app/routes/feed.py b/backend/app/routes/feed.py index 84531f3c..49303b72 100644 --- a/backend/app/routes/feed.py +++ b/backend/app/routes/feed.py @@ -104,7 +104,8 @@ def get_feed( try: vals, cursor_id = decode_cursor(keys, cursor) except ValueError: - raise HTTPException(status_code=400, detail="Invalid feed cursor") + # `from None`: the ValueError is an expected bad-input signal, not an error to chain. + raise HTTPException(status_code=400, detail="Invalid feed cursor") from None for k, v in zip(keys, vals): k["value"] = v query = query.where(keyset_predicate(keys, cursor_id)) diff --git a/backend/tests/test_feed_cursor.py b/backend/tests/test_feed_cursor.py new file mode 100644 index 00000000..0aab9339 --- /dev/null +++ b/backend/tests/test_feed_cursor.py @@ -0,0 +1,45 @@ +"""Feed cursor endpoint: a malformed ?cursor= must be a clean HTTP 400, not a 500. + +Locks the refactor that split cursor handling across modules — feedquery.decode_cursor raises a +plain ValueError and routes/feed.get_feed translates it to 400 — so a future regression (a dropped +try/except, or a new decode_cursor caller without its own catch) turns the 400 back into a 500 and +fails right here. 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 _get(db, user, monkeypatch, path): + 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.get(path) + finally: + app.dependency_overrides.clear() + + +def test_malformed_feed_cursor_returns_400(db, monkeypatch): + user = User(email="feedcursor@example.com") + db.add(user) + db.commit() + resp = _get(db, user, monkeypatch, "/api/feed?cursor=not-a-real-cursor") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Invalid feed cursor" + + +def test_feed_without_cursor_is_200(db, monkeypatch): + # Sanity: the same endpoint without a cursor is a normal 200 (proves the 400 above is the + # cursor path, not a globally-broken request). A fresh user has no subscriptions → empty feed. + user = User(email="feednocursor@example.com") + db.add(user) + db.commit() + resp = _get(db, user, monkeypatch, "/api/feed") + assert resp.status_code == 200 + assert resp.json()["items"] == []