1package restic
2
3import (
4 "errors"
5 "testing"
6
7 "git.secluded.site/keld/internal/config"
8)
9
10func TestBuildSnapshotCmd(t *testing.T) {
11 t.Parallel()
12
13 tests := []struct {
14 name string
15 cfg *config.ResolvedConfig
16 wantArgv []string
17 wantErr bool
18 }{
19 {
20 name: "basic repo flag",
21 cfg: &config.ResolvedConfig{
22 Command: "restore",
23 Flags: []config.Flag{
24 {Name: "--repo", Value: "/srv/backup"},
25 {Name: "--target", Value: "/tmp/restore"},
26 },
27 },
28 wantArgv: []string{"restic", "snapshots", "--json", "--repo", "/srv/backup"},
29 },
30 {
31 name: "repo with password-file",
32 cfg: &config.ResolvedConfig{
33 Command: "restore",
34 Flags: []config.Flag{
35 {Name: "--repo", Value: "rclone:remote:/backup"},
36 {Name: "--password-file", Value: "/etc/restic/pw"},
37 {Name: "--target", Value: "/tmp/restore"},
38 {Name: "--overwrite", Value: "if-changed"},
39 },
40 },
41 wantArgv: []string{
42 "restic", "snapshots", "--json",
43 "--repo", "rclone:remote:/backup",
44 "--password-file", "/etc/restic/pw",
45 },
46 },
47 {
48 name: "repo with cache-dir and no-lock",
49 cfg: &config.ResolvedConfig{
50 Command: "restore",
51 Flags: []config.Flag{
52 {Name: "--repo", Value: "/srv/backup"},
53 {Name: "--cache-dir", Value: "/tmp/cache"},
54 {Name: "--no-lock"},
55 {Name: "--target", Value: "/tmp/restore"},
56 },
57 },
58 wantArgv: []string{
59 "restic", "snapshots", "--json",
60 "--repo", "/srv/backup",
61 "--cache-dir", "/tmp/cache",
62 "--no-lock",
63 },
64 },
65 {
66 name: "repo via repository-file",
67 cfg: &config.ResolvedConfig{
68 Command: "restore",
69 Flags: []config.Flag{
70 {Name: "--repository-file", Value: "/etc/restic/repo"},
71 {Name: "--target", Value: "/tmp/restore"},
72 },
73 },
74 wantArgv: []string{
75 "restic", "snapshots", "--json",
76 "--repository-file", "/etc/restic/repo",
77 },
78 },
79 {
80 name: "multiple connection flags preserved",
81 cfg: &config.ResolvedConfig{
82 Command: "restore",
83 Flags: []config.Flag{
84 {Name: "--repo", Value: "s3:https://bucket/backup"},
85 {Name: "--cacert", Value: "/etc/ssl/custom.pem"},
86 {Name: "--insecure-tls"},
87 {Name: "--limit-download", Value: "1024"},
88 {Name: "--tls-client-cert", Value: "/etc/ssl/client.pem"},
89 {Name: "--key-hint", Value: "abc123"},
90 {Name: "--target", Value: "/tmp/restore"},
91 {Name: "--exclude", Value: "*.log"},
92 },
93 },
94 wantArgv: []string{
95 "restic", "snapshots", "--json",
96 "--repo", "s3:https://bucket/backup",
97 "--cacert", "/etc/ssl/custom.pem",
98 "--insecure-tls",
99 "--limit-download", "1024",
100 "--tls-client-cert", "/etc/ssl/client.pem",
101 "--key-hint", "abc123",
102 },
103 },
104 {
105 name: "repo from environ only",
106 cfg: &config.ResolvedConfig{
107 Command: "restore",
108 Flags: []config.Flag{
109 {Name: "--target", Value: "/tmp/restore"},
110 },
111 Environ: map[string]string{
112 "RESTIC_REPOSITORY": "/srv/backup",
113 },
114 },
115 // No --repo flag needed; restic reads the env var directly.
116 wantArgv: []string{"restic", "snapshots", "--json"},
117 },
118 {
119 name: "password-command flag preserved",
120 cfg: &config.ResolvedConfig{
121 Command: "restore",
122 Flags: []config.Flag{
123 {Name: "--repo", Value: "/srv/backup"},
124 {Name: "--password-command", Value: "op read 'op://Vault/Backup/password'"},
125 {Name: "--target", Value: "/tmp/restore"},
126 },
127 },
128 wantArgv: []string{
129 "restic", "snapshots", "--json",
130 "--repo", "/srv/backup",
131 "--password-command", "op read 'op://Vault/Backup/password'",
132 },
133 },
134 }
135
136 for _, tt := range tests {
137 t.Run(tt.name, func(t *testing.T) {
138 t.Parallel()
139
140 argv, err := buildSnapshotCmd(tt.cfg)
141 if (err != nil) != tt.wantErr {
142 t.Fatalf("buildSnapshotCmd(): err=%v, wantErr=%v", err, tt.wantErr)
143 }
144 if err != nil {
145 return
146 }
147
148 if len(argv) != len(tt.wantArgv) {
149 t.Fatalf("argv length: got %d %v, want %d %v",
150 len(argv), argv, len(tt.wantArgv), tt.wantArgv)
151 }
152 for i := range argv {
153 if argv[i] != tt.wantArgv[i] {
154 t.Errorf("argv[%d]: got %q, want %q", i, argv[i], tt.wantArgv[i])
155 }
156 }
157 })
158 }
159}
160
161func TestBuildSnapshotCmdNoRepoSentinel(t *testing.T) {
162 // Not parallel: uses t.Setenv to control process environment.
163 t.Setenv("RESTIC_REPOSITORY", "")
164 t.Setenv("RESTIC_REPOSITORY_FILE", "")
165
166 cfg := &config.ResolvedConfig{
167 Command: "restore",
168 Flags: []config.Flag{
169 {Name: "--target", Value: "/tmp/restore"},
170 },
171 }
172
173 _, err := buildSnapshotCmd(cfg)
174 if !errors.Is(err, ErrNoRepo) {
175 t.Errorf("expected ErrNoRepo, got %v", err)
176 }
177}
178
179func TestBuildSnapshotCmdProcessEnv(t *testing.T) {
180 // Not parallel: uses t.Setenv to control process environment.
181 t.Setenv("RESTIC_REPOSITORY", "/srv/from-env")
182 t.Setenv("RESTIC_REPOSITORY_FILE", "")
183
184 cfg := &config.ResolvedConfig{
185 Command: "restore",
186 Flags: []config.Flag{
187 {Name: "--target", Value: "/tmp/restore"},
188 },
189 }
190
191 argv, err := buildSnapshotCmd(cfg)
192 if err != nil {
193 t.Fatalf("unexpected error: %v", err)
194 }
195
196 // No --repo flag in argv since repo comes from process env;
197 // restic reads it directly.
198 wantArgv := []string{"restic", "snapshots", "--json"}
199 if len(argv) != len(wantArgv) {
200 t.Fatalf("argv: got %v, want %v", argv, wantArgv)
201 }
202 for i := range argv {
203 if argv[i] != wantArgv[i] {
204 t.Errorf("argv[%d]: got %q, want %q", i, argv[i], wantArgv[i])
205 }
206 }
207}
208
209func TestCopyEnviron(t *testing.T) {
210 t.Parallel()
211
212 original := map[string]string{
213 "RESTIC_REPOSITORY": "/srv/backup",
214 "RESTIC_PASSWORD_COMMAND": "op read 'op://Vault/pw'",
215 "AWS_ACCESS_KEY_ID_COMMAND": "op read 'op://Vault/aws-key'",
216 }
217
218 copied := copyEnviron(original)
219
220 // Mutation of the copy must not affect the original.
221 copied["RESTIC_REPOSITORY"] = "changed"
222 delete(copied, "RESTIC_PASSWORD_COMMAND")
223 copied["NEW_KEY"] = "new_value"
224
225 if original["RESTIC_REPOSITORY"] != "/srv/backup" {
226 t.Error("original RESTIC_REPOSITORY was mutated")
227 }
228 if _, ok := original["RESTIC_PASSWORD_COMMAND"]; !ok {
229 t.Error("original RESTIC_PASSWORD_COMMAND was deleted")
230 }
231 if _, ok := original["NEW_KEY"]; ok {
232 t.Error("original gained NEW_KEY from copy")
233 }
234}