- Strip standalone reissue keywords (remaster/remastered/remix/deluxe/ expanded/edition/reissue/anniversary/bonus) regardless of brackets so non-parenthesized remasters still match the plain local title above the 0.85 threshold (was falsely reported missing). - Fix TestAppRun_ScanLogsMissingReleases to verify run() performs the scan itself (capture its log output) instead of re-running ScanAll independently, which passed even if run() were a no-op. - Fix TestSimilarity misleading cases that downgraded to a <0.85 range check with dead want/epsilon fields; assert actual computed scores.
240 lines
6.1 KiB
Go
240 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"naviwatcher/internal/config"
|
|
"naviwatcher/internal/database"
|
|
)
|
|
|
|
func TestAppRun_ScanLogsMissingReleases(t *testing.T) {
|
|
// Verify the compute-only run() hook scans monitored artists and returns
|
|
// nil without starting notifier/web. Uses an in-memory DB with one
|
|
// monitored artist that has one missing release (Animals) vs a local album
|
|
// (The Wall). The context is left live so the scan actually executes; we
|
|
// cancel shortly after to let run() return cleanly.
|
|
db, err := database.New(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("database.New() error: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := database.SaveArtistSettings(db, &database.ArtistSettings{
|
|
ID: "artist-1",
|
|
Name: "Pink Floyd",
|
|
Monitored: true,
|
|
}); err != nil {
|
|
t.Fatalf("seed artist: %v", err)
|
|
}
|
|
if err := database.SaveLocalAlbum(db, &database.LocalAlbum{
|
|
ID: "l1",
|
|
ArtistID: "artist-1",
|
|
Title: "The Wall",
|
|
}); err != nil {
|
|
t.Fatalf("seed local album: %v", err)
|
|
}
|
|
if err := database.SaveExternalRelease(db, &database.ExternalRelease{
|
|
RGID: "rg2",
|
|
ArtistID: "artist-1",
|
|
Title: "Animals",
|
|
}); err != nil {
|
|
t.Fatalf("seed external release: %v", err)
|
|
}
|
|
|
|
app := &App{
|
|
cfg: &config.Config{Scanner: config.ScannerConfig{FuzzyThreshold: 0.85}},
|
|
db: db,
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Capture run()'s log output so we assert that run() ITSELF performed
|
|
// the scan (not a separately re-run ScanAll). This guards against the
|
|
// hook silently becoming a no-op while still passing.
|
|
var buf bytes.Buffer
|
|
log.SetOutput(&buf)
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
// Run the (blocking) hook in a goroutine; cancel after it has had time to
|
|
// perform the scan so run() returns nil via the ctx.Done() path.
|
|
done := make(chan error, 1)
|
|
go func() { done <- app.run(ctx) }()
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("app.run() returned error: %v", err)
|
|
}
|
|
|
|
// run() must have logged the missing release (Animals) for artist-1.
|
|
out := buf.String()
|
|
if !strings.Contains(out, "missing: artist=artist-1") || !strings.Contains(out, "Animals") {
|
|
t.Fatalf("app.run() did not log the expected missing release; log output:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestConfigIntegration(t *testing.T) {
|
|
// Integration test: write a minimal valid config and load it via config.LoadConfig,
|
|
// verifying the full path that main() uses.
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.yaml")
|
|
|
|
yaml := `server:
|
|
host: "127.0.0.1"
|
|
port: 9090
|
|
navidrome:
|
|
url: "http://localhost:4533"
|
|
user: "test"
|
|
password: "test"
|
|
musicbrainz:
|
|
user_agent: "NaviWatcher/1.0 ( test@example.com )"
|
|
`
|
|
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig returned error: %v", err)
|
|
}
|
|
|
|
if cfg.Server.Host != "127.0.0.1" {
|
|
t.Errorf("expected host 127.0.0.1, got %q", cfg.Server.Host)
|
|
}
|
|
if cfg.Server.Port != 9090 {
|
|
t.Errorf("expected port 9090, got %d", cfg.Server.Port)
|
|
}
|
|
if cfg.Navidrome.URL != "http://localhost:4533" {
|
|
t.Errorf("expected navidrome url http://localhost:4533, got %q", cfg.Navidrome.URL)
|
|
}
|
|
}
|
|
|
|
func TestNewApp_CreatesMusicBrainzClient(t *testing.T) {
|
|
// Verify that NewApp initializes the MusicBrainz client from config.
|
|
cfg := &config.Config{
|
|
Server: config.ServerConfig{
|
|
Host: "127.0.0.1",
|
|
Port: 9090,
|
|
},
|
|
Navidrome: config.NavidromeConfig{
|
|
URL: "http://localhost:4533",
|
|
User: "test",
|
|
Password: "test",
|
|
},
|
|
MusicBrainz: config.MusicBrainzConfig{
|
|
UserAgent: "NaviWatcher/1.0 ( test@example.com )",
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
app, err := NewApp(ctx, cfg, ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("NewApp returned error: %v", err)
|
|
}
|
|
defer app.Close()
|
|
|
|
if app.mbClient == nil {
|
|
t.Fatal("expected MusicBrainz client to be initialized, got nil")
|
|
}
|
|
if app.db == nil {
|
|
t.Fatal("expected database to be initialized, got nil")
|
|
}
|
|
if app.cfg != cfg {
|
|
t.Fatal("expected app.cfg to be the config passed to NewApp")
|
|
}
|
|
}
|
|
|
|
func TestNewApp_GracefulShutdown(t *testing.T) {
|
|
// Verify that App.Close() cleans up resources without error.
|
|
cfg := &config.Config{
|
|
Server: config.ServerConfig{
|
|
Host: "127.0.0.1",
|
|
Port: 9090,
|
|
},
|
|
Navidrome: config.NavidromeConfig{
|
|
URL: "http://localhost:4533",
|
|
User: "test",
|
|
Password: "test",
|
|
},
|
|
MusicBrainz: config.MusicBrainzConfig{
|
|
UserAgent: "NaviWatcher/1.0 ( test@example.com )",
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
app, err := NewApp(ctx, cfg, ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("NewApp returned error: %v", err)
|
|
}
|
|
|
|
// Close should not panic or return error.
|
|
app.Close()
|
|
}
|
|
|
|
func TestAppRun_GracefulShutdown(t *testing.T) {
|
|
// Verify that app.run() returns nil when context is cancelled.
|
|
cfg := &config.Config{
|
|
Server: config.ServerConfig{
|
|
Host: "127.0.0.1",
|
|
Port: 9090,
|
|
},
|
|
Navidrome: config.NavidromeConfig{
|
|
URL: "http://localhost:4533",
|
|
User: "test",
|
|
Password: "test",
|
|
},
|
|
MusicBrainz: config.MusicBrainzConfig{
|
|
UserAgent: "NaviWatcher/1.0 ( test@example.com )",
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
app, err := NewApp(ctx, cfg, ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("NewApp returned error: %v", err)
|
|
}
|
|
defer app.Close()
|
|
|
|
// Cancel the context to trigger shutdown.
|
|
cancel()
|
|
|
|
if err := app.run(ctx); err != nil {
|
|
t.Fatalf("app.run returned error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMusicBrainzUserAgentValidation(t *testing.T) {
|
|
// Verify that config validation requires MusicBrainz.UserAgent to be set.
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.yaml")
|
|
|
|
yaml := `server:
|
|
host: "127.0.0.1"
|
|
port: 9090
|
|
navidrome:
|
|
url: "http://localhost:4533"
|
|
user: "test"
|
|
password: "test"
|
|
musicbrainz:
|
|
user_agent: ""
|
|
`
|
|
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
|
|
_, err := config.LoadConfig(path)
|
|
if err == nil {
|
|
t.Fatal("expected config validation error for empty musicbrainz.user_agent, got nil")
|
|
}
|
|
}
|