package scanner import ( "context" "testing" "naviwatcher/internal/database" ) // newTestDB creates an in-memory SQLite database with all migrations applied. func newTestDB(t *testing.T) *database.DB { t.Helper() db, err := database.New(":memory:") if err != nil { t.Fatalf("database.New() error: %v", err) } return db } // seedArtist inserts a minimal artist_settings row so FK constraints pass. func seedArtist(t *testing.T, db *database.DB, id, name string) { t.Helper() if err := database.SaveArtistSettings(db, &database.ArtistSettings{ ID: id, Name: name, Monitored: true, }); err != nil { t.Fatalf("seedArtist(%s) error: %v", id, err) } } // seedLocalAlbum inserts a local_albums row for an artist. func seedLocalAlbum(t *testing.T, db *database.DB, id, artistID, title string) { t.Helper() if err := database.SaveLocalAlbum(db, &database.LocalAlbum{ ID: id, ArtistID: artistID, Title: title, }); err != nil { t.Fatalf("seedLocalAlbum(%s) error: %v", id, err) } } // seedExternalRelease inserts an external_releases row for an artist. func seedExternalRelease(t *testing.T, db *database.DB, rgid, artistID, title string, ignored bool) { t.Helper() if err := database.SaveExternalRelease(db, &database.ExternalRelease{ RGID: rgid, ArtistID: artistID, Title: title, IsIgnored: ignored, }); err != nil { t.Fatalf("seedExternalRelease(%s) error: %v", rgid, err) } } func TestScanArtist(t *testing.T) { db := newTestDB(t) defer db.Close() seedArtist(t, db, "artist-1", "Pink Floyd") // Local collection has "The Wall" but not "Animals". seedLocalAlbum(t, db, "l1", "artist-1", "The Wall") seedExternalRelease(t, db, "rg1", "artist-1", "The Wall", false) seedExternalRelease(t, db, "rg2", "artist-1", "Animals", false) missing, err := ScanArtist(context.Background(), db, "artist-1", 0) if err != nil { t.Fatalf("ScanArtist() error: %v", err) } rgids := map[string]bool{} for _, m := range missing { rgids[m.RGID] = true } if !rgids["rg2"] { t.Errorf("expected rg2 (Animals) to be missing, got %v", rgids) } if rgids["rg1"] { t.Errorf("did not expect rg1 (The Wall) to be missing, got %v", rgids) } } func TestScanArtist_IgnoredNotReported(t *testing.T) { db := newTestDB(t) defer db.Close() seedArtist(t, db, "artist-1", "Pink Floyd") seedExternalRelease(t, db, "rg1", "artist-1", "Animals", true) missing, err := ScanArtist(context.Background(), db, "artist-1", 0.85) if err != nil { t.Fatalf("ScanArtist() error: %v", err) } if len(missing) != 0 { t.Errorf("ignored release should not be reported, got %v", missing) } } func TestScanArtist_RemasteredVariantNotMissing(t *testing.T) { db := newTestDB(t) defer db.Close() seedArtist(t, db, "artist-1", "Pink Floyd") seedLocalAlbum(t, db, "l1", "artist-1", "The Wall (Remastered)") seedExternalRelease(t, db, "rg1", "artist-1", "The Wall", false) missing, err := ScanArtist(context.Background(), db, "artist-1", 0) if err != nil { t.Fatalf("ScanArtist() error: %v", err) } if len(missing) != 0 { t.Errorf("remastered local should match external, got %v", missing) } } func TestScanArtist_CtxCancelled(t *testing.T) { db := newTestDB(t) defer db.Close() seedArtist(t, db, "artist-1", "Pink Floyd") ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := ScanArtist(ctx, db, "artist-1", 0.85); err == nil { t.Fatal("expected error from cancelled context, got nil") } } func TestScanAll(t *testing.T) { db := newTestDB(t) defer db.Close() // Monitored artist with one missing release. seedArtist(t, db, "artist-1", "Pink Floyd") seedLocalAlbum(t, db, "l1", "artist-1", "The Wall") seedExternalRelease(t, db, "rg1", "artist-1", "The Wall", false) seedExternalRelease(t, db, "rg2", "artist-1", "Animals", false) // Unmonitored artist — must be skipped entirely. seedArtistUnmonitored(t, db, "artist-2", "Other") seedExternalRelease(t, db, "rg3", "artist-2", "Some Album", false) missing, err := ScanAll(context.Background(), db, 0) if err != nil { t.Fatalf("ScanAll() error: %v", err) } rgids := map[string]bool{} for _, m := range missing { rgids[m.RGID] = true } if !rgids["rg2"] { t.Errorf("expected rg2 (Animals) missing, got %v", rgids) } if rgids["rg1"] { t.Errorf("did not expect rg1 (The Wall) missing, got %v", rgids) } if rgids["rg3"] { t.Errorf("unmonitored artist's release must not be scanned, got %v", rgids) } } func TestScanAll_CtxCancelledMidIteration(t *testing.T) { db := newTestDB(t) defer db.Close() seedArtist(t, db, "artist-1", "Pink Floyd") seedArtist(t, db, "artist-2", "Other") // Cancel before scanning starts. ctx, cancel := context.WithCancel(context.Background()) cancel() missing, err := ScanAll(ctx, db, 0.85) if err == nil { t.Fatal("expected error from cancelled context, got nil") } if missing != nil { t.Errorf("expected nil results on early cancellation, got %v", missing) } } // seedArtistUnmonitored inserts an artist_settings row with Monitored=false. func seedArtistUnmonitored(t *testing.T, db *database.DB, id, name string) { t.Helper() if err := database.SaveArtistSettings(db, &database.ArtistSettings{ ID: id, Name: name, Monitored: false, }); err != nil { t.Fatalf("seedArtistUnmonitored(%s) error: %v", id, err) } }