refactor(channels): shared tag-mutation hook + a channel_detail tag_ids test (R8 review)

Address the two remaining review findings on the tag work:
- extract useChannelTagActions — the optimistic attach/detach + create-and-attach flow that
  the channel manager and the channel detail page had copy-pasted; the cache-shape-specific
  optimistic update is injected (manager maps a ManagedChannel[]; detail patches one
  ChannelDetail), so the two stay in sync;
- add a DB-lane test asserting GET /api/channels/{id} returns exactly this user's personal
  tag links — not another user's, not the auto/system (user_id NULL) links.
This commit is contained in:
2026-07-30 23:33:50 +02:00
parent 3352d0095c
commit f091247624
4 changed files with 132 additions and 59 deletions
+54
View File
@@ -0,0 +1,54 @@
"""DB-backed regression (R8 Part 2): GET /api/channels/{id} returns exactly THIS user's personal
tag links for the channel — not another user's, and not the auto/system links (user_id NULL) —
so the channel detail page can reuse the manager's tag editor. Would ship broken if the tag_ids
query ever dropped its user_id filter.
Uses the DB lane (`db` fixture) — skipped when no Postgres is reachable.
"""
from datetime import datetime, timezone
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 Channel, ChannelTag, Tag, User
def test_channel_detail_returns_only_this_users_tag_ids(db, monkeypatch):
# published_at set so channel_detail skips the YouTube-enrich path (no quota / network in tests).
ch = Channel(
id="UC_tags_test",
title="Tag Test Channel",
published_at=datetime(2020, 1, 1, tzinfo=timezone.utc),
)
db.add(ch)
me = User(email="me@example.com")
other = User(email="other@example.com")
db.add_all([me, other])
db.flush()
mine = Tag(user_id=me.id, name="Mine", category="other")
theirs = Tag(user_id=other.id, name="Theirs", category="other")
auto = Tag(user_id=None, name="English", category="language") # a system/auto tag
db.add_all([mine, theirs, auto])
db.flush()
# My personal link, another user's link, and a system/auto link on the same channel — only MINE
# must come back for `me`.
db.add(ChannelTag(channel_id=ch.id, tag_id=mine.id, user_id=me.id, source="user"))
db.add(ChannelTag(channel_id=ch.id, tag_id=theirs.id, user_id=other.id, source="user"))
db.add(ChannelTag(channel_id=ch.id, tag_id=auto.id, user_id=None, source="auto"))
db.commit()
# The request gate 503s an unconfigured instance; flip the process-level flag (auto-restored).
monkeypatch.setattr(state, "_configured_cache", True)
app.dependency_overrides[get_db] = lambda: db
app.dependency_overrides[current_user] = lambda: me
try:
client = TestClient(app) # no context-manager: skip lifespan (scheduler/HLS sweep)
resp = client.get(f"/api/channels/{ch.id}")
finally:
app.dependency_overrides.clear()
assert resp.status_code == 200, resp.text
assert resp.json()["tag_ids"] == [mine.id]