test-go / test (push) Successful in 1m9s
test-go / integration (push) Successful in 3m28s
release / Build signed APK (releases and dev) (push) Successful in 4m38s
release / Build + push container image (push) Successful in 1m26s
release / Verify release artifacts (tag releases only) (push) Skipped
Two identities per track, because they answer different questions: - audio_stream_sha256: SHA-256 of the ENCODED audio packets (ffmpeg -map 0:a -c:a copy -f hash). Equal means identical audio whatever the tags say. Measured against the #3885 pair: the two WWW files hash identically here and differently as whole files. Packets rather than decoded samples, so an ffmpeg upgrade cannot silently change every stored hash, and nothing is decoded. - chromaprint: fpcalc -raw -signed. The same recording at another bitrate or codec, for the acoustic tier. fpcalc ships in the image (libchromaprint-tools); shelled out because CGO_ENABLED=0 rules out bindings. Stored in a track_fingerprints table rather than on tracks: eight queries read tracks with SELECT *, including album pages, search and the Subsonic surface, and a ~4 KB array there would be de-TOASTed on every one of them. The scan fingerprints only bytes it has not seen (a new path, or mtime past the row's). A tag-repair pass leaves fingerprints alone, and unchanged files with no fingerprint are the backfill's job (#3908). Folding that into the skip check would re-decode the whole library on the first scan after upgrade and push a sync change per track. A failure that says nothing about the file (timeout, cancelled scan, tool not installed) is never stored, and on changed bytes it removes the old row. A tool that rejects the file stores NULL at the current version, so the backfill does not retry it every boot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
131 lines
5.3 KiB
Go
131 lines
5.3 KiB
Go
// Package dbtest provides shared helpers for integration tests that need
|
|
// a clean Postgres state without disturbing the operator's admin user.
|
|
//
|
|
// Background: when integration tests run against the same Postgres
|
|
// instance an operator uses for local dev (a common dev-laptop setup),
|
|
// a TRUNCATE that includes the users table wipes the operator's admin
|
|
// login between every test run. To avoid that, tests should:
|
|
//
|
|
// 1. Call ResetDB instead of issuing TRUNCATE statements directly. It
|
|
// truncates every data table EXCEPT users, then deletes only those
|
|
// users whose username starts with TestUserPrefix.
|
|
// 2. Always create test user rows with a username that begins with
|
|
// TestUserPrefix; otherwise those rows survive across runs and
|
|
// leak into other tests.
|
|
//
|
|
// One test legitimately needs to wipe the entire users table:
|
|
// internal/auth/bootstrap_test.go, which exercises first-time admin
|
|
// bootstrap. That file does its own TRUNCATE and does not use this
|
|
// helper.
|
|
package dbtest
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// TestUserPrefix is the required username prefix for any user row
|
|
// created by an integration test. ResetDB removes every user matching
|
|
// this prefix and leaves all others intact.
|
|
const TestUserPrefix = "test-"
|
|
|
|
// dataTables is the union of every non-users table touched by any
|
|
// integration test in the repo. Truncating the union is harmless for
|
|
// callers that only care about a subset; truncating tables that don't
|
|
// exist would error, but every name here corresponds to a migration
|
|
// that has shipped.
|
|
var dataTables = []string{
|
|
"artist_similarity",
|
|
"track_similarity",
|
|
"artist_similarity_unmatched", // M5c
|
|
"scrobble_queue",
|
|
"contextual_likes",
|
|
"general_likes_albums",
|
|
"general_likes_artists",
|
|
"general_likes",
|
|
"play_events",
|
|
"skip_events",
|
|
"play_sessions",
|
|
"sessions",
|
|
"lidarr_quarantine_actions",
|
|
"lidarr_quarantine",
|
|
// #2374. Keyed by (user_id, candidate_mbid) with no FK to artists —
|
|
// candidates are out-of-library — so the CASCADE from artists/users
|
|
// does NOT reach it for a leftover row whose user survived. Truncate
|
|
// explicitly or a stale snooze silently hides a candidate from the
|
|
// next test's suggestion assertions.
|
|
"suggestion_snoozes",
|
|
// #2376. Same reasoning: keyed by candidate MBID with no FK anywhere,
|
|
// so nothing cascades to them. A leftover tag row would make a
|
|
// candidate look enriched to the next test, and a leftover state row
|
|
// would make it look already-settled and thus ineligible.
|
|
"candidate_artist_tags",
|
|
"candidate_artist_tag_state",
|
|
"playlist_tracks",
|
|
"playlists",
|
|
"library_changes", // M7 #357 — must reset to keep cursor isolated per test
|
|
// SettingsService.reconcile() idempotently re-UpsertProviderSettings
|
|
// for every registered provider at boot, so truncating this is the
|
|
// correct per-test reset (clears test-modified enabled/api_key rows).
|
|
// cover_art_sources_meta is NOT truncated — boot only READS it
|
|
// (never recreates the singleton, seeded once by 0018); ResetDB
|
|
// resets its counter via UPDATE below instead.
|
|
"cover_art_provider_settings",
|
|
// Same reasoning for the tag-enrichment settings (#1490): truncate the
|
|
// per-provider rows (reconcile re-seeds registered ones), and reset the
|
|
// tag_sources_meta counter via UPDATE below rather than truncating it.
|
|
"tag_provider_settings",
|
|
// recsettings.New reconciles shipped defaults on every construction
|
|
// (#1250), so truncating gives each test pristine tuning values.
|
|
"recommendation_weight_profiles",
|
|
"taste_tuning",
|
|
// #2377. Same reasoning as taste_tuning above: recsettings.New re-seeds
|
|
// shipped defaults on every construction, so truncating gives each test
|
|
// pristine Discover knobs rather than whatever a previous test tuned.
|
|
"discover_tuning",
|
|
"recommendation_tuning_audit",
|
|
"track_fingerprints", // M400
|
|
"tracks",
|
|
"albums",
|
|
"artists",
|
|
}
|
|
|
|
// ResetDB clears all data tables and removes any user whose username
|
|
// begins with TestUserPrefix. Users without that prefix (notably the
|
|
// operator's admin row) are left alone. Calls t.Fatalf on error.
|
|
func ResetDB(t *testing.T, pool *pgxpool.Pool) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
stmt := "TRUNCATE " + strings.Join(dataTables, ", ") + " RESTART IDENTITY CASCADE"
|
|
if _, err := pool.Exec(ctx, stmt); err != nil {
|
|
t.Fatalf("dbtest.ResetDB truncate: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx,
|
|
"DELETE FROM users WHERE username LIKE $1",
|
|
TestUserPrefix+"%",
|
|
); err != nil {
|
|
t.Fatalf("dbtest.ResetDB delete test users: %v", err)
|
|
}
|
|
// Reset the monotonic cover-art source-version counter to its
|
|
// post-migration seeded value. Truncating the row would break
|
|
// SettingsService boot, which reads (never recreates) this
|
|
// singleton; an UPDATE keeps the row while clearing cross-test
|
|
// version accumulation (CurrentVersion=4 want 1, key-only-bump).
|
|
if _, err := pool.Exec(ctx,
|
|
"UPDATE cover_art_sources_meta SET current_version = 1",
|
|
); err != nil {
|
|
t.Fatalf("dbtest.ResetDB reset cover-art version: %v", err)
|
|
}
|
|
// Same for the tag-sources counter (#1490) — clears cross-test version
|
|
// accumulation that would spuriously report version_bumped on a
|
|
// key-only change.
|
|
if _, err := pool.Exec(ctx,
|
|
"UPDATE tag_sources_meta SET current_version = 1, last_registered_providers_hash = ''",
|
|
); err != nil {
|
|
t.Fatalf("dbtest.ResetDB reset tag-sources version: %v", err)
|
|
}
|
|
}
|