Files
NaviWatcher/internal/config/config_test.go
Vladimir Zagainov 6ff4ec3045 feat: add configuration management with YAML parsing and validation
Add internal/config package with Config struct (Server, Navidrome,
MusicBrainz, Telegram, Scanner sections), LoadConfig function for
YAML parsing, config validation (required fields, port range,
threshold range), and defaults. Include config.yaml.example matching
spec. Update main.go to use real config package instead of
placeholders. Add comprehensive tests covering valid configs,
defaults, missing files, malformed YAML, and validation boundaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-20 08:47:04 +03:00

367 lines
8.7 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: "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.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 zero with explicit 0 and no default override", 0},
{"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 := `
navidrome:
url: "http://localhost:4533"
user: "u"
password: "p"
musicbrainz:
user_agent: "NaviWatcher/1.0 ( test@example.com )"
server:
port: ` + string(rune('0'+tt.port%10)) + `
`
// Use Sprintf for proper port formatting
yaml = buildConfigWithPort(path, 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.port == 0 {
// port 0 gets defaulted to 8080, which is valid
if err != nil {
t.Fatalf("port 0 should get default, got error: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error for port %d, got nil", tt.port)
}
})
}
}
func buildConfigWithPort(path string, 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(path, 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)
}
})
}
}