package playlists_test import ( "context" "crypto/rand" "fmt" "io" "log/slog" "os" "path/filepath" "sync/atomic" "testing" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "git.fabledsword.com/bvandeusen/minstrel/internal/db" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/dbtest" ) // seedTrackCounter ensures every seedTrack call gets a unique file_path // even when title + artist repeat. Tracks dedupe by file_path so we // must vary it; using t.TempDir() alone isn't enough because some // tests seed multiple tracks within one test function. var seedTrackCounter uint64 // newPool migrates a fresh schema against MINSTREL_TEST_DATABASE_URL // and returns a connection pool with all data tables wiped (test // users only — see internal/dbtest/reset.go for the why). Skips when // the env var isn't set or -short is passed. func newPool(t *testing.T) *pgxpool.Pool { t.Helper() if testing.Short() { t.Skip("skipping integration test in -short mode") } dsn := os.Getenv("MINSTREL_TEST_DATABASE_URL") if dsn == "" { t.Skip("MINSTREL_TEST_DATABASE_URL not set") } if err := db.Migrate(dsn, slog.New(slog.NewTextHandler(io.Discard, nil))); err != nil { t.Fatalf("migrate: %v", err) } pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("pool: %v", err) } t.Cleanup(pool.Close) dbtest.ResetDB(t, pool) return pool } // seedUser inserts a `test-`-prefixed user (so dbtest.ResetDB can // clean it without touching the operator's admin row) and returns // the persisted user record. func seedUser(t *testing.T, pool *pgxpool.Pool, name string) dbq.User { t.Helper() u, err := dbq.New(pool).CreateUser(context.Background(), dbq.CreateUserParams{ Username: dbtest.TestUserPrefix + name, PasswordHash: "x", ApiToken: name + "-token", IsAdmin: false, }) if err != nil { t.Fatalf("seed user %s: %v", name, err) } return u } // seedTrack creates an artist + album + track triple suitable for // playlist track-list mutation tests. Each call produces a fresh // track row with a unique file_path; artist and album are not // deduplicated across calls (mbid-less upsert), but that's fine for // these tests — playlist snapshots just capture the strings. func seedTrack(t *testing.T, pool *pgxpool.Pool, title, artist string) dbq.Track { t.Helper() q := dbq.New(pool) a, err := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{ Name: artist, SortName: artist, }) if err != nil { t.Fatalf("seed artist: %v", err) } al, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{ Title: artist + " - Album", SortTitle: artist + " - Album", ArtistID: a.ID, }) if err != nil { t.Fatalf("seed album: %v", err) } n := atomic.AddUint64(&seedTrackCounter, 1) track, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{ Title: title, AlbumID: al.ID, ArtistID: a.ID, DurationMs: 1000, FilePath: filepath.Join(t.TempDir(), fmt.Sprintf("%s-%d.mp3", title, n)), FileSize: 100, FileFormat: "mp3", }) if err != nil { t.Fatalf("seed track: %v", err) } return track } // randomUUID returns a fresh valid pgtype.UUID. Used in tests that // need an id guaranteed-not-to-exist in the database. func randomUUID() pgtype.UUID { var u pgtype.UUID if _, err := rand.Read(u.Bytes[:]); err != nil { panic(err) } u.Valid = true return u } // uuidString renders a pgtype.UUID as the canonical 8-4-4-4-12 form // without depending on a third-party UUID package — used in tests // that build a filename from a playlist id. func uuidString(u pgtype.UUID) string { if !u.Valid { return "" } const hex = "0123456789abcdef" out := make([]byte, 36) j := 0 for i, x := range u.Bytes { if i == 4 || i == 6 || i == 8 || i == 10 { out[j] = '-' j++ } out[j] = hex[x>>4] out[j+1] = hex[x&0x0f] j += 2 } return string(out) }