package config import ( "os" "testing" "gopkg.in/yaml.v3" ) // --- IntRange --- func TestIntRangeValue_Scalar(t *testing.T) { r := IntRange{Min: 5, Max: 5} if v := r.Value(); v != 5 { t.Errorf("expected 5, got %d", v) } } func TestIntRangeValue_Range(t *testing.T) { r := IntRange{Min: 10, Max: 20} for i := 0; i < 100; i++ { v := r.Value() if v < 10 || v > 20 { t.Errorf("value %d out of [10, 20]", v) } } } func TestIntRangeUnmarshalYAML_Scalar(t *testing.T) { yamlData := []byte("timeout: 5000\n") type wrapper struct { Timeout int `yaml:"timeout"` } var w wrapper if err := yaml.Unmarshal(yamlData, &w); err != nil { t.Fatal(err) } if w.Timeout != 5000 { t.Errorf("expected 5000, got %d", w.Timeout) } } func TestIntRangeUnmarshalYAML_FromFullConfig(t *testing.T) { yamlData := []byte(` check: playlists: all-cooldown: 1 one-cooldown: [2, 3] channels: cooldown: [0, 1] `) cfg := defaults() if err := yaml.Unmarshal(yamlData, cfg); err != nil { t.Fatal(err) } if cfg.Check.Playlists.AllCooldown.Min != 1000 || cfg.Check.Playlists.AllCooldown.Max != 1000 { t.Errorf("scalar all-cooldown: expected Min=Max=1000 ms from 1 s, got Min=%d Max=%d", cfg.Check.Playlists.AllCooldown.Min, cfg.Check.Playlists.AllCooldown.Max) } if cfg.Check.Playlists.OneCooldown.Min != 2000 || cfg.Check.Playlists.OneCooldown.Max != 3000 { t.Errorf("range one-cooldown: expected [2000,3000] ms from [2,3] s, got [%d,%d]", cfg.Check.Playlists.OneCooldown.Min, cfg.Check.Playlists.OneCooldown.Max) } if cfg.Check.Channels.Cooldown.Min != 0 || cfg.Check.Channels.Cooldown.Max != 1000 { t.Errorf("range cooldown: expected [0,1000] ms from [0,1] s, got [%d,%d]", cfg.Check.Channels.Cooldown.Min, cfg.Check.Channels.Cooldown.Max) } } // --- UserAgents --- func TestUserAgentsPick_Empty(t *testing.T) { ua := UserAgents{} if ua.Pick() != "" { t.Error("expected empty string for empty UserAgents") } } func TestUserAgentsPick_Single(t *testing.T) { ua := UserAgents{"Mozilla"} if ua.Pick() != "Mozilla" { t.Errorf("expected 'Mozilla', got '%s'", ua.Pick()) } } func TestUserAgentsPick_Multiple(t *testing.T) { ua := UserAgents{"A", "B", "C"} for i := 0; i < 100; i++ { v := ua.Pick() found := false for _, s := range ua { if v == s { found = true break } } if !found { t.Errorf("Pick returned '%s' which is not in list", v) } } } func TestUserAgentsUnmarshalYAML_Scalar(t *testing.T) { yamlData := []byte(` check: channels: user-agent: Mozilla/5.0 `) cfg := defaults() if err := yaml.Unmarshal(yamlData, cfg); err != nil { t.Fatal(err) } if len(cfg.Check.Channels.UserAgent) != 1 || cfg.Check.Channels.UserAgent[0] != "Mozilla/5.0" { t.Errorf("expected single 'Mozilla/5.0', got %v", cfg.Check.Channels.UserAgent) } } func TestUserAgentsUnmarshalYAML_Array(t *testing.T) { yamlData := []byte(` check: channels: user-agent: - Mozilla/5.0 - curl/8.0 `) cfg := defaults() if err := yaml.Unmarshal(yamlData, cfg); err != nil { t.Fatal(err) } if len(cfg.Check.Channels.UserAgent) != 2 { t.Errorf("expected 2 user-agents, got %d", len(cfg.Check.Channels.UserAgent)) } } // --- parseBool / parseUint / parseInt --- func TestParseBool(t *testing.T) { cases := []struct { in string def bool want bool }{ {"true", false, true}, {"1", false, true}, {"false", true, false}, {"0", true, false}, {"yes", false, false}, {"", true, true}, } for _, c := range cases { got := parseBool(c.in, c.def) if got != c.want { t.Errorf("parseBool(%q, %v) = %v, want %v", c.in, c.def, got, c.want) } } } func TestParseUint(t *testing.T) { cases := []struct { in string def uint want uint }{ {"8800", 0, 8800}, {"0", 999, 0}, {"abc", 999, 999}, {"", 999, 999}, } for _, c := range cases { got := parseUint(c.in, c.def) if got != c.want { t.Errorf("parseUint(%q, %d) = %d, want %d", c.in, c.def, got, c.want) } } } func TestParseInt(t *testing.T) { cases := []struct { in string def int want int }{ {"100", 0, 100}, {"-5", 0, -5}, {"abc", 999, 999}, {"", 999, 999}, } for _, c := range cases { got := parseInt(c.in, c.def) if got != c.want { t.Errorf("parseInt(%q, %d) = %d, want %d", c.in, c.def, got, c.want) } } } // --- applyEnvIntRange --- func TestApplyEnvIntRange_Scalar(t *testing.T) { t.Setenv("TEST_COOLDOWN", "1.5") r := IntRange{Min: 0, Max: 0} applyEnvIntRange(&r, "TEST_COOLDOWN") if r.Min != 1500 || r.Max != 1500 { t.Errorf("expected Min=Max=1500, got Min=%d Max=%d", r.Min, r.Max) } } func TestApplyEnvIntRange_Range(t *testing.T) { t.Setenv("TEST_COOLDOWN", "1,2") r := IntRange{Min: 0, Max: 0} applyEnvIntRange(&r, "TEST_COOLDOWN") if r.Min != 1000 || r.Max != 2000 { t.Errorf("expected [1000,2000], got [%d,%d]", r.Min, r.Max) } } func TestApplyEnvIntRange_Invalid(t *testing.T) { t.Setenv("TEST_COOLDOWN", "1,invalid") r := IntRange{Min: 5, Max: 5} applyEnvIntRange(&r, "TEST_COOLDOWN") if r.Min != 5 || r.Max != 5 { t.Errorf("expected unchanged [5,5], got [%d,%d]", r.Min, r.Max) } } func TestApplyEnvIntRange_None(t *testing.T) { r := IntRange{Min: 7, Max: 7} applyEnvIntRange(&r, "TEST_COOLDOWN") if r.Min != 7 || r.Max != 7 { t.Errorf("expected unchanged [7,7], got [%d,%d]", r.Min, r.Max) } } // --- applyEnvStringSlice --- func TestApplyEnvStringSlice_Multiple(t *testing.T) { t.Setenv("MY_PREFIX_1", "foo") t.Setenv("MY_PREFIX_2", "bar") t.Setenv("MY_PREFIX_3", "baz") result := applyEnvStringSlice("MY_PREFIX") if len(result) != 3 { t.Fatalf("expected 3 items, got %d", len(result)) } if result[0] != "foo" || result[1] != "bar" || result[2] != "baz" { t.Errorf("unexpected result: %v", result) } } func TestApplyEnvStringSlice_EmptyValue(t *testing.T) { t.Setenv("MY_PREFIX_1", "foo") t.Setenv("MY_PREFIX_2", "") t.Setenv("MY_PREFIX_3", "baz") result := applyEnvStringSlice("MY_PREFIX") if len(result) != 2 { t.Fatalf("expected 2 items (empty skipped), got %d: %v", len(result), result) } if result[0] != "foo" || result[1] != "baz" { t.Errorf("unexpected result: %v", result) } } func TestApplyEnvStringSlice_None(t *testing.T) { result := applyEnvStringSlice("NONEXISTENT_PREFIX") if result != nil { t.Errorf("expected nil, got %v", result) } } // --- validate --- func TestValidate_ServerPort(t *testing.T) { cfg := defaults() cfg.Server.Port = 0 validate(cfg) if cfg.Server.Port != 8800 { t.Errorf("expected 8800, got %d", cfg.Server.Port) } } func TestValidate_ServerPortTooHigh(t *testing.T) { cfg := defaults() cfg.Server.Port = 99999 validate(cfg) if cfg.Server.Port != 8800 { t.Errorf("expected 8800, got %d", cfg.Server.Port) } } func TestValidate_BaseUrlEmpty(t *testing.T) { cfg := defaults() cfg.Site.BaseUrl = "" cfg.Server.Port = 3000 validate(cfg) if cfg.Site.BaseUrl != "http://localhost:3000" { t.Errorf("expected http://localhost:3000, got %s", cfg.Site.BaseUrl) } } func TestValidate_CacheEnabled(t *testing.T) { cfg := defaults() cfg.Cache.Enabled = true cfg.Cache.Host = "" cfg.Cache.Port = 0 cfg.Cache.Ttl = 0 validate(cfg) if cfg.Cache.Host != "localhost" { t.Errorf("expected localhost, got %s", cfg.Cache.Host) } if cfg.Cache.Port != 6379 { t.Errorf("expected 6379, got %d", cfg.Cache.Port) } if cfg.Cache.Ttl != 30 { t.Errorf("expected 30, got %d", cfg.Cache.Ttl) } } func TestValidate_PlaylistsTimeout(t *testing.T) { cfg := defaults() cfg.Check.Playlists.Timeout = 0 validate(cfg) if cfg.Check.Playlists.Timeout != 10000 { t.Errorf("expected 10000, got %d", cfg.Check.Playlists.Timeout) } } func TestValidate_ChannelsTimeout(t *testing.T) { cfg := defaults() cfg.Check.Channels.Timeout = -5 validate(cfg) if cfg.Check.Channels.Timeout != 10000 { t.Errorf("expected 10000, got %d", cfg.Check.Channels.Timeout) } } func TestValidate_PlaylistsMaxRoutines(t *testing.T) { cfg := defaults() cfg.Check.Playlists.MaxRoutines = 0 validate(cfg) if cfg.Check.Playlists.MaxRoutines != 5 { t.Errorf("expected 5, got %d", cfg.Check.Playlists.MaxRoutines) } } func TestValidate_ChannelsMaxRoutines(t *testing.T) { cfg := defaults() cfg.Check.Channels.MaxRoutines = 0 validate(cfg) if cfg.Check.Channels.MaxRoutines != 50 { t.Errorf("expected 50, got %d", cfg.Check.Channels.MaxRoutines) } } func TestValidate_ChannelsByteRange(t *testing.T) { cfg := defaults() cfg.Check.Channels.ByteRange = 0 validate(cfg) if cfg.Check.Channels.ByteRange != 512 { t.Errorf("expected 512, got %d", cfg.Check.Channels.ByteRange) } } func TestValidate_UserAgentEmpty(t *testing.T) { cfg := defaults() cfg.Check.Playlists.UserAgent = nil cfg.Check.Channels.UserAgent = nil validate(cfg) if len(cfg.Check.Playlists.UserAgent) == 0 { t.Error("expected non-empty playlists user-agent") } if len(cfg.Check.Channels.UserAgent) == 0 { t.Error("expected non-empty channels user-agent") } } // --- validateIntRange --- func TestValidateIntRange_Swap(t *testing.T) { r := IntRange{Min: 100, Max: 10} validateIntRange(&r, "test", 0, 1000) if r.Min != 10 || r.Max != 100 { t.Errorf("expected [10,100] after swap, got [%d,%d]", r.Min, r.Max) } } func TestValidateIntRange_ClampMin(t *testing.T) { r := IntRange{Min: -5, Max: 10} validateIntRange(&r, "test", 0, 1000) if r.Min != 0 { t.Errorf("expected Min=0 after clamp, got %d", r.Min) } } func TestValidateIntRange_ClampMax(t *testing.T) { r := IntRange{Min: 0, Max: 999999} validateIntRange(&r, "test", 0, 1000) if r.Max != 1000 { t.Errorf("expected Max=1000 after clamp, got %d", r.Max) } } func TestValidateIntRange_SwapThenClamp(t *testing.T) { r := IntRange{Min: 2000, Max: -10} validateIntRange(&r, "test", 0, 1000) // swap → [−10, 2000], clamp → [0, 1000] if r.Min != 0 || r.Max != 1000 { t.Errorf("expected [0,1000], got [%d,%d]", r.Min, r.Max) } } // --- defaults --- func TestDefaults(t *testing.T) { cfg := defaults() if cfg.App.Timezone != "GMT" { t.Errorf("expected GMT, got %s", cfg.App.Timezone) } if cfg.Server.Port != 8800 { t.Errorf("expected 8800, got %d", cfg.Server.Port) } if cfg.Check.Playlists.Timeout != 10000 { t.Errorf("expected 10000 ms, got %d", cfg.Check.Playlists.Timeout) } if cfg.Check.Channels.Timeout != 10000 { t.Errorf("expected 10000 ms, got %d", cfg.Check.Channels.Timeout) } if cfg.Cache.Port != 6379 { t.Errorf("expected 6379, got %d", cfg.Cache.Port) } } // --- Init (integration) --- func TestInit_NoConfigFile(t *testing.T) { cfg := Init("/nonexistent/config.yml") if cfg == nil { t.Fatal("expected non-nil config") } if cfg.Server.Port != 8800 { t.Errorf("expected default port 8800, got %d", cfg.Server.Port) } } func TestInit_WithConfigFile(t *testing.T) { tmp := t.TempDir() + "/config.yml" data := []byte(` app: timezone: UTC+3 debug: true server: port: 3000 host: 0.0.0.0 check: playlists: timeout: 5 all-cooldown: [1, 2] channels: byte-range: 1024 cache: enabled: true host: db.local port: 6380 `) if err := os.WriteFile(tmp, data, 0644); err != nil { t.Fatal(err) } cfg := Init(tmp) if cfg.App.Timezone != "UTC+3" { t.Errorf("expected UTC+3, got %s", cfg.App.Timezone) } if !cfg.App.Debug { t.Error("expected debug=true") } if cfg.Server.Port != 3000 { t.Errorf("expected 3000, got %d", cfg.Server.Port) } if cfg.Server.Host != "0.0.0.0" { t.Errorf("expected 0.0.0.0, got %s", cfg.Server.Host) } if cfg.Check.Playlists.Timeout != 5000 { t.Errorf("expected 5000 ms from 5 s, got %d", cfg.Check.Playlists.Timeout) } if cfg.Check.Playlists.AllCooldown.Min != 1000 || cfg.Check.Playlists.AllCooldown.Max != 2000 { t.Errorf("expected [1000,2000] ms from [1,2] s, got [%d,%d]", cfg.Check.Playlists.AllCooldown.Min, cfg.Check.Playlists.AllCooldown.Max) } if cfg.Check.Channels.ByteRange != 1024 { t.Errorf("expected 1024, got %d", cfg.Check.Channels.ByteRange) } if !cfg.Cache.Enabled { t.Error("expected cache enabled") } if cfg.Cache.Host != "db.local" { t.Errorf("expected db.local, got %s", cfg.Cache.Host) } if cfg.Cache.Port != 6380 { t.Errorf("expected 6380, got %d", cfg.Cache.Port) } } func TestInit_EnvOverridesConfig(t *testing.T) { tmp := t.TempDir() + "/config.yml" data := []byte("server:\n port: 3000\n") if err := os.WriteFile(tmp, data, 0644); err != nil { t.Fatal(err) } t.Setenv("SERVER_PORT", "9090") t.Setenv("APP_DEBUG", "true") t.Setenv("CHECK_PLAYLISTS_TIMEOUT", "7") t.Setenv("CHECK_PLAYLISTS_USER_AGENT_1", "CustomUA") t.Setenv("CHECK_PLAYLISTS_ALL_COOLDOWN", "1.5,2.5") cfg := Init(tmp) if cfg.Server.Port != 9090 { t.Errorf("expected 9090 from env, got %d", cfg.Server.Port) } if !cfg.App.Debug { t.Error("expected debug=true from env") } if cfg.Check.Playlists.Timeout != 7000 { t.Errorf("expected 7000 ms from 7 s env, got %d", cfg.Check.Playlists.Timeout) } if len(cfg.Check.Playlists.UserAgent) != 1 || cfg.Check.Playlists.UserAgent[0] != "CustomUA" { t.Errorf("expected [CustomUA], got %v", cfg.Check.Playlists.UserAgent) } if cfg.Check.Playlists.AllCooldown.Min != 1500 || cfg.Check.Playlists.AllCooldown.Max != 2500 { t.Errorf("expected [1500,2500] ms from [1.5,2.5] s env, got [%d,%d]", cfg.Check.Playlists.AllCooldown.Min, cfg.Check.Playlists.AllCooldown.Max) } } func TestInit_EnvScalarCooldownOverridesRange(t *testing.T) { tmp := t.TempDir() + "/config.yml" data := []byte("check:\n playlists:\n all-cooldown: [1, 2]\n") if err := os.WriteFile(tmp, data, 0644); err != nil { t.Fatal(err) } t.Setenv("CHECK_PLAYLISTS_ALL_COOLDOWN", "2.5") cfg := Init(tmp) if cfg.Check.Playlists.AllCooldown.Min != 2500 || cfg.Check.Playlists.AllCooldown.Max != 2500 { t.Errorf("expected scalar [2500,2500] ms from 2.5 s env, got [%d,%d]", cfg.Check.Playlists.AllCooldown.Min, cfg.Check.Playlists.AllCooldown.Max) } } // helper removed — using yaml.Unmarshal directly