- Fix notifications_sent PK: changed from (rgid, sent_at) to rgid-only PK to prevent duplicate RGID rows across seconds. Use INSERT OR REPLACE instead of INSERT OR IGNORE for true idempotency. - Add foreign key constraints to DDL (artist_id references artist_settings, rgid references external_releases) per specification. - Enable PRAGMA foreign_keys=ON and PRAGMA busy_timeout=5000 for concurrent access safety. - Fix GetNotificationSentAt query: add ORDER BY sent_at DESC LIMIT 1 for deterministic results. - Fix config test: change YAML key from 'chat' to 'chat_id' to match struct tag, add ChatID assertion. - Fix migration tracking test: correct error message from "expected 4" to "expected 3". - Remove dead code in TestLoadConfig_InvalidPort: eliminate unused YAML template and remove port 0 case (valid, not invalid). - Remove unused path parameter from buildConfigWithPort helper. - Remove pointless 100ms sleep in run() and unused time import. - Remove tautological TestDefaultConfigPath test. - Update README.md: Go version 1.21+ to 1.25+, placeholder passwords to CHANGE_ME. - Update config.yaml.example: placeholder passwords to CHANGE_ME. - Update all database tests to insert parent rows first for FK satisfaction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
349 lines
8.3 KiB
Go
349 lines
8.3 KiB
Go
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
|
|
ignore_bootlegs: true
|
|
include_compilations: false
|
|
`
|
|
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)
|
|
}
|
|
if !cfg.Scanner.IgnoreBootlegs {
|
|
t.Error("expected ignore_bootlegs true")
|
|
}
|
|
if cfg.Scanner.IncludeCompilations {
|
|
t.Error("expected include_compilations false")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|