Initial project skeleton (M0)

Add SPECS.md, JSON config loader with env overrides and validation,
and an empty tview shell with tabs and status bar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 17:38:15 +02:00
co-authored by Claude Sonnet 5.5
commit 9f05523c02
10 changed files with 693 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
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)
}
}