summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/config/config.go101
-rw-r--r--internal/config/config_test.go84
2 files changed, 165 insertions, 20 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
index b9ae6c2..b8d0532 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -13,8 +13,12 @@ import (
"strings"
)
-// configPath is the location of the optional user config file.
-const configPath = "~/.config/foostore.json"
+const (
+ // configPath is the location of the optional user config file.
+ configPath = "~/.config/foostore.json"
+ // fallbackHomeDirName is used when we cannot resolve a valid home directory.
+ fallbackHomeDirName = "foostore-home"
+)
// Config holds all application-wide configuration values.
// JSON field names use snake_case to match the original geheim.rb Config::DEFAULTS keys.
@@ -31,14 +35,53 @@ type Config struct {
SyncRepos []string `json:"sync_repos"`
}
-// defaultConfig returns a Config populated with built-in defaults.
-// EditCmd honours the $EDITOR environment variable and falls back to "vi"
-// when the variable is unset or empty, so users get their preferred editor
-// automatically without touching the config file.
-// It calls os.UserHomeDir() so that path fields expand correctly at runtime.
-func defaultConfig() Config {
- home, _ := os.UserHomeDir()
+// resolveHomeDir resolves the current user's home directory from OS state.
+// If resolution succeeds with os.UserHomeDir, error is nil.
+// If resolution falls back to HOME or a temp-based directory, an explanatory
+// non-nil error is returned so callers can warn without failing hard.
+func resolveHomeDir() (string, error) {
+ return resolveHomeDirFrom(os.UserHomeDir, os.Getenv("HOME"), os.TempDir())
+}
+
+// resolveHomeDirFrom is a test seam for home directory resolution.
+func resolveHomeDirFrom(userHomeDir func() (string, error), envHome, tempDir string) (string, error) {
+ home, err := userHomeDir()
+ if err == nil && home != "" {
+ return home, nil
+ }
+
+ if envHome != "" && filepath.IsAbs(envHome) {
+ if err != nil {
+ return envHome, fmt.Errorf("os.UserHomeDir failed; using HOME=%q: %w", envHome, err)
+ }
+ return envHome, fmt.Errorf("os.UserHomeDir returned empty home; using HOME=%q", envHome)
+ }
+
+ fallbackHome := filepath.Join(tempDir, fallbackHomeDirName)
+ if envHome != "" && !filepath.IsAbs(envHome) {
+ if err != nil {
+ return fallbackHome, fmt.Errorf("os.UserHomeDir failed; HOME is not absolute (%q), using %q: %w", envHome, fallbackHome, err)
+ }
+ return fallbackHome, fmt.Errorf("os.UserHomeDir returned empty home; HOME is not absolute (%q), using %q", envHome, fallbackHome)
+ }
+
+ if err != nil {
+ return fallbackHome, fmt.Errorf("os.UserHomeDir failed and HOME is unavailable; using %q: %w", fallbackHome, err)
+ }
+ return fallbackHome, fmt.Errorf("os.UserHomeDir returned empty home and HOME is unavailable; using %q", fallbackHome)
+}
+
+// homeDirOrFallback resolves a usable home path and logs fallback reasons.
+func homeDirOrFallback() string {
+ home, err := resolveHomeDir()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
+ }
+ return home
+}
+// defaultConfigWithHome returns built-in defaults using the supplied home path.
+func defaultConfigWithHome(home string) Config {
// Prefer $EDITOR; fall back to vi if not set.
editCmd := os.Getenv("EDITOR")
if editCmd == "" {
@@ -59,22 +102,39 @@ func defaultConfig() Config {
}
}
-// expandTilde replaces a leading "~" in path with the user's home directory.
-// Non-tilde paths and empty strings are returned unchanged.
-func expandTilde(path string) string {
+// defaultConfig returns a Config populated with built-in defaults.
+// EditCmd honours the $EDITOR environment variable and falls back to "vi"
+// when the variable is unset or empty, so users get their preferred editor
+// automatically without touching the config file.
+func defaultConfig() Config {
+ return defaultConfigWithHome(homeDirOrFallback())
+}
+
+// expandTildeWithHome replaces a leading "~" in path with the supplied home.
+func expandTildeWithHome(path, home string) string {
if path == "" || !strings.HasPrefix(path, "~") {
return path
}
- home, _ := os.UserHomeDir()
// Replace only the leading "~"; preserve any subdirectory suffix.
return home + path[1:]
}
+// expandTilde replaces a leading "~" in path with the user's home directory.
+// Non-tilde paths and empty strings are returned unchanged.
+func expandTilde(path string) string {
+ return expandTildeWithHome(path, homeDirOrFallback())
+}
+
+// expandPathFieldsWithHome tilde-expands every path-typed field in cfg in place.
+func expandPathFieldsWithHome(cfg *Config, home string) {
+ cfg.DataDir = expandTildeWithHome(cfg.DataDir, home)
+ cfg.ExportDir = expandTildeWithHome(cfg.ExportDir, home)
+ cfg.KeyFile = expandTildeWithHome(cfg.KeyFile, home)
+}
+
// expandPathFields tilde-expands every path-typed field in cfg in place.
func expandPathFields(cfg *Config) {
- cfg.DataDir = expandTilde(cfg.DataDir)
- cfg.ExportDir = expandTilde(cfg.ExportDir)
- cfg.KeyFile = expandTilde(cfg.KeyFile)
+ expandPathFieldsWithHome(cfg, homeDirOrFallback())
}
// Load reads ~/.config/foostore.json and merges it over the built-in defaults.
@@ -86,8 +146,9 @@ func expandPathFields(cfg *Config) {
// Note: the Ruby reference uses puts (stdout) for this warning; we use stderr
// intentionally because warnings belong on the error stream.
func Load() Config {
- cfg := defaultConfig()
- path := expandTilde(configPath)
+ home := homeDirOrFallback()
+ cfg := defaultConfigWithHome(home)
+ path := expandTildeWithHome(configPath, home)
data, err := os.ReadFile(path)
if err != nil {
@@ -103,12 +164,12 @@ func Load() Config {
// JSON document are overwritten; all others retain their default values.
if err := json.Unmarshal(data, &cfg); err != nil {
fmt.Fprintf(os.Stderr, "Unable to read %s, using defaults! %v\n", path, err)
- return defaultConfig()
+ return defaultConfigWithHome(home)
}
// Tilde-expand path fields that may have been supplied as "~/…" strings
// in the JSON file (defaultConfig() already returns absolute paths, but
// user-supplied values might use "~").
- expandPathFields(&cfg)
+ expandPathFieldsWithHome(&cfg, home)
return cfg
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 96b6a9d..4b2e7d9 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -2,6 +2,7 @@ package config
import (
"bytes"
+ "errors"
"io"
"os"
"path/filepath"
@@ -65,6 +66,89 @@ func TestExpandTilde(t *testing.T) {
}
}
+func TestResolveHomeDirFrom(t *testing.T) {
+ lookupErr := errors.New("lookup failed")
+
+ cases := []struct {
+ name string
+ userHome string
+ userErr error
+ envHome string
+ tempDir string
+ wantHome string
+ wantErrContains string
+ }{
+ {
+ name: "user home success",
+ userHome: "/users/alice",
+ tempDir: "/tmp",
+ wantHome: "/users/alice",
+ },
+ {
+ name: "falls back to absolute HOME when user lookup fails",
+ userErr: lookupErr,
+ envHome: "/env/home",
+ tempDir: "/tmp",
+ wantHome: "/env/home",
+ wantErrContains: "using HOME",
+ },
+ {
+ name: "falls back to absolute HOME when user lookup is empty",
+ envHome: "/env/home",
+ tempDir: "/tmp",
+ wantHome: "/env/home",
+ wantErrContains: "returned empty home",
+ },
+ {
+ name: "relative HOME falls back to temp-based path",
+ userErr: lookupErr,
+ envHome: "relative/home",
+ tempDir: "/tmp/runtime",
+ wantHome: "/tmp/runtime/foostore-home",
+ wantErrContains: "HOME is not absolute",
+ },
+ {
+ name: "missing HOME falls back to temp-based path",
+ userErr: lookupErr,
+ tempDir: "/tmp/runtime",
+ wantHome: "/tmp/runtime/foostore-home",
+ wantErrContains: "HOME is unavailable",
+ },
+ {
+ name: "empty user home and missing HOME falls back to temp-based path",
+ tempDir: "/tmp/runtime",
+ wantHome: "/tmp/runtime/foostore-home",
+ wantErrContains: "returned empty home and HOME is unavailable",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ gotHome, err := resolveHomeDirFrom(
+ func() (string, error) { return tc.userHome, tc.userErr },
+ tc.envHome,
+ tc.tempDir,
+ )
+ if gotHome != tc.wantHome {
+ t.Fatalf("home = %q; want %q", gotHome, tc.wantHome)
+ }
+
+ if tc.wantErrContains == "" {
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return
+ }
+ if err == nil {
+ t.Fatalf("expected error containing %q; got nil", tc.wantErrContains)
+ }
+ if !strings.Contains(err.Error(), tc.wantErrContains) {
+ t.Fatalf("error = %q; want substring %q", err.Error(), tc.wantErrContains)
+ }
+ })
+ }
+}
+
// ---- Load() ----------------------------------------------------------------
// TestLoad_defaults verifies all 10 default values when no config file exists.