package config import ( "fmt" "os" "path/filepath" "testing" ) func TestLoadConfig_Valid(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := ` server: host: "127.0.0.1" port: 9090 username: "testuser" password: "testpass" navidrome: url: "http://navidrome:4533" user: "service" password: "secret" musicbrainz: user_agent: "NaviWatcher/1.0 ( test@example.com )" cache_ttl: 24h telegram: enabled: true token: "abc123" chat_id: "chat456" cron_schedule: "0 10 * * *" scanner: fuzzy_threshold: 0.9 ` if err := os.WriteFile(path, []byte(yaml), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } cfg, err := 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 %s", cfg.Server.Host) } if cfg.Server.Port != 9090 { t.Errorf("expected port 9090, got %d", cfg.Server.Port) } if cfg.Server.Username != "testuser" { t.Errorf("expected username testuser, got %s", cfg.Server.Username) } if cfg.Navidrome.URL != "http://navidrome:4533" { t.Errorf("expected navidrome url http://navidrome:4533, got %s", cfg.Navidrome.URL) } if cfg.Telegram.ChatID != "chat456" { t.Errorf("expected telegram chat_id chat456, got %s", cfg.Telegram.ChatID) } if cfg.Navidrome.User != "service" { t.Errorf("expected navidrome user service, got %s", cfg.Navidrome.User) } if cfg.Scanner.FuzzyThreshold != 0.9 { t.Errorf("expected fuzzy_threshold 0.9, got %f", cfg.Scanner.FuzzyThreshold) } } func TestLoadConfig_Defaults(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := ` navidrome: url: "http://localhost:4533" user: "u" password: "p" 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 := LoadConfig(path) if err != nil { t.Fatalf("LoadConfig returned error: %v", err) } if cfg.Server.Host != "0.0.0.0" { t.Errorf("expected default host 0.0.0.0, got %s", cfg.Server.Host) } if cfg.Server.Port != 8080 { t.Errorf("expected default port 8080, got %d", cfg.Server.Port) } if cfg.Scanner.FuzzyThreshold != 0.85 { t.Errorf("expected default fuzzy_threshold 0.85, got %f", cfg.Scanner.FuzzyThreshold) } } func TestLoadConfig_MissingFile(t *testing.T) { _, err := LoadConfig("/nonexistent/path/config.yaml") if err == nil { t.Fatal("expected error for missing file, got nil") } } func TestLoadConfig_MalformedYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := ` server: host: [invalid yaml: {broken ` if err := os.WriteFile(path, []byte(yaml), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } _, err := LoadConfig(path) if err == nil { t.Fatal("expected error for malformed YAML, got nil") } } func TestLoadConfig_InvalidPort(t *testing.T) { tests := []struct { name string port int }{ {"port too high", 70000}, {"port negative", -1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := buildConfigWithPort(tt.port) if err := os.WriteFile(path, []byte(yaml), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } _, err := LoadConfig(path) if err == nil { t.Fatalf("expected error for port %d, got nil", tt.port) } }) } } func buildConfigWithPort(port int) string { return "navidrome:\n url: \"http://localhost:4533\"\n user: \"u\"\n password: \"p\"\n\nmusicbrainz:\n user_agent: \"NaviWatcher/1.0 ( test@example.com )\"\n\nserver:\n port: " + fmt.Sprintf("%d", port) + "\n" } func TestLoadConfig_InvalidThreshold(t *testing.T) { tests := []struct { name string threshold float64 }{ {"threshold too high", 1.5}, {"threshold negative", -0.1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := "navidrome:\n url: \"http://localhost:4533\"\n user: \"u\"\n password: \"p\"\n\nmusicbrainz:\n user_agent: \"NaviWatcher/1.0 ( test@example.com )\"\n\nscanner:\n fuzzy_threshold: " + fmt.Sprintf("%.1f", tt.threshold) + "\n" if err := os.WriteFile(path, []byte(yaml), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } _, err := LoadConfig(path) if err == nil { t.Fatalf("expected error for threshold %f, got nil", tt.threshold) } }) } } func TestValidate_MissingNavidromeURL(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := ` navidrome: user: "u" password: "p" 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) } _, err := LoadConfig(path) if err == nil { t.Fatal("expected error for missing navidrome.url, got nil") } } func TestValidate_MissingNavidromeUser(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := ` navidrome: url: "http://localhost:4533" password: "p" 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) } _, err := LoadConfig(path) if err == nil { t.Fatal("expected error for missing navidrome.user, got nil") } } func TestValidate_MissingNavidromePassword(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := ` navidrome: url: "http://localhost:4533" user: "u" 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) } _, err := LoadConfig(path) if err == nil { t.Fatal("expected error for missing navidrome.password, got nil") } } func TestValidate_MissingMusicBrainzUserAgent(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := ` navidrome: url: "http://localhost:4533" user: "u" password: "p" musicbrainz: cache_ttl: 24h ` if err := os.WriteFile(path, []byte(yaml), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } _, err := LoadConfig(path) if err == nil { t.Fatal("expected error for missing musicbrainz.user_agent, got nil") } } func TestValidate_BoundaryThreshold(t *testing.T) { tests := []struct { name string threshold float64 wantErr bool }{ {"threshold 0.0", 0.0, false}, {"threshold 1.0", 1.0, false}, {"threshold 0.5", 0.5, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := "navidrome:\n url: \"http://localhost:4533\"\n user: \"u\"\n password: \"p\"\n\nmusicbrainz:\n user_agent: \"NaviWatcher/1.0 ( test@example.com )\"\n\nscanner:\n fuzzy_threshold: " + fmt.Sprintf("%.2f", tt.threshold) + "\n" if err := os.WriteFile(path, []byte(yaml), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } _, err := LoadConfig(path) if tt.wantErr && err == nil { t.Fatalf("expected error for threshold %f, got nil", tt.threshold) } if !tt.wantErr && err != nil { t.Fatalf("unexpected error for threshold %f: %v", tt.threshold, err) } }) } } func TestValidate_BoundaryPort(t *testing.T) { tests := []struct { name string port int wantErr bool }{ {"port 1", 1, false}, {"port 65535", 65535, false}, {"port 8080", 8080, false}, {"port 65536", 65536, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") yaml := buildConfigWithPort(tt.port) if err := os.WriteFile(path, []byte(yaml), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } _, err := LoadConfig(path) if tt.wantErr && err == nil { t.Fatalf("expected error for port %d, got nil", tt.port) } if !tt.wantErr && err != nil { t.Fatalf("unexpected error for port %d: %v", tt.port, err) } }) } }