Files
bookhoard/tests/ui/COVERAGE.md
T
John O'Keefe 49b0b6eeb9 test(podcasts): integration suite + RLS auditor + contract widening (task 1.10)
cmd/server/tests/podcast_test.go — six suites, the shared-dev-DB
doctrine (test*-named libraries, cascading cleanup, no binary
fixtures):

- TestPodcastRLSSurfaces: THE AUDITOR (invariant 13) — non-subscriber
  404 on feed detail + SSR page (existence leak), episodes absent from
  generic media reads, admin escape, subscriber visibility, then the
  hidden-library flip hiding an already-subscribed feed (the feed
  policy's library-visibility join). Seeding runs as the FIRST ADMIN —
  the seed identity has neither subscription nor users row, so podcast
  INSERT..RETURNING (SELECT policy under FORCE) 42501s for it;
  production pods run as exactly that admin (invariant 12). All direct
  row probes read through the admin scope for the same reason.
- TestPodcastAddFeedFlow: member add + subscribe + metadata-only
  upsert; duplicate-URL dedupe (no second row); allow_member_feeds
  gate (member 403 / admin 201 / subscribe-still-allowed).
- TestPodcastShrunkenFeedRetention: feed purges an episode from the
  XML, both rows persist (upsert-without-delete pin, D14).
- TestPodcastEditSegregation: generic edit 400 + write-to-file refusal
  for episodes; control book edit unaffected.
- TestPodcastDownloadWorker (ffmpeg-generated tone at runtime): file
  lands at <download-root>/<feed-slug>/NNNN-…, audio_metadata probed;
  re-download of a downloaded episode never re-fetches (idempotency
  pin, enclosure-hit counted); dead enclosure → failed + persisted
  reason + metadata intact. Fetch/SSRF tests swap the client via
  SetPodcastHTTPClientForTests (the production client refuses
  loopback httptest servers by design — the real client gets its own
  negative test in services).
- TestPodcastScannerExclusion: SetFolders on a podcasts library root
  yields zero scanner folders (invariant 4 pin) — MediaScanner
  .FolderCount() accessor added.

Unit: internal/services/podcast_rss_test.go — namespace-aware parsing
fixture (three XML namespaces, fallback chains, hosts/guests),
duration spellings, RFC-822 variants, explicit variants, slug/path
layout, enclosure-extension selection, stripHTML, and the SSRF matrix
(private/loopback/link-local/multicast/mapped-v6 blocked; a loopback
httptest server PROVES the production client refuses it).

suites green: full integration run 161s / 0 failures; vitest 12 files
163 tests.

Contract widening: TestContractPin_MediaListBothBranches' raw-row
allowlist gains the 15 podcast episode columns (additive; documented in
docs/contract_pins.md — inert defaults on scan rows). Bruno: bruno/
podcasts/ mirrors the §8 endpoints (12 requests) + Bookhoard env vars.
tests/ui/COVERAGE.md rows 18-23: podcasts pages/lifecycle/RLS/chain —
Playwright sweep pending.
2026-10-29 18:09:35 -04:00

2.7 KiB

UI sweep coverage (plan §7.5)

Gate: every line below executes for BOTH personas unless marked "admin-only". Status: ✅ swept+green · 🔲 pending (future sweep runs).

# Page / control admin restricted
1 Login: bad password rejected; good login lands authed; logout ✅ ✅
2 Dashboard: sections render (Recently Added / Not Started) ✅ ✅ (hidden lib absent)
3 Dashboard: hidden library visibility (Audiobooks) ✅ visible ✅ absent
4 Search: results scoped (/api/media-items/search) ✅ ✅ no leaks
5 Media list: no hidden items in payloads ✅ ✅
6 Library browse grid + pagination 🔲 🔲
7 Library create / edit / delete (admin) 🔲 n/a
8 Admin visibility toggle roundtrip (hide → restricted loses → unhide) 🔲 🔲
9 Media detail: edit metadata / archive / delete 🔲 n/a
10 Reader: open, page turn, TOC, bookmark, highlight, note, settings 🔲 🔲
11 Collections: CRUD, add/remove items, smart-rule editor 🔲 🔲
12 Series: list/detail 🔲 🔲
13 Stats screens + range filters 🔲 🔲
14 Admin: users CRUD, visibility matrix, settings toggles, processing issues 🔲 n/a
15 Upload: EPUB generated at runtime, upload → appears → delete 🔲 n/a
16 Auth edges: restricted → admin URL; anonymous → media URL 🔲 ✅ (detail/404)
17 OPDS / KOReader API-covered (invariant test) API-covered
18 Podcasts: page renders (empty state before any feed) 🔲 🔲
19 Podcasts: add-feed modal → feed page (member flow) 🔲 🔲
20 Podcasts: feed detail — episode list lifecycle (download/queue/failed+retry states inline), Download All, gap banner 🔲 🔲
21 Podcasts: cross-user invisibility — own feed only (RLS §20 surfaces) 🔲 🔲
22 Podcasts: order selector re-renders episode list; serial suggestion shown only for serial/audiobook feeds 🔲 🔲
23 Podcasts: audio reader "Ep N/M" + prev/next episode; chain-advance to next episode 🔲 🔲

Podcast-specific sweep notes (plan task 1.10)

  • The download lifecycle needs a fetchable enclosure: sweep with a local feed server fixture (the integration tests use swapFeedServer; the Playwright run needs the same trick) — otherwise episodes stay pending and rows 20's queue/failed states can't render.
  • Row 21 rides TestPodcastRLSSurfaces (API level) until the sweep lands; the page-level check is the 404 render for a foreign feed id.
  • Row 23 chains by page navigation — assert the URL becomes the next episode's reader and progress carries over.