Files
NaviWatcher/internal/database/artist_settings_test.go
Vladimir Zagainov 735ff0828e feat: add foundation layer (Go module, config, database, Docker)
Squashed commits from foundation-layer branch:

- Initialize Go module and project skeleton (cmd/naviwatcher/main.go)
- Add configuration management with YAML parsing and validation
- Add database layer with schema migrations (artist_settings, external_releases, notifications_sent)
- Add CRUD operations for artist_settings, external_releases, notifications_sent
- Add Docker setup with multi-stage build and docker-compose
- Verify acceptance criteria (tests, vet, fmt)
- Update README.md with build/run/test instructions
- Fix: filter ignored releases in GetUnnotifiedReleases (spec compliance)
- Fix: add FK constraint on notifications_sent.rgid
- Fix: add config.yaml to .gitignore (security)
- Fix: run Docker container as non-root user
- Fix: pin alpine:3.21 instead of alpine:latest
- Fix: wrap migrations in transactions for atomicity

All 49 tests pass, go vet clean, Docker image builds successfully.
2026-05-20 16:11:11 +03:00

308 lines
8.0 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 sql.ErrNoRows.
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 != sql.ErrNoRows {
t.Errorf("expected sql.ErrNoRows, 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")
}
}