config_test.go

  1package config
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"reflect"
  7	"strings"
  8	"testing"
  9)
 10
 11func TestResolve(t *testing.T) {
 12	tmpDir := t.TempDir()
 13	homeDir := filepath.Join(tmpDir, "home")
 14	if err := os.MkdirAll(homeDir, 0o755); err != nil {
 15		t.Fatalf("creating temp HOME: %v", err)
 16	}
 17
 18	configPath := filepath.Join(tmpDir, "config.toml")
 19	if err := os.WriteFile(configPath, []byte(resolveFixtureTOML), 0o600); err != nil {
 20		t.Fatalf("writing fixture config: %v", err)
 21	}
 22
 23	t.Setenv("HOME", homeDir)
 24	t.Setenv("KELD_CONFIG_FILE", configPath)
 25	t.Setenv("KELD_CONFIG_PATHS", "")
 26
 27	tests := []struct {
 28		name         string
 29		preset       string
 30		command      string
 31		overrides    map[string][]string
 32		wantCommand  string
 33		wantWorkdir  string
 34		wantArgs     []string
 35		wantSections []string
 36		wantEnviron  map[string]string
 37		wantFlags    map[string][]string
 38	}{
 39		{
 40			name:         "global command only",
 41			preset:       "",
 42			command:      "backup",
 43			wantCommand:  "backup",
 44			wantWorkdir:  "",
 45			wantArgs:     nil,
 46			wantSections: []string{"global", "global.backup"},
 47			wantEnviron: map[string]string{
 48				"RESTIC_REPOSITORY":       "/repos/global",
 49				"RESTIC_PASSWORD_COMMAND": "pass global",
 50			},
 51			wantFlags: map[string][]string{
 52				"--password-file":      {"/secrets/global"},
 53				"--verbose":            {""},
 54				"--tag":                {"global-one", "global-two"},
 55				"--exclude-file":       {"/etc/restic/global-excludes"},
 56				"--exclude-if-present": {".nobackup"},
 57				"--cache-dir":          {"/srv/cache"},
 58			},
 59		},
 60		{
 61			name:         "preset command merge with special keys",
 62			preset:       "home",
 63			command:      "backup",
 64			wantCommand:  "backup",
 65			wantWorkdir:  filepath.Join(homeDir, "work", "home"),
 66			wantArgs:     []string{"/home/alice", "/home/shared"},
 67			wantSections: []string{"global", "global.backup", "home", "home.backup"},
 68			wantEnviron: map[string]string{
 69				"RESTIC_REPOSITORY":       "/repos/global",
 70				"RESTIC_PASSWORD_COMMAND": "pass home",
 71			},
 72			wantFlags: map[string][]string{
 73				"--password-file":      {"/secrets/home"},
 74				"--verbose":            {""},
 75				"--tag":                {"home-tag"},
 76				"--exclude-file":       {"/etc/restic/global-excludes"},
 77				"--exclude-if-present": {".nobackup"},
 78				"--cache-dir":          {"/srv/cache"},
 79				"--repo":               {"/repos/home"},
 80			},
 81		},
 82		{
 83			name:         "split preset sections",
 84			preset:       "home@cloud",
 85			command:      "backup",
 86			wantCommand:  "backup",
 87			wantWorkdir:  "",
 88			wantArgs:     []string{"/data/cloud"},
 89			wantSections: []string{"global", "global.backup", "@cloud", "home@", "home@.backup", "home@cloud", "home@cloud.backup"},
 90			wantEnviron: map[string]string{
 91				"RESTIC_REPOSITORY":       "/repos/cloud",
 92				"RESTIC_PASSWORD_COMMAND": "pass cloud backup",
 93			},
 94			wantFlags: map[string][]string{
 95				"--password-file":      {"/secrets/global"},
 96				"--verbose":            {""},
 97				"--tag":                {"cloud-tag"},
 98				"--exclude-file":       {"/etc/restic/home-split-excludes"},
 99				"--exclude-if-present": {".nobackup"},
100				"--cache-dir":          {"/srv/cache"},
101				"--repo":               {"/repos/home-cloud"},
102			},
103		},
104		{
105			name:         "command alias",
106			preset:       "archive",
107			command:      "backup",
108			wantCommand:  "snapshots",
109			wantWorkdir:  "",
110			wantArgs:     []string{"latest"},
111			wantSections: []string{"global", "global.backup", "archive", "archive.backup"},
112			wantEnviron: map[string]string{
113				"RESTIC_REPOSITORY":       "/repos/global",
114				"RESTIC_PASSWORD_COMMAND": "pass global",
115			},
116			wantFlags: map[string][]string{
117				"--password-file":      {"/secrets/global"},
118				"--verbose":            {""},
119				"--tag":                {"global-one", "global-two"},
120				"--exclude-file":       {"/etc/restic/global-excludes"},
121				"--exclude-if-present": {".nobackup"},
122				"--cache-dir":          {"/srv/cache"},
123				"--json":               {""},
124			},
125		},
126		{
127			name:    "cli overrides take precedence",
128			preset:  "home",
129			command: "backup",
130			overrides: map[string][]string{
131				"tag":                {"cli-a", "cli-b"},
132				"password-file":      {"/secrets/cli"},
133				"repo":               {"/repos/cli"},
134				"json":               nil,
135				"_arguments":         {"/cli/path"},
136				"_workdir":           {"~/work/cli"},
137				"_command":           {"backup"},
138				"exclude-if-present": {".override-marker"},
139			},
140			wantCommand:  "backup",
141			wantWorkdir:  filepath.Join(homeDir, "work", "cli"),
142			wantArgs:     []string{"/cli/path"},
143			wantSections: []string{"global", "global.backup", "home", "home.backup"},
144			wantEnviron: map[string]string{
145				"RESTIC_REPOSITORY":       "/repos/global",
146				"RESTIC_PASSWORD_COMMAND": "pass home",
147			},
148			wantFlags: map[string][]string{
149				"--password-file":      {"/secrets/cli"},
150				"--verbose":            {""},
151				"--tag":                {"cli-a", "cli-b"},
152				"--exclude-file":       {"/etc/restic/global-excludes"},
153				"--exclude-if-present": {".override-marker"},
154				"--cache-dir":          {"/srv/cache"},
155				"--repo":               {"/repos/cli"},
156				"--json":               {""},
157			},
158		},
159	}
160
161	for _, tt := range tests {
162		tt := tt
163		t.Run(tt.name, func(t *testing.T) {
164			cfg, err := Resolve(tt.preset, tt.command, tt.overrides)
165			if err != nil {
166				t.Fatalf("Resolve() error: %v", err)
167			}
168
169			if cfg.Command != tt.wantCommand {
170				t.Fatalf("command mismatch: got %q, want %q", cfg.Command, tt.wantCommand)
171			}
172			if cfg.Workdir != tt.wantWorkdir {
173				t.Fatalf("workdir mismatch: got %q, want %q", cfg.Workdir, tt.wantWorkdir)
174			}
175			if !equalStrings(cfg.Arguments, tt.wantArgs) {
176				t.Fatalf("arguments mismatch: got %#v, want %#v", cfg.Arguments, tt.wantArgs)
177			}
178			if !reflect.DeepEqual(cfg.SectionsRead, tt.wantSections) {
179				t.Fatalf("sections mismatch: got %#v, want %#v", cfg.SectionsRead, tt.wantSections)
180			}
181			if !reflect.DeepEqual(cfg.Environ, tt.wantEnviron) {
182				t.Fatalf("environ mismatch: got %#v, want %#v", cfg.Environ, tt.wantEnviron)
183			}
184
185			for _, f := range cfg.Flags {
186				if !strings.HasPrefix(f.Name, "-") {
187					t.Fatalf("flag name %q is missing CLI prefix", f.Name)
188				}
189			}
190
191			gotFlags := collectFlags(cfg.Flags)
192			if !reflect.DeepEqual(gotFlags, tt.wantFlags) {
193				t.Fatalf("flags mismatch: got %#v, want %#v", gotFlags, tt.wantFlags)
194			}
195		})
196	}
197}
198
199func collectFlags(flags []Flag) map[string][]string {
200	out := make(map[string][]string)
201	for _, flag := range flags {
202		out[flag.Name] = append(out[flag.Name], flag.Value)
203	}
204	return out
205}
206
207func equalStrings(a, b []string) bool {
208	if len(a) == 0 && len(b) == 0 {
209		return true
210	}
211	return reflect.DeepEqual(a, b)
212}
213
214const resolveFixtureTOML = `
215[vars]
216cache-root = "/srv"
217
218[global]
219password-file = "/secrets/global"
220verbose = true
221tag = ["global-one", "global-two"]
222
223[global.backup]
224exclude-file = "/etc/restic/global-excludes"
225exclude-if-present = ".nobackup"
226cache-dir = "${vars.cache-root}/cache"
227
228[global.backup.environ]
229RESTIC_REPOSITORY = "/repos/global"
230RESTIC_PASSWORD_COMMAND = "pass global"
231
232[home]
233repo = "/repos/home"
234_workdir = "~/work/home"
235tag = ["home-tag"]
236
237[home.backup]
238_arguments = ["/home/alice", "/home/shared"]
239password-file = "/secrets/home"
240
241[home.backup.environ]
242RESTIC_PASSWORD_COMMAND = "pass home"
243
244["@cloud"]
245repo = "/repos/cloud-base"
246tag = ["cloud-tag"]
247
248["@cloud".environ]
249RESTIC_REPOSITORY = "/repos/cloud"
250
251["home@"]
252repo = "/repos/home-prefix"
253
254["home@".backup]
255exclude-file = "/etc/restic/home-split-excludes"
256
257["home@".forget]
258keep-last = 7
259
260["home@cloud"]
261repo = "/repos/home-cloud"
262
263["home@cloud".backup]
264_arguments = ["/data/cloud"]
265
266["home@cloud".backup.environ]
267RESTIC_PASSWORD_COMMAND = "pass cloud backup"
268
269[archive.backup]
270_command = "snapshots"
271_arguments = ["latest"]
272json = true
273`