Files
wehub-resource-sync ead81af521
CI / go (push) Waiting to run
CI / build (darwin, macos-14) (push) Waiting to run
CI / build (windows, windows-2025) (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

798 lines
18 KiB
Go

package config
import (
"os"
"path/filepath"
"strconv"
"testing"
)
func TestDefaultConfig(t *testing.T) {
cfg := defaultConfig()
if cfg.Repeat != "off" {
t.Errorf("Repeat = %q, want off", cfg.Repeat)
}
if cfg.Speed != 1.0 {
t.Errorf("Speed = %f, want 1.0", cfg.Speed)
}
if cfg.SeekStepLarge != 30 {
t.Errorf("SeekStepLarge = %d, want 30", cfg.SeekStepLarge)
}
if cfg.SampleRate != 0 {
t.Errorf("SampleRate = %d, want 0 (auto)", cfg.SampleRate)
}
if cfg.BufferMs != 100 {
t.Errorf("BufferMs = %d, want 100", cfg.BufferMs)
}
if cfg.ResampleQuality != 4 {
t.Errorf("ResampleQuality = %d, want 4", cfg.ResampleQuality)
}
if cfg.BitDepth != 16 {
t.Errorf("BitDepth = %d, want 16", cfg.BitDepth)
}
if cfg.PaddingH != 3 {
t.Errorf("PaddingH = %d, want 3", cfg.PaddingH)
}
if cfg.PaddingV != 1 {
t.Errorf("PaddingV = %d, want 1", cfg.PaddingV)
}
if cfg.Spotify.Bitrate != 320 {
t.Errorf("Spotify.Bitrate = %d, want 320", cfg.Spotify.Bitrate)
}
if cfg.AutoPlay {
t.Error("AutoPlay should be false by default")
}
if cfg.Shuffle {
t.Error("Shuffle should be false by default")
}
if cfg.Mono {
t.Error("Mono should be false by default")
}
}
func TestClampVolume(t *testing.T) {
tests := []struct {
name string
vol float64
want float64
}{
{"within range", -10, -10},
{"too low", -60, -50},
{"too high", 20, 6},
{"min boundary", -50, -50},
{"max boundary", 6, 6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := defaultConfig()
cfg.Volume = tt.vol
cfg.clamp()
if cfg.Volume != tt.want {
t.Errorf("Volume = %f, want %f", cfg.Volume, tt.want)
}
})
}
}
func TestClampVolumeMin(t *testing.T) {
tests := []struct {
name string
min float64
want float64
}{
{"default", -50, -50},
{"custom valid", -70, -70},
{"too low", -100, -90},
{"too high positive", 5, 0},
{"zero boundary", 0, 0},
{"low boundary", -90, -90},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := defaultConfig()
cfg.VolumeMin = tt.min
cfg.clamp()
if cfg.VolumeMin != tt.want {
t.Errorf("VolumeMin = %f, want %f", cfg.VolumeMin, tt.want)
}
})
}
}
func TestVolumeClampedToVolumeMin(t *testing.T) {
tests := []struct {
name string
volumeMin float64
volume float64
want float64
}{
{"below floor", -30, -40, -30},
{"at floor", -30, -30, -30},
{"above floor", -30, -10, -10},
{"custom floor -60", -60, -70, -60},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := defaultConfig()
cfg.VolumeMin = tt.volumeMin
cfg.Volume = tt.volume
cfg.clamp()
if cfg.Volume != tt.want {
t.Errorf("Volume = %f, want %f", cfg.Volume, tt.want)
}
})
}
}
func TestClampSpeed(t *testing.T) {
tests := []struct {
name string
speed float64
want float64
}{
{"normal", 1.0, 1.0},
{"valid slow", 0.5, 0.5},
{"valid fast", 1.5, 1.5},
{"too slow", 0.1, 1.0},
{"too fast", 3.0, 1.0},
{"min boundary", 0.25, 0.25},
{"max boundary", 2.0, 2.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := defaultConfig()
cfg.Speed = tt.speed
cfg.clamp()
if cfg.Speed != tt.want {
t.Errorf("Speed = %f, want %f", cfg.Speed, tt.want)
}
})
}
}
func TestClampSampleRate(t *testing.T) {
tests := []struct {
input int
want int
}{
{0, 0}, // auto-detect preserved
{44100, 44100}, // exact match
{48000, 48000},
{96000, 96000},
{30000, 22050}, // rounds to nearest
{45000, 44100},
{50000, 48000},
{100000, 96000},
{200000, 192000},
}
for _, tt := range tests {
got := clampSampleRate(tt.input)
if got != tt.want {
t.Errorf("clampSampleRate(%d) = %d, want %d", tt.input, got, tt.want)
}
}
}
func TestClampBitDepth(t *testing.T) {
tests := []struct {
input int
want int
}{
{8, 16},
{16, 16},
{24, 32},
{32, 32},
}
for _, tt := range tests {
got := clampBitDepth(tt.input)
if got != tt.want {
t.Errorf("clampBitDepth(%d) = %d, want %d", tt.input, got, tt.want)
}
}
}
func TestClampSpotifyBitrate(t *testing.T) {
tests := []struct {
input int
want int
}{
{-1, 320},
{0, 320},
{96, 96},
{160, 160},
{320, 320},
{120, 96},
{128, 96},
{200, 160},
{240, 160},
{500, 320},
}
for _, tt := range tests {
got := clampSpotifyBitrate(tt.input)
if got != tt.want {
t.Errorf("clampSpotifyBitrate(%d) = %d, want %d", tt.input, got, tt.want)
}
}
}
func TestClampBufferMs(t *testing.T) {
tests := []struct {
input int
want int
}{
{100, 100},
{10, 50},
{600, 500},
{50, 50},
{500, 500},
}
for _, tt := range tests {
cfg := defaultConfig()
cfg.BufferMs = tt.input
cfg.clamp()
if cfg.BufferMs != tt.want {
t.Errorf("BufferMs(%d) = %d, want %d", tt.input, cfg.BufferMs, tt.want)
}
}
}
func TestClampResampleQuality(t *testing.T) {
tests := []struct {
input int
want int
}{
{0, 1},
{1, 1},
{4, 4},
{5, 4},
}
for _, tt := range tests {
cfg := defaultConfig()
cfg.ResampleQuality = tt.input
cfg.clamp()
if cfg.ResampleQuality != tt.want {
t.Errorf("ResampleQuality(%d) = %d, want %d", tt.input, cfg.ResampleQuality, tt.want)
}
}
}
func TestClampPadding(t *testing.T) {
tests := []struct {
name string
inH, inV int
wantH, wantV int
}{
{"negative clamped to 0", -1, -1, 0, 0},
{"over max clamped", 20, 10, 10, 5},
{"within range", 3, 1, 3, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := defaultConfig()
cfg.PaddingH = tt.inH
cfg.PaddingV = tt.inV
cfg.clamp()
if cfg.PaddingH != tt.wantH {
t.Errorf("PaddingH = %d, want %d", cfg.PaddingH, tt.wantH)
}
if cfg.PaddingV != tt.wantV {
t.Errorf("PaddingV = %d, want %d", cfg.PaddingV, tt.wantV)
}
})
}
}
func TestClampSeekStepLarge(t *testing.T) {
tests := []struct {
input int
want int
}{
{30, 30},
{1, 6},
{700, 600},
{6, 6},
{600, 600},
}
for _, tt := range tests {
cfg := defaultConfig()
cfg.SeekStepLarge = tt.input
cfg.clamp()
if cfg.SeekStepLarge != tt.want {
t.Errorf("SeekStepLarge(%d) = %d, want %d", tt.input, cfg.SeekStepLarge, tt.want)
}
}
}
func TestParseEQ(t *testing.T) {
tests := []struct {
name string
val string
want [10]float64
}{
{
name: "all zeros",
val: "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]",
want: [10]float64{},
},
{
name: "mixed values",
val: "[3, -2, 0, 1.5, -5, 0, 0, 0, 0, 0]",
want: [10]float64{3, -2, 0, 1.5, -5, 0, 0, 0, 0, 0},
},
{
name: "clamped to range",
val: "[15, -20, 0, 0, 0, 0, 0, 0, 0, 0]",
want: [10]float64{12, -12, 0, 0, 0, 0, 0, 0, 0, 0},
},
{
name: "fewer than 10",
val: "[1, 2, 3]",
want: [10]float64{1, 2, 3, 0, 0, 0, 0, 0, 0, 0},
},
{
name: "empty",
val: "[]",
want: [10]float64{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseEQ(tt.val)
if got != tt.want {
t.Errorf("parseEQ(%q) = %v, want %v", tt.val, got, tt.want)
}
})
}
}
func TestNavidromeIsSet(t *testing.T) {
tests := []struct {
name string
cfg NavidromeConfig
want bool
}{
{"all set", NavidromeConfig{URL: "https://x", User: "u", Password: "p"}, true},
{"missing URL", NavidromeConfig{User: "u", Password: "p"}, false},
{"missing user", NavidromeConfig{URL: "https://x", Password: "p"}, false},
{"missing password", NavidromeConfig{URL: "https://x", User: "u"}, false},
{"all empty", NavidromeConfig{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.IsSet(); got != tt.want {
t.Errorf("IsSet() = %v, want %v", got, tt.want)
}
})
}
}
func TestSpotifyIsSet(t *testing.T) {
tests := []struct {
name string
cfg SpotifyConfig
want bool
}{
{"section + custom id", SpotifyConfig{Enabled: true, ClientID: "abc"}, true},
{"section only (uses fallback)", SpotifyConfig{Enabled: true}, true},
{"no section", SpotifyConfig{}, false},
{"disabled", SpotifyConfig{Disabled: true, Enabled: true, ClientID: "abc"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.IsSet(); got != tt.want {
t.Errorf("IsSet() = %v, want %v", got, tt.want)
}
})
}
}
func TestSpotifyResolveClientID(t *testing.T) {
const fallback = "FALLBACK"
tests := []struct {
name string
cfg SpotifyConfig
want string
}{
{"user id wins", SpotifyConfig{ClientID: "user-id"}, "user-id"},
{"empty falls back", SpotifyConfig{}, fallback},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.ResolveClientID(fallback); got != tt.want {
t.Errorf("ResolveClientID() = %q, want %q", got, tt.want)
}
})
}
}
func TestLoadSpotifyBitrate(t *testing.T) {
tests := []struct {
name string
bitrate int
want int
}{
{"exact supported value", 160, 160},
{"rounded to nearest supported value", 200, 160},
{"non-positive value", 0, 320},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("HOME", t.TempDir())
path := filepath.Join(os.Getenv("HOME"), ".config", "cliamp", "config.toml")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
data := []byte("[spotify]\nbitrate = " + strconv.Itoa(tt.bitrate) + "\n")
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.Spotify.Bitrate != tt.want {
t.Fatalf("Spotify.Bitrate = %d, want %d", cfg.Spotify.Bitrate, tt.want)
}
})
}
}
func TestQobuzIsSet(t *testing.T) {
tests := []struct {
name string
cfg QobuzConfig
want bool
}{
{"section present", QobuzConfig{Enabled: true}, true},
{"explicitly disabled", QobuzConfig{Enabled: true, Disabled: true}, false},
{"absent", QobuzConfig{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.IsSet(); got != tt.want {
t.Errorf("IsSet() = %v, want %v", got, tt.want)
}
})
}
}
func TestLoadQobuz(t *testing.T) {
tests := []struct {
name string
body string
wantIsSet bool
wantQuality int
}{
{"section enables, default quality", "[qobuz]\n", true, 6},
{"explicit quality", "[qobuz]\nquality = 27\n", true, 27},
{"disabled", "[qobuz]\nenabled = false\n", false, 6},
{"absent", "", false, 6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("HOME", t.TempDir())
path := filepath.Join(os.Getenv("HOME"), ".config", "cliamp", "config.toml")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(path, []byte(tt.body), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if got := cfg.Qobuz.IsSet(); got != tt.wantIsSet {
t.Errorf("Qobuz.IsSet() = %v, want %v", got, tt.wantIsSet)
}
if cfg.Qobuz.Quality != tt.wantQuality {
t.Errorf("Qobuz.Quality = %d, want %d", cfg.Qobuz.Quality, tt.wantQuality)
}
})
}
}
func TestPlexIsSet(t *testing.T) {
tests := []struct {
name string
cfg PlexConfig
want bool
}{
{"both set", PlexConfig{URL: "http://x", Token: "t"}, true},
{"no URL", PlexConfig{Token: "t"}, false},
{"no token", PlexConfig{URL: "http://x"}, false},
{"empty", PlexConfig{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.IsSet(); got != tt.want {
t.Errorf("IsSet() = %v, want %v", got, tt.want)
}
})
}
}
func TestJellyfinIsSet(t *testing.T) {
tests := []struct {
name string
cfg JellyfinConfig
want bool
}{
{"token auth", JellyfinConfig{URL: "http://x", Token: "t"}, true},
{"password auth", JellyfinConfig{URL: "http://x", User: "u", Password: "p"}, true},
{"no URL", JellyfinConfig{Token: "t"}, false},
{"no auth", JellyfinConfig{URL: "http://x"}, false},
{"user without password", JellyfinConfig{URL: "http://x", User: "u"}, false},
{"empty", JellyfinConfig{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.IsSet(); got != tt.want {
t.Errorf("IsSet() = %v, want %v", got, tt.want)
}
})
}
}
func TestYouTubeMusicIsSetOrFallback(t *testing.T) {
hasFallback := func() (string, string) { return "id", "secret" }
noFallback := func() (string, string) { return "", "" }
tests := []struct {
name string
cfg YouTubeMusicConfig
fallbackFn func() (string, string)
want bool
}{
{"enabled section", YouTubeMusicConfig{Enabled: true}, nil, true},
{"disabled", YouTubeMusicConfig{Disabled: true}, hasFallback, false},
{"fallback available", YouTubeMusicConfig{}, hasFallback, true},
{"no fallback", YouTubeMusicConfig{}, noFallback, false},
{"nil fallback", YouTubeMusicConfig{}, nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.IsSetOrFallback(tt.fallbackFn); got != tt.want {
t.Errorf("IsSetOrFallback() = %v, want %v", got, tt.want)
}
})
}
}
func TestYouTubeMusicResolveCredentials(t *testing.T) {
fallback := func() (string, string) { return "fb_id", "fb_secret" }
tests := []struct {
name string
cfg YouTubeMusicConfig
fallbackFn func() (string, string)
wantID string
wantSecret string
}{
{"user credentials take priority", YouTubeMusicConfig{ClientID: "my_id", ClientSecret: "my_secret"}, fallback, "my_id", "my_secret"},
{"falls back when empty", YouTubeMusicConfig{}, fallback, "fb_id", "fb_secret"},
{"nil fallback returns empty", YouTubeMusicConfig{}, nil, "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, secret := tt.cfg.ResolveCredentials(tt.fallbackFn)
if id != tt.wantID || secret != tt.wantSecret {
t.Errorf("got (%q, %q), want (%q, %q)", id, secret, tt.wantID, tt.wantSecret)
}
})
}
}
func TestOverridesApply(t *testing.T) {
cfg := defaultConfig()
vol := -15.0
shuffle := true
repeat := "all"
mono := true
theme := "dark"
compact := true
sr := 48000
play := true
overrides := Overrides{
Volume: &vol,
Shuffle: &shuffle,
Repeat: &repeat,
Mono: &mono,
Theme: &theme,
Compact: &compact,
SampleRate: &sr,
Play: &play,
}
overrides.Apply(&cfg)
if cfg.Volume != -15 {
t.Errorf("Volume = %f, want -15", cfg.Volume)
}
if !cfg.Shuffle {
t.Error("Shuffle should be true")
}
if cfg.Repeat != "all" {
t.Errorf("Repeat = %q, want all", cfg.Repeat)
}
if !cfg.Mono {
t.Error("Mono should be true")
}
if cfg.Theme != "dark" {
t.Errorf("Theme = %q, want dark", cfg.Theme)
}
if !cfg.Compact {
t.Error("Compact should be true")
}
if cfg.SampleRate != 48000 {
t.Errorf("SampleRate = %d, want 48000", cfg.SampleRate)
}
if !cfg.AutoPlay {
t.Error("AutoPlay should be true")
}
}
func TestOverridesApplyNil(t *testing.T) {
cfg := defaultConfig()
original := cfg
overrides := Overrides{} // all nil
overrides.Apply(&cfg)
// Nothing should change except clamp effects
if cfg.Volume != original.Volume {
t.Error("nil overrides changed Volume")
}
if cfg.Shuffle != original.Shuffle {
t.Error("nil overrides changed Shuffle")
}
}
func TestOverridesApplyClamps(t *testing.T) {
cfg := defaultConfig()
vol := 100.0 // out of range
overrides := Overrides{Volume: &vol}
overrides.Apply(&cfg)
if cfg.Volume != 6 {
t.Errorf("Volume should be clamped to 6, got %f", cfg.Volume)
}
}
func TestOverridesApplyLowPower(t *testing.T) {
cfg := defaultConfig()
cfg.Visualizer = "Bars"
lowPower := true
Overrides{LowPower: &lowPower}.Apply(&cfg)
if !cfg.LowPower {
t.Fatal("LowPower = false, want true")
}
if cfg.Visualizer != "none" {
t.Fatalf("Visualizer = %q, want none", cfg.Visualizer)
}
}
// Mock player for ApplyPlayer tests
type mockPlayer struct {
volumeMin float64
volume float64
speed float64
eq [10]float64
mono bool
}
func (m *mockPlayer) SetVolumeMin(db float64) { m.volumeMin = db }
func (m *mockPlayer) SetVolume(db float64) { m.volume = db }
func (m *mockPlayer) SetSpeed(ratio float64) { m.speed = ratio }
func (m *mockPlayer) SetEQBand(band int, dB float64) { m.eq[band] = dB }
func (m *mockPlayer) ToggleMono() { m.mono = !m.mono }
func TestApplyPlayer(t *testing.T) {
cfg := defaultConfig()
cfg.VolumeMin = -70
cfg.Volume = -10
cfg.Speed = 1.5
cfg.EQ = [10]float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
cfg.EQPreset = "" // Custom
cfg.Mono = true
p := &mockPlayer{}
cfg.ApplyPlayer(p)
if p.volumeMin != -70 {
t.Errorf("volumeMin = %f, want -70", p.volumeMin)
}
if p.volume != -10 {
t.Errorf("volume = %f, want -10", p.volume)
}
if p.speed != 1.5 {
t.Errorf("speed = %f, want 1.5", p.speed)
}
for i, want := range cfg.EQ {
if p.eq[i] != want {
t.Errorf("eq[%d] = %f, want %f", i, p.eq[i], want)
}
}
if !p.mono {
t.Error("mono should be true")
}
}
func TestApplyPlayerDefaultSpeed(t *testing.T) {
cfg := defaultConfig()
cfg.Speed = 1.0 // default, should NOT call SetSpeed
p := &mockPlayer{}
cfg.ApplyPlayer(p)
if p.speed != 0 {
t.Errorf("speed = %f, should not have been set for 1.0x", p.speed)
}
}
func TestApplyPlayerWithPreset(t *testing.T) {
cfg := defaultConfig()
cfg.EQPreset = "Rock"
cfg.EQ = [10]float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
p := &mockPlayer{}
cfg.ApplyPlayer(p)
// With a preset set (not Custom/""), individual EQ bands should NOT be applied
for i, v := range p.eq {
if v != 0 {
t.Errorf("eq[%d] = %f, want 0 (preset should skip band apply)", i, v)
}
}
}
// Mock playlist for ApplyPlaylist tests
type mockPlaylist struct {
repeatCycles int
shuffled bool
}
func (m *mockPlaylist) CycleRepeat() { m.repeatCycles++ }
func (m *mockPlaylist) ToggleShuffle() { m.shuffled = !m.shuffled }
func TestApplyPlaylist(t *testing.T) {
tests := []struct {
name string
repeat string
shuffle bool
wantCycles int
wantShuffle bool
}{
{"off no shuffle", "off", false, 0, false},
{"all", "all", false, 1, false},
{"one", "one", false, 2, false},
{"shuffle", "off", true, 0, true},
{"all + shuffle", "all", true, 1, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := defaultConfig()
cfg.Repeat = tt.repeat
cfg.Shuffle = tt.shuffle
pl := &mockPlaylist{}
cfg.ApplyPlaylist(pl)
if pl.repeatCycles != tt.wantCycles {
t.Errorf("repeat cycles = %d, want %d", pl.repeatCycles, tt.wantCycles)
}
if pl.shuffled != tt.wantShuffle {
t.Errorf("shuffled = %v, want %v", pl.shuffled, tt.wantShuffle)
}
})
}
}