summaryrefslogtreecommitdiff
path: root/internal/config/config_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config/config_test.go')
-rw-r--r--internal/config/config_test.go228
1 files changed, 228 insertions, 0 deletions
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
new file mode 100644
index 0000000..a443ce0
--- /dev/null
+++ b/internal/config/config_test.go
@@ -0,0 +1,228 @@
+package config
+
+import (
+ "bytes"
+ "io"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// homeDir is a test helper that returns the current user's home directory,
+// panicking if it cannot be determined (should never happen in tests).
+func homeDir(t *testing.T) string {
+ t.Helper()
+ home, err := os.UserHomeDir()
+ if err != nil {
+ t.Fatalf("os.UserHomeDir: %v", err)
+ }
+ return home
+}
+
+// writeConfigFile creates a temporary directory, writes content to
+// geheim.json inside it, and returns the file path.
+func writeConfigFile(t *testing.T, content string) string {
+ t.Helper()
+ dir := t.TempDir()
+ path := filepath.Join(dir, "geheim.json")
+ if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
+ t.Fatalf("WriteFile: %v", err)
+ }
+ return path
+}
+
+// loadFromPath replicates the Load() merge logic against an arbitrary file
+// path instead of the real ~/.config/geheim.json, so tests don't need to
+// redirect HOME or touch the real user config.
+func loadFromPath(path string) Config {
+ cfg := defaultConfig()
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return cfg
+ }
+ if err := json.Unmarshal(data, &cfg); err != nil {
+ return defaultConfig()
+ }
+ expandPathFields(&cfg)
+ return cfg
+}
+
+// ---- tests ------------------------------------------------------------------
+
+// TestExpandTilde verifies all three cases: tilde prefix, no tilde, empty string.
+func TestExpandTilde(t *testing.T) {
+ home := homeDir(t)
+
+ cases := []struct {
+ name string
+ input string
+ want string
+ }{
+ {"tilde only", "~", home},
+ {"tilde with subpath", "~/foo/bar", home + "/foo/bar"},
+ {"absolute path unchanged", "/etc/passwd", "/etc/passwd"},
+ {"relative path unchanged", "relative/path", "relative/path"},
+ {"empty string unchanged", "", ""},
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ got := expandTilde(tc.input)
+ if got != tc.want {
+ t.Errorf("expandTilde(%q) = %q; want %q", tc.input, got, tc.want)
+ }
+ })
+ }
+}
+
+// TestLoad_defaults verifies that Load() returns fully-expanded default values
+// when the config file does not exist. HOME is redirected to a temp dir so
+// Load() looks for a config file that is guaranteed not to exist.
+func TestLoad_defaults(t *testing.T) {
+ dir := t.TempDir()
+ t.Setenv("HOME", dir)
+
+ cfg := Load()
+ home := dir // HOME was redirected, defaultConfig() uses the new value
+
+ if cfg.DataDir != filepath.Join(home, "git", "geheimlager") {
+ t.Errorf("DataDir = %q; want %q", cfg.DataDir, filepath.Join(home, "git", "geheimlager"))
+ }
+ if cfg.ExportDir != filepath.Join(home, ".geheimlagerexport") {
+ t.Errorf("ExportDir = %q; want %q", cfg.ExportDir, filepath.Join(home, ".geheimlagerexport"))
+ }
+ if cfg.KeyFile != filepath.Join(home, ".geheimlager.key") {
+ t.Errorf("KeyFile = %q; want %q", cfg.KeyFile, filepath.Join(home, ".geheimlager.key"))
+ }
+ if cfg.KeyLength != 32 {
+ t.Errorf("KeyLength = %d; want 32", cfg.KeyLength)
+ }
+ if cfg.EncAlg != "AES-256-CBC" {
+ t.Errorf("EncAlg = %q; want AES-256-CBC", cfg.EncAlg)
+ }
+ if cfg.AddToIV != "Hello world" {
+ t.Errorf("AddToIV = %q; want 'Hello world'", cfg.AddToIV)
+ }
+ if cfg.EditCmd != "hx" {
+ t.Errorf("EditCmd = %q; want hx", cfg.EditCmd)
+ }
+ if cfg.GnomeClipboardCmd != "gpaste-client" {
+ t.Errorf("GnomeClipboardCmd = %q; want gpaste-client", cfg.GnomeClipboardCmd)
+ }
+ if cfg.MacOSClipboardCmd != "pbcopy" {
+ t.Errorf("MacOSClipboardCmd = %q; want pbcopy", cfg.MacOSClipboardCmd)
+ }
+ if len(cfg.SyncRepos) != 2 || cfg.SyncRepos[0] != "git1" || cfg.SyncRepos[1] != "git2" {
+ t.Errorf("SyncRepos = %v; want [git1 git2]", cfg.SyncRepos)
+ }
+}
+
+// TestLoad_override verifies that fields present in the JSON file override
+// defaults while absent fields retain their default values.
+func TestLoad_override(t *testing.T) {
+ jsonContent := `{"edit_cmd":"nvim","key_length":64,"sync_repos":["github","gitlab"]}`
+ path := writeConfigFile(t, jsonContent)
+
+ cfg := loadFromPath(path)
+
+ // Overridden fields.
+ if cfg.EditCmd != "nvim" {
+ t.Errorf("EditCmd = %q; want nvim", cfg.EditCmd)
+ }
+ if cfg.KeyLength != 64 {
+ t.Errorf("KeyLength = %d; want 64", cfg.KeyLength)
+ }
+ if len(cfg.SyncRepos) != 2 || cfg.SyncRepos[0] != "github" || cfg.SyncRepos[1] != "gitlab" {
+ t.Errorf("SyncRepos = %v; want [github gitlab]", cfg.SyncRepos)
+ }
+
+ // Non-overridden fields must keep their defaults.
+ home := homeDir(t)
+ if cfg.EncAlg != "AES-256-CBC" {
+ t.Errorf("EncAlg = %q; want AES-256-CBC", cfg.EncAlg)
+ }
+ if cfg.DataDir != filepath.Join(home, "git", "geheimlager") {
+ t.Errorf("DataDir = %q; want default", cfg.DataDir)
+ }
+}
+
+// TestLoad_pathOverride verifies that tilde paths supplied via JSON are
+// expanded to absolute paths after loading.
+func TestLoad_pathOverride(t *testing.T) {
+ home := homeDir(t)
+ jsonContent := `{"data_dir":"~/custom/vault"}`
+ path := writeConfigFile(t, jsonContent)
+
+ cfg := loadFromPath(path)
+
+ want := filepath.Join(home, "custom", "vault")
+ if cfg.DataDir != want {
+ t.Errorf("DataDir = %q; want %q", cfg.DataDir, want)
+ }
+}
+
+// TestLoad_invalid_json verifies that invalid JSON causes Load() to return
+// defaults and print a warning to stderr.
+func TestLoad_invalid_json(t *testing.T) {
+ // Write invalid JSON to the expected config location inside a temp HOME.
+ dir := t.TempDir()
+ cfgDir := filepath.Join(dir, ".config")
+ if err := os.MkdirAll(cfgDir, 0o755); err != nil {
+ t.Fatalf("MkdirAll: %v", err)
+ }
+ badJSON := filepath.Join(cfgDir, "geheim.json")
+ if err := os.WriteFile(badJSON, []byte("{invalid json}"), 0o600); err != nil {
+ t.Fatalf("WriteFile: %v", err)
+ }
+
+ // Capture stderr to verify the warning message is emitted.
+ origStderr := os.Stderr
+ r, w, _ := os.Pipe()
+ os.Stderr = w
+ t.Setenv("HOME", dir)
+
+ cfg := Load()
+
+ w.Close()
+ os.Stderr = origStderr
+ var buf bytes.Buffer
+ _, _ = io.Copy(&buf, r)
+ stderrOutput := buf.String()
+
+ if !strings.Contains(stderrOutput, "Unable to read") {
+ t.Errorf("expected warning on stderr, got: %q", stderrOutput)
+ }
+
+ // Returned config must equal defaults (with the redirected HOME).
+ if cfg.EditCmd != "hx" {
+ t.Errorf("EditCmd = %q; want hx (default)", cfg.EditCmd)
+ }
+ if cfg.KeyLength != 32 {
+ t.Errorf("KeyLength = %d; want 32 (default)", cfg.KeyLength)
+ }
+}
+
+// TestLoad_missing_file_no_warning verifies that a missing config file does
+// NOT produce a warning on stderr — absence of the file is a normal condition
+// (first run or unconfigured installation).
+func TestLoad_missing_file_no_warning(t *testing.T) {
+ dir := t.TempDir() // no geheim.json inside
+
+ origStderr := os.Stderr
+ r, w, _ := os.Pipe()
+ os.Stderr = w
+ t.Setenv("HOME", dir)
+
+ _ = Load()
+
+ w.Close()
+ os.Stderr = origStderr
+ var buf bytes.Buffer
+ _, _ = io.Copy(&buf, r)
+
+ if buf.Len() != 0 {
+ t.Errorf("expected no stderr output for missing file, got: %q", buf.String())
+ }
+}