First start: create the config file and open the Config view
Without a config file arr-tui refused to start. It now creates an empty one (private, with its directory) and opens the Config view. A service with neither address nor key is "not configured", which validation allows: its client returns ErrNotConfigured without a request, its indicator stays grey, it is logged once as information, and its lists say so instead of showing errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
@@ -18,6 +18,24 @@ type Service struct {
|
||||
APIKey string `json:"apikey"`
|
||||
}
|
||||
|
||||
// Configured reports whether the service is set up at all (address or key
|
||||
// given). Validate checks that a configured service has both.
|
||||
func (s Service) Configured() bool { return s.URL != "" || s.APIKey != "" }
|
||||
|
||||
// AnyConfigured reports whether at least one service is set up.
|
||||
func (c *Config) AnyConfigured() bool {
|
||||
return c.Radarr.Configured() || c.Sonarr.Configured() || c.SABnzbd.Configured()
|
||||
}
|
||||
|
||||
// Create writes an empty configuration to path, creating its directory
|
||||
// (private, like the file). Every service is not set up yet.
|
||||
func Create(path string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fmt.Errorf("creating config directory: %w", err)
|
||||
}
|
||||
return Save(path, &Config{})
|
||||
}
|
||||
|
||||
// Arr is a Radarr or Sonarr instance, with optional defaults for the add form.
|
||||
type Arr struct {
|
||||
Service
|
||||
@@ -246,7 +264,9 @@ func (c *Config) applyDefaults() {
|
||||
c.SABnzbd.URL = strings.TrimRight(c.SABnzbd.URL, "/")
|
||||
}
|
||||
|
||||
// Validate checks that every service has a usable URL and API key.
|
||||
// Validate checks that every service that is set up has a usable URL and API
|
||||
// key. A service with neither is not set up, which is allowed: the program
|
||||
// starts without it and says so, and the Config view can fill it in.
|
||||
func (c *Config) Validate() error {
|
||||
var errs []error
|
||||
for _, s := range []struct {
|
||||
@@ -257,6 +277,9 @@ func (c *Config) Validate() error {
|
||||
{"sonarr", c.Sonarr.Service},
|
||||
{"sabnzbd", c.SABnzbd},
|
||||
} {
|
||||
if !s.svc.Configured() {
|
||||
continue // not set up (yet); the program says so where it is needed
|
||||
}
|
||||
if s.svc.APIKey == "" {
|
||||
errs = append(errs, fmt.Errorf("%s: apikey is required", s.name))
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ func TestParseErrors(t *testing.T) {
|
||||
"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"},
|
||||
"key only": {`{"radarr":{"apikey":"x"}}`, "radarr: url is required"},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
@@ -200,3 +201,35 @@ func TestEffectiveValidates(t *testing.T) {
|
||||
t.Error("an invalid config must not become effective")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicesMayBeLeftOut(t *testing.T) {
|
||||
cfg, err := Parse([]byte(`{"radarr":{"url":"http://r","apikey":"k"}}`))
|
||||
if err != nil {
|
||||
t.Fatalf("a config with only Radarr must load: %v", err)
|
||||
}
|
||||
if !cfg.Radarr.Configured() || cfg.Sonarr.Configured() || cfg.SABnzbd.Configured() || !cfg.AnyConfigured() {
|
||||
t.Errorf("configured: radarr %v sonarr %v sab %v", cfg.Radarr.Configured(), cfg.Sonarr.Configured(), cfg.SABnzbd.Configured())
|
||||
}
|
||||
empty, err := Parse([]byte(`{}`))
|
||||
if err != nil || empty.AnyConfigured() {
|
||||
t.Errorf("an empty config must load with nothing set up: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "new", "dir", "config.json")
|
||||
if err := Create(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("file: %v %v", info, err)
|
||||
}
|
||||
if dir, _ := os.Stat(filepath.Dir(path)); dir.Mode().Perm() != 0o700 {
|
||||
t.Errorf("directory mode %v", dir.Mode().Perm())
|
||||
}
|
||||
cfg, warnings, err := Load(path)
|
||||
if err != nil || len(warnings) > 0 || cfg.AnyConfigured() {
|
||||
t.Errorf("the new file must load cleanly: %v %v", warnings, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user