Replace compute-only run() with an immediate sync+scan followed by a ticker-driven periodic loop, add the sync.interval config field (default 6h) with defaults and validation, and add tests for the scheduling logic.
422 lines
9.9 KiB
Go
422 lines
9.9 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig_SyncIntervalDefault(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.Sync.Interval != DefaultSyncInterval {
|
|
t.Errorf("expected default sync interval %v, got %v", DefaultSyncInterval, cfg.Sync.Interval)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig_SyncIntervalParsed(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 )"
|
|
|
|
sync:
|
|
interval: 30m
|
|
`
|
|
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.Sync.Interval != 30*time.Minute {
|
|
t.Errorf("expected sync interval 30m, got %v", cfg.Sync.Interval)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig_InvalidSyncInterval(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 )"
|
|
|
|
sync:
|
|
interval: -1s
|
|
`
|
|
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
|
|
if _, err := LoadConfig(path); err == nil {
|
|
t.Fatal("expected error for negative sync interval, got nil")
|
|
}
|
|
}
|