Files
arr-tui/internal/config/config_test.go
T
nilsandClaude Opus 5.5 8f162729ea Cover style "halves": left and right half blocks only
Twice the horizontal detail of "cells", for terminals such as macOS
Terminal whose upper and lower half blocks do not meet at the middle of
the cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
2026-10-21 11:38:04 +02:00

191 lines
6.1 KiB
Go

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"`, `"halves"`, `"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 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")
}
}