package config import ( "os" "path/filepath" "strings" "testing" ) const valid = `{ "radarr": {"url": "http://localhost:7878/", "apikey": "r"}, "sonarr": {"url": "http://localhost:8989", "apikey": "s"}, "sabnzbd": {"url": "https://sab.example.com", "apikey": "n"} }` func TestParseValid(t *testing.T) { cfg, err := Parse([]byte(valid)) if err != nil { t.Fatal(err) } if cfg.Radarr.URL != "http://localhost:7878" { t.Errorf("trailing slash not trimmed: %q", cfg.Radarr.URL) } if cfg.UI.RefreshSeconds != defaultRefreshSeconds { t.Errorf("refresh default = %d", cfg.UI.RefreshSeconds) } } func TestParseErrors(t *testing.T) { tests := map[string]struct { json string want string }{ "unknown field": {`{"radar": {}}`, "unknown field"}, "missing key": {`{"radarr":{"url":"http://a"},"sonarr":{"url":"http://b","apikey":"x"},"sabnzbd":{"url":"http://c","apikey":"x"}}`, "radarr: apikey is required"}, "bad url": {`{"radarr":{"url":"localhost","apikey":"x"},"sonarr":{"url":"http://b","apikey":"x"},"sabnzbd":{"url":"http://c","apikey":"x"}}`, "must be an http(s) URL"}, "invalid json": {`{`, "parsing config"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { _, err := Parse([]byte(tc.json)) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("err = %v, want containing %q", err, tc.want) } }) } } func TestEnvOverride(t *testing.T) { t.Setenv("ARR_TUI_RADARR_APIKEY", "from-env") cfg, err := Parse([]byte(valid)) if err != nil { t.Fatal(err) } if cfg.Radarr.APIKey != "from-env" { t.Errorf("apikey = %q", cfg.Radarr.APIKey) } } func TestLoadPermissionWarning(t *testing.T) { path := filepath.Join(t.TempDir(), "config.json") if err := os.WriteFile(path, []byte(valid), 0o644); err != nil { t.Fatal(err) } _, warnings, err := Load(path) if err != nil { t.Fatal(err) } if len(warnings) != 1 { t.Fatalf("warnings = %v", warnings) } if err := os.Chmod(path, 0o600); err != nil { t.Fatal(err) } if _, warnings, _ = Load(path); len(warnings) != 0 { t.Fatalf("unexpected warnings: %v", warnings) } } func TestShowCovers(t *testing.T) { base := `{"radarr":{"url":"http://r","apikey":"k"},"sonarr":{"url":"http://s","apikey":"k"},"sabnzbd":{"url":"http://a","apikey":"k"}` for _, tc := range []struct { ui string want bool }{ {``, true}, {`,"ui":{}`, true}, {`,"ui":{"covers":true}`, true}, {`,"ui":{"covers":false}`, false}, } { cfg, err := Parse([]byte(base + tc.ui + `}`)) if err != nil { t.Fatal(err) } if got := cfg.UI.ShowCovers(); got != tc.want { t.Errorf("%q: ShowCovers = %v, want %v", tc.ui, got, tc.want) } } } func TestCoverStyleValidation(t *testing.T) { base := `{"radarr":{"url":"http://r","apikey":"k"},"sonarr":{"url":"http://s","apikey":"k"},"sabnzbd":{"url":"http://a","apikey":"k"},"ui":{"cover_style":` for _, ok := range []string{`"auto"`, `"quadrant"`, `"sextant"`, `"cells"`, `"kitty"`, `""`} { if _, err := Parse([]byte(base + ok + `}}`)); err != nil { t.Errorf("%s rejected: %v", ok, err) } } if _, err := Parse([]byte(base + `"sixel"}}`)); err == nil { t.Error("unknown style accepted") } } func TestThemeValidation(t *testing.T) { base := `{"radarr":{"url":"http://r","apikey":"k"},"sonarr":{"url":"http://s","apikey":"k"},"sabnzbd":{"url":"http://a","apikey":"k"},"ui":{"theme":` for _, ok := range []string{`"auto"`, `"dark"`, `"light"`, `""`} { if _, err := Parse([]byte(base + ok + `}}`)); err != nil { t.Errorf("%s rejected: %v", ok, err) } } if _, err := Parse([]byte(base + `"solarized"}}`)); err == nil { t.Error("unknown theme accepted") } } func TestSaveRoundTripAndPermissions(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.json") covers := false cfg := &Config{ Radarr: Arr{Service: Service{URL: "http://r:7878", APIKey: "rk"}, DefaultQualityProfile: "HD-1080p"}, Sonarr: Arr{Service: Service{URL: "http://s:8989", APIKey: "sk"}}, SABnzbd: Service{URL: "http://a:8080", APIKey: "ak"}, UI: UI{RefreshSeconds: 5, Covers: &covers, CoverStyle: "sextant", Bell: true}, } if err := Save(path, cfg); err != nil { t.Fatal(err) } info, err := os.Stat(path) if err != nil || info.Mode().Perm() != 0o600 { t.Fatalf("new file mode = %v, want 0600 (it holds API keys): %v", info.Mode().Perm(), err) } got, err := LoadFile(path) if err != nil { t.Fatal(err) } if got.Radarr.DefaultQualityProfile != "HD-1080p" || got.UI.RefreshSeconds != 5 || got.UI.Covers == nil || *got.UI.Covers || got.UI.CoverStyle != "sextant" || !got.UI.Bell { t.Errorf("round trip lost values: %+v", got) } // Unset optional settings stay out of the file. if data, _ := os.ReadFile(path); strings.Contains(string(data), "notify_downloads") || strings.Contains(string(data), "default_root_folder") { t.Errorf("unset settings written:\n%s", data) } // An existing file keeps its permissions and no temporary file is left behind. if err := os.Chmod(path, 0o640); err != nil { t.Fatal(err) } if err := Save(path, cfg); err != nil { t.Fatal(err) } if info, _ := os.Stat(path); info.Mode().Perm() != 0o640 { t.Errorf("existing mode changed to %v", info.Mode().Perm()) } entries, _ := os.ReadDir(dir) if len(entries) != 1 { t.Errorf("leftover files in the config directory: %v", entries) } } func TestLoadFileIgnoresEnvironmentAndDefaults(t *testing.T) { t.Setenv("ARR_TUI_RADARR_APIKEY", "from-env") path := filepath.Join(t.TempDir(), "config.json") os.WriteFile(path, []byte(`{"radarr":{"url":"http://r/","apikey":"from-file"},"sonarr":{"url":"http://s","apikey":"k"},"sabnzbd":{"url":"http://a","apikey":"k"}}`), 0o600) raw, err := LoadFile(path) if err != nil { t.Fatal(err) } if raw.Radarr.APIKey != "from-file" || raw.Radarr.URL != "http://r/" || raw.UI.RefreshSeconds != 0 { t.Errorf("raw = %+v: environment, trailing slash trimming and defaults must not be applied", raw) } eff, err := raw.Effective(os.Getenv) if err != nil { t.Fatal(err) } if eff.Radarr.APIKey != "from-env" || eff.Radarr.URL != "http://r" || eff.UI.RefreshSeconds != 3 { t.Errorf("effective = %+v", eff) } if raw.Radarr.APIKey != "from-file" { t.Error("Effective must not modify the raw config") } if got := Overrides(os.Getenv); len(got) != 1 || got["radarr.apikey"] != "ARR_TUI_RADARR_APIKEY" { t.Errorf("overrides = %v", got) } } func TestEffectiveValidates(t *testing.T) { raw := &Config{Radarr: Arr{Service: Service{URL: "ftp://x", APIKey: "k"}}} if _, err := raw.Effective(func(string) string { return "" }); err == nil { t.Error("an invalid config must not become effective") } }