Drift audit 2026-06-02 — 26 findings shipped #75
@@ -69,15 +69,32 @@ func TestAdminLibraryCoverage_MixedRowsReturnCorrectBuckets(t *testing.T) {
|
||||
sourceNone := "none"
|
||||
sourceSidecar := "sidecar"
|
||||
sourceMbcaa := "mbcaa"
|
||||
sourceEmbedded := "embedded"
|
||||
sourceTheaudiodb := "theaudiodb"
|
||||
sourceDeezer := "deezer"
|
||||
sourceLastfm := "lastfm"
|
||||
mbid1 := "11111111-1111-1111-1111-111111111111"
|
||||
mbid2 := "22222222-2222-2222-2222-222222222222"
|
||||
mbid3 := "33333333-3333-3333-3333-333333333333"
|
||||
mbid4 := "44444444-4444-4444-4444-444444444444"
|
||||
mbid5 := "55555555-5555-5555-5555-555555555555"
|
||||
mbid6 := "66666666-6666-6666-6666-666666666666"
|
||||
mbid7 := "77777777-7777-7777-7777-777777777777"
|
||||
// Cover every cover_art_source value the migrations allow so a future
|
||||
// addition without updating the rollup query trips this test —
|
||||
// regression guard for drift #557, the gap that hid #556 (deezer +
|
||||
// lastfm omission introduced by migration 0020 but never wired into
|
||||
// the rollup whitelist).
|
||||
rows := []seed{
|
||||
{title: "WithArtSidecar", source: &sourceSidecar, mbid: &mbid1}, // with_art
|
||||
{title: "WithArtMbcaa", source: &sourceMbcaa, mbid: &mbid2}, // with_art
|
||||
{title: "PendingHasMbid", source: nil, mbid: &mbid3}, // pending (eligible)
|
||||
{title: "PendingNoMbid", source: nil, mbid: nil}, // pending + pending_no_mbid
|
||||
{title: "Settled", source: &sourceNone, mbid: nil}, // settled (no mbid is fine here)
|
||||
{title: "WithArtSidecar", source: &sourceSidecar, mbid: &mbid1}, // with_art
|
||||
{title: "WithArtMbcaa", source: &sourceMbcaa, mbid: &mbid2}, // with_art
|
||||
{title: "WithArtEmbedded", source: &sourceEmbedded, mbid: &mbid4}, // with_art
|
||||
{title: "WithArtTheaudiodb", source: &sourceTheaudiodb, mbid: &mbid5}, // with_art
|
||||
{title: "WithArtDeezer", source: &sourceDeezer, mbid: &mbid6}, // with_art
|
||||
{title: "WithArtLastfm", source: &sourceLastfm, mbid: &mbid7}, // with_art
|
||||
{title: "PendingHasMbid", source: nil, mbid: &mbid3}, // pending (eligible)
|
||||
{title: "PendingNoMbid", source: nil, mbid: nil}, // pending + pending_no_mbid
|
||||
{title: "Settled", source: &sourceNone, mbid: nil}, // settled (no mbid is fine here)
|
||||
}
|
||||
for _, s := range rows {
|
||||
if _, err := pool.Exec(context.Background(), `
|
||||
@@ -100,11 +117,13 @@ func TestAdminLibraryCoverage_MixedRowsReturnCorrectBuckets(t *testing.T) {
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Total != 5 {
|
||||
t.Errorf("Total = %d, want 5", resp.Total)
|
||||
if resp.Total != 9 {
|
||||
t.Errorf("Total = %d, want 9", resp.Total)
|
||||
}
|
||||
if resp.WithArt != 2 {
|
||||
t.Errorf("WithArt = %d, want 2", resp.WithArt)
|
||||
// Six with_art rows — one per valid cover_art_source value
|
||||
// (sidecar, mbcaa, embedded, theaudiodb, deezer, lastfm).
|
||||
if resp.WithArt != 6 {
|
||||
t.Errorf("WithArt = %d, want 6", resp.WithArt)
|
||||
}
|
||||
if resp.Pending != 2 {
|
||||
t.Errorf("Pending = %d, want 2", resp.Pending)
|
||||
|
||||
@@ -126,7 +126,8 @@ func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, erro
|
||||
const getAlbumCoverageRollup = `-- name: GetAlbumCoverageRollup :one
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IN ('sidecar','embedded','mbcaa','theaudiodb')) AS with_art,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IN
|
||||
('sidecar','embedded','mbcaa','theaudiodb','deezer','lastfm')) AS with_art,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IS NULL) AS pending,
|
||||
COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IS NULL AND mbid IS NULL) AS pending_no_mbid
|
||||
@@ -151,10 +152,12 @@ type GetAlbumCoverageRollupRow struct {
|
||||
// Invariant: with_art + pending + settled = total. (pending_no_mbid is
|
||||
// not part of the sum — it's a subset of pending, surfaced separately.)
|
||||
//
|
||||
// The IN list below ('sidecar','embedded','mbcaa','theaudiodb') must stay in sync
|
||||
// with the cover_art_source CHECK constraint in migration
|
||||
// 0016_album_cover_source.up.sql. If a new source value is added there
|
||||
// without updating this query, with_art will silently undercount.
|
||||
// The IN list below must stay in sync with the cover_art_source CHECK
|
||||
// constraint — currently relaxed by migration 0020 to include 'deezer'
|
||||
// and 'lastfm', and migration 0030 further relaxed it to any non-empty
|
||||
// string. If a new source value is added without updating this query,
|
||||
// with_art will silently undercount. Drift #556 caught the deezer +
|
||||
// lastfm omission introduced by migration 0020.
|
||||
func (q *Queries) GetAlbumCoverageRollup(ctx context.Context) (GetAlbumCoverageRollupRow, error) {
|
||||
row := q.db.QueryRow(ctx, getAlbumCoverageRollup)
|
||||
var i GetAlbumCoverageRollupRow
|
||||
|
||||
@@ -156,13 +156,16 @@ SELECT a.id AS album_id,
|
||||
-- Invariant: with_art + pending + settled = total. (pending_no_mbid is
|
||||
-- not part of the sum — it's a subset of pending, surfaced separately.)
|
||||
--
|
||||
-- The IN list below ('sidecar','embedded','mbcaa','theaudiodb') must stay in sync
|
||||
-- with the cover_art_source CHECK constraint in migration
|
||||
-- 0016_album_cover_source.up.sql. If a new source value is added there
|
||||
-- without updating this query, with_art will silently undercount.
|
||||
-- The IN list below must stay in sync with the cover_art_source CHECK
|
||||
-- constraint — currently relaxed by migration 0020 to include 'deezer'
|
||||
-- and 'lastfm', and migration 0030 further relaxed it to any non-empty
|
||||
-- string. If a new source value is added without updating this query,
|
||||
-- with_art will silently undercount. Drift #556 caught the deezer +
|
||||
-- lastfm omission introduced by migration 0020.
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IN ('sidecar','embedded','mbcaa','theaudiodb')) AS with_art,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IN
|
||||
('sidecar','embedded','mbcaa','theaudiodb','deezer','lastfm')) AS with_art,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IS NULL) AS pending,
|
||||
COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled,
|
||||
COUNT(*) FILTER (WHERE cover_art_source IS NULL AND mbid IS NULL) AS pending_no_mbid
|
||||
|
||||
@@ -23,13 +23,20 @@ import (
|
||||
syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync"
|
||||
)
|
||||
|
||||
// audioExtensions is the v1 set. Duration extraction is not wired, so all of
|
||||
// these get duration_ms=0 until an ffprobe / native-decoder pass lands.
|
||||
// audioExtensions is the set the scanner indexes. Keep in sync with
|
||||
// `internal/api/media.go` MIME detection — the stream handler must be
|
||||
// able to serve every extension the scanner indexes, and there is no
|
||||
// point in adding extensions to the stream handler that the scanner
|
||||
// will silently skip. Drift #571 caught the divergence after .opus,
|
||||
// .aac, and .wav were added to media.go but not here.
|
||||
var audioExtensions = map[string]bool{
|
||||
".mp3": true,
|
||||
".m4a": true,
|
||||
".flac": true,
|
||||
".ogg": true,
|
||||
".opus": true,
|
||||
".aac": true,
|
||||
".wav": true,
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
|
||||
@@ -106,7 +106,13 @@ export type LikedIdsResponse = {
|
||||
};
|
||||
|
||||
export type EventRequest =
|
||||
| { type: 'play_started'; track_id: string; client_id?: string }
|
||||
// `source` is the system-playlist variant the play came from
|
||||
// (for_you, discover, deep_cuts, …); empty/undefined for library
|
||||
// / user-playlist / radio / Subsonic. Server stores it on
|
||||
// play_events.source and uses it to advance the rotation. Drift
|
||||
// #555: the type was missing `source` so callers had no typed slot
|
||||
// for it and a "clean up extra properties" refactor could drop it.
|
||||
| { type: 'play_started'; track_id: string; client_id?: string; source?: string }
|
||||
| { type: 'play_ended'; play_event_id: string; duration_played_ms: number }
|
||||
| { type: 'play_skipped'; play_event_id: string; position_ms: number };
|
||||
|
||||
|
||||
@@ -30,4 +30,22 @@ describe('isPublicRoute', () => {
|
||||
expect(isPublicRoute('/login/extra')).toBe(false);
|
||||
expect(isPublicRoute('/loginx')).toBe(false);
|
||||
});
|
||||
|
||||
// Regression guard for drift #558: /forgot-password and the
|
||||
// /reset-password/<token> deep links must be reachable without a
|
||||
// session — the reset flow is entered by clicking an email link
|
||||
// while signed out, so the auth gate redirecting to /login broke
|
||||
// account recovery.
|
||||
test('/forgot-password is public', () => {
|
||||
expect(isPublicRoute('/forgot-password')).toBe(true);
|
||||
});
|
||||
|
||||
test('/reset-password/<token> is public', () => {
|
||||
expect(isPublicRoute('/reset-password/abc-123')).toBe(true);
|
||||
expect(isPublicRoute('/reset-password/')).toBe(true);
|
||||
});
|
||||
|
||||
test('/reset-password (no trailing slash) is NOT public — only the token sub-path', () => {
|
||||
expect(isPublicRoute('/reset-password')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
// when the visitor is unauthenticated. Bootstrap-admin self-registration
|
||||
// (#376) requires /register to be reachable without a session — without
|
||||
// /register here, the login page's "Register" link bounces back to /login.
|
||||
const PUBLIC_ROUTES = new Set(['/login', '/register']);
|
||||
//
|
||||
// Drift #558: /forgot-password and /reset-password/<token> were missing
|
||||
// from this set. The email-link reset flow is by definition entered
|
||||
// without a session — a signed-out user clicking their reset link was
|
||||
// being bounced to /login, breaking account recovery.
|
||||
const PUBLIC_ROUTES = new Set(['/login', '/register', '/forgot-password']);
|
||||
const PUBLIC_PREFIXES = ['/reset-password/'];
|
||||
|
||||
export function isPublicRoute(pathname: string): boolean {
|
||||
return PUBLIC_ROUTES.has(pathname);
|
||||
if (PUBLIC_ROUTES.has(pathname)) return true;
|
||||
return PUBLIC_PREFIXES.some((p) => pathname.startsWith(p));
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={confirmDeleteFile}
|
||||
class="rounded bg-action-danger px-3 py-2 text-sm text-action-fg hover:opacity-90"
|
||||
class="rounded bg-action-destructive px-3 py-2 text-sm text-action-fg hover:opacity-90"
|
||||
>Delete file</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user