Implements Task 8: artist detail page (local albums + found-missing with ignore buttons), ignored-releases archive with restore, and POST handlers toggling ignore flags and ignore_singles. Adds ErrArtistNotFound sentinel so callers can distinguish missing artists, and wires routes via the enhanced ServeMux path wildcard.
463 lines
12 KiB
Go
463 lines
12 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
)
|
|
|
|
// TestGetArtistSettings_Found verifies retrieving an existing artist.
|
|
func TestGetArtistSettings_Found(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Insert a row directly.
|
|
_, err = db.Conn().Exec(
|
|
"INSERT INTO artist_settings (id, name, ignore_singles, ignore_compilations, monitored) VALUES (?, ?, ?, ?, ?)",
|
|
"artist-1", "Test Artist", true, false, true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
|
|
s, err := GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
|
|
if s.ID != "artist-1" {
|
|
t.Errorf("expected ID 'artist-1', got %q", s.ID)
|
|
}
|
|
if s.Name != "Test Artist" {
|
|
t.Errorf("expected Name 'Test Artist', got %q", s.Name)
|
|
}
|
|
if !s.IgnoreSingles {
|
|
t.Error("expected IgnoreSingles true")
|
|
}
|
|
if s.IgnoreCompilations {
|
|
t.Error("expected IgnoreCompilations false")
|
|
}
|
|
if !s.Monitored {
|
|
t.Error("expected Monitored true")
|
|
}
|
|
}
|
|
|
|
// TestGetArtistSettings_NotFound verifies that a missing artist returns ErrArtistNotFound.
|
|
func TestGetArtistSettings_NotFound(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = GetArtistSettings(db, "nonexistent")
|
|
if err != ErrArtistNotFound {
|
|
t.Errorf("expected ErrArtistNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestSaveArtistSettings_Insert verifies inserting a new artist.
|
|
func TestSaveArtistSettings_Insert(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
s := &ArtistSettings{
|
|
ID: "artist-1",
|
|
Name: "New Artist",
|
|
IgnoreSingles: false,
|
|
IgnoreCompilations: true,
|
|
Monitored: true,
|
|
}
|
|
|
|
if err := SaveArtistSettings(db, s); err != nil {
|
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
// Verify it was inserted.
|
|
got, err := GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
if got.Name != "New Artist" {
|
|
t.Errorf("expected Name 'New Artist', got %q", got.Name)
|
|
}
|
|
if got.IgnoreCompilations != true {
|
|
t.Errorf("expected IgnoreCompilations true, got %v", got.IgnoreCompilations)
|
|
}
|
|
}
|
|
|
|
// TestSaveArtistSettings_Update verifies that SaveArtistSettings replaces an existing row.
|
|
func TestSaveArtistSettings_Update(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Insert initial row.
|
|
s1 := &ArtistSettings{
|
|
ID: "artist-1",
|
|
Name: "Original Name",
|
|
IgnoreSingles: false,
|
|
IgnoreCompilations: false,
|
|
Monitored: true,
|
|
}
|
|
if err := SaveArtistSettings(db, s1); err != nil {
|
|
t.Fatalf("first SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
// Update the row.
|
|
s2 := &ArtistSettings{
|
|
ID: "artist-1",
|
|
Name: "Updated Name",
|
|
IgnoreSingles: true,
|
|
IgnoreCompilations: true,
|
|
Monitored: false,
|
|
}
|
|
if err := SaveArtistSettings(db, s2); err != nil {
|
|
t.Fatalf("second SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
got, err := GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
if got.Name != "Updated Name" {
|
|
t.Errorf("expected Name 'Updated Name', got %q", got.Name)
|
|
}
|
|
if !got.IgnoreSingles {
|
|
t.Error("expected IgnoreSingles true")
|
|
}
|
|
if !got.IgnoreCompilations {
|
|
t.Error("expected IgnoreCompilations true")
|
|
}
|
|
if got.Monitored {
|
|
t.Error("expected Monitored false")
|
|
}
|
|
}
|
|
|
|
// TestGetAllArtistSettings_Empty verifies GetAllArtistSettings returns empty slice when no rows.
|
|
func TestGetAllArtistSettings_Empty(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
results, err := GetAllArtistSettings(db)
|
|
if err != nil {
|
|
t.Fatalf("GetAllArtistSettings() error: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
// TestGetAllArtistSettings_Populated verifies GetAllArtistSettings returns all rows.
|
|
func TestGetAllArtistSettings_Populated(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
artists := []ArtistSettings{
|
|
{ID: "a1", Name: "Artist One", Monitored: true},
|
|
{ID: "a2", Name: "Artist Two", IgnoreSingles: true},
|
|
{ID: "a3", Name: "Artist Three", IgnoreCompilations: true},
|
|
}
|
|
|
|
for _, a := range artists {
|
|
if err := SaveArtistSettings(db, &a); err != nil {
|
|
t.Fatalf("SaveArtistSettings(%s) error: %v", a.ID, err)
|
|
}
|
|
}
|
|
|
|
results, err := GetAllArtistSettings(db)
|
|
if err != nil {
|
|
t.Fatalf("GetAllArtistSettings() error: %v", err)
|
|
}
|
|
if len(results) != 3 {
|
|
t.Fatalf("expected 3 results, got %d", len(results))
|
|
}
|
|
|
|
// Verify all artists are present (order not guaranteed, use a map).
|
|
byID := make(map[string]ArtistSettings)
|
|
for _, r := range results {
|
|
byID[r.ID] = r
|
|
}
|
|
for _, expected := range artists {
|
|
got, ok := byID[expected.ID]
|
|
if !ok {
|
|
t.Errorf("expected artist %s in results", expected.ID)
|
|
continue
|
|
}
|
|
if got.Name != expected.Name {
|
|
t.Errorf("artist %s: expected Name %q, got %q", expected.ID, expected.Name, got.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUpdateArtistSettings_Success verifies partial updates work.
|
|
func TestUpdateArtistSettings_Success(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Insert initial row.
|
|
s := &ArtistSettings{
|
|
ID: "artist-1",
|
|
Name: "Original",
|
|
IgnoreSingles: false,
|
|
IgnoreCompilations: false,
|
|
Monitored: true,
|
|
}
|
|
if err := SaveArtistSettings(db, s); err != nil {
|
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
// Update only name and monitored.
|
|
updates := map[string]interface{}{
|
|
"name": "Updated",
|
|
"monitored": false,
|
|
}
|
|
if err := UpdateArtistSettings(db, "artist-1", updates); err != nil {
|
|
t.Fatalf("UpdateArtistSettings() error: %v", err)
|
|
}
|
|
|
|
got, err := GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
if got.Name != "Updated" {
|
|
t.Errorf("expected Name 'Updated', got %q", got.Name)
|
|
}
|
|
if got.Monitored {
|
|
t.Error("expected Monitored false")
|
|
}
|
|
// Unchanged fields should remain.
|
|
if got.IgnoreSingles != false {
|
|
t.Error("expected IgnoreSingles unchanged (false)")
|
|
}
|
|
if got.IgnoreCompilations != false {
|
|
t.Error("expected IgnoreCompilations unchanged (false)")
|
|
}
|
|
}
|
|
|
|
// TestUpdateArtistSettings_NotFound verifies updating a nonexistent artist returns error.
|
|
func TestUpdateArtistSettings_NotFound(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
updates := map[string]interface{}{"name": "Ghost"}
|
|
err = UpdateArtistSettings(db, "nonexistent", updates)
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent artist, got nil")
|
|
}
|
|
}
|
|
|
|
// TestUpdateArtistSettings_InvalidColumn verifies unknown columns are rejected.
|
|
func TestUpdateArtistSettings_InvalidColumn(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
s := &ArtistSettings{ID: "artist-1", Name: "Test"}
|
|
if err := SaveArtistSettings(db, s); err != nil {
|
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
updates := map[string]interface{}{"invalid_col": "value"}
|
|
err = UpdateArtistSettings(db, "artist-1", updates)
|
|
if err == nil {
|
|
t.Error("expected error for invalid column, got nil")
|
|
}
|
|
}
|
|
|
|
// TestUpdateArtistSettings_EmptyUpdates verifies empty updates map returns error.
|
|
func TestUpdateArtistSettings_EmptyUpdates(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
s := &ArtistSettings{ID: "artist-1", Name: "Test"}
|
|
if err := SaveArtistSettings(db, s); err != nil {
|
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
updates := map[string]interface{}{}
|
|
err = UpdateArtistSettings(db, "artist-1", updates)
|
|
if err == nil {
|
|
t.Error("expected error for empty updates, got nil")
|
|
}
|
|
}
|
|
|
|
// TestMigration008_MbidColumnExists verifies the 008 migration adds the mbid
|
|
// column and that rows created before resolution have a NULL/empty MBID.
|
|
func TestMigration008_MbidColumnExists(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Insert a row without supplying mbid (simulates a pre-resolution row).
|
|
if _, err := db.Conn().Exec(
|
|
"INSERT INTO artist_settings (id, name) VALUES (?, ?)",
|
|
"artist-1", "No MBID Yet",
|
|
); err != nil {
|
|
t.Fatalf("insert without mbid: %v", err)
|
|
}
|
|
|
|
var mbid sql.NullString
|
|
if err := db.Conn().QueryRow(
|
|
"SELECT mbid FROM artist_settings WHERE id = ?", "artist-1",
|
|
).Scan(&mbid); err != nil {
|
|
t.Fatalf("query mbid: %v", err)
|
|
}
|
|
if mbid.Valid && mbid.String != "" {
|
|
t.Errorf("expected empty mbid for pre-resolution row, got %q", mbid.String)
|
|
}
|
|
}
|
|
|
|
// TestArtistSettings_MbidRoundTrip verifies Save/Get round-trips an MBID,
|
|
// and that an empty MBID is preserved (not overwritten with garbage).
|
|
func TestArtistSettings_MbidRoundTrip(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
s := &ArtistSettings{
|
|
ID: "artist-1",
|
|
Name: "Test Artist",
|
|
MBID: "f27e6623-8771-4a2e-8dcb-6c8b1a4f8b9a",
|
|
Monitored: true,
|
|
}
|
|
if err := SaveArtistSettings(db, s); err != nil {
|
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
got, err := GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
if got.MBID != s.MBID {
|
|
t.Errorf("expected MBID %q, got %q", s.MBID, got.MBID)
|
|
}
|
|
}
|
|
|
|
// TestArtistSettings_MbidEmptyAllowed verifies an artist can be saved and
|
|
// retrieved with no MBID set (lazy resolution not yet performed).
|
|
func TestArtistSettings_MbidEmptyAllowed(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
s := &ArtistSettings{ID: "artist-1", Name: "No MBID", Monitored: true}
|
|
if err := SaveArtistSettings(db, s); err != nil {
|
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
got, err := GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
if got.MBID != "" {
|
|
t.Errorf("expected empty MBID, got %q", got.MBID)
|
|
}
|
|
}
|
|
|
|
// TestArtistSettings_MbidUpdatePersists verifies UpdateArtistSettings can set
|
|
// and clear the MBID column.
|
|
func TestArtistSettings_MbidUpdatePersists(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := SaveArtistSettings(db, &ArtistSettings{ID: "artist-1", Name: "Test", Monitored: true}); err != nil {
|
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
|
}
|
|
|
|
mbid := "f27e6623-8771-4a2e-8dcb-6c8b1a4f8b9a"
|
|
if err := UpdateArtistSettings(db, "artist-1", map[string]interface{}{"mbid": mbid}); err != nil {
|
|
t.Fatalf("UpdateArtistSettings(set mbid) error: %v", err)
|
|
}
|
|
got, err := GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
if got.MBID != mbid {
|
|
t.Errorf("expected MBID %q after set, got %q", mbid, got.MBID)
|
|
}
|
|
|
|
// Clear it again.
|
|
if err := UpdateArtistSettings(db, "artist-1", map[string]interface{}{"mbid": ""}); err != nil {
|
|
t.Fatalf("UpdateArtistSettings(clear mbid) error: %v", err)
|
|
}
|
|
got, err = GetArtistSettings(db, "artist-1")
|
|
if err != nil {
|
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
|
}
|
|
if got.MBID != "" {
|
|
t.Errorf("expected empty MBID after clear, got %q", got.MBID)
|
|
}
|
|
}
|
|
|
|
// TestArtistSettings_MbidInGetAll verifies GetAllArtistSettings returns the
|
|
// MBID field for all rows.
|
|
func TestArtistSettings_MbidInGetAll(t *testing.T) {
|
|
db, err := New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
artists := []ArtistSettings{
|
|
{ID: "a1", Name: "Artist One", MBID: "mbid-1", Monitored: true},
|
|
{ID: "a2", Name: "Artist Two", Monitored: true},
|
|
}
|
|
for _, a := range artists {
|
|
if err := SaveArtistSettings(db, &a); err != nil {
|
|
t.Fatalf("SaveArtistSettings(%s) error: %v", a.ID, err)
|
|
}
|
|
}
|
|
|
|
results, err := GetAllArtistSettings(db)
|
|
if err != nil {
|
|
t.Fatalf("GetAllArtistSettings() error: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Fatalf("expected 2 results, got %d", len(results))
|
|
}
|
|
byID := make(map[string]ArtistSettings)
|
|
for _, r := range results {
|
|
byID[r.ID] = r
|
|
}
|
|
if byID["a1"].MBID != "mbid-1" {
|
|
t.Errorf("artist a1: expected MBID 'mbid-1', got %q", byID["a1"].MBID)
|
|
}
|
|
if byID["a2"].MBID != "" {
|
|
t.Errorf("artist a2: expected empty MBID, got %q", byID["a2"].MBID)
|
|
}
|
|
}
|