1package restic
2
3import (
4 "testing"
5 "time"
6)
7
8// Embed test JSON as string constants so tests are self-contained and
9// don't depend on external files.
10
11const snapshotFixtureNormal = `[
12 {
13 "time": "2026-03-15T10:30:00.123456789+01:00",
14 "parent": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
15 "tree": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
16 "paths": ["/home/user"],
17 "hostname": "myhost",
18 "username": "user",
19 "uid": 1000,
20 "gid": 1000,
21 "excludes": ["*.tmp"],
22 "tags": ["daily", "important"],
23 "program_version": "restic 0.18.1",
24 "id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
25 "short_id": "a1b2c3d4"
26 },
27 {
28 "time": "2026-03-14T08:00:00Z",
29 "tree": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
30 "paths": ["/home/user", "/etc"],
31 "hostname": "server",
32 "username": "root",
33 "uid": 0,
34 "gid": 0,
35 "program_version": "restic 0.18.1",
36 "id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3",
37 "short_id": "b2c3d4e5"
38 }
39]`
40
41const snapshotFixtureEmpty = `[]`
42
43const snapshotFixtureSingleNoTags = `[
44 {
45 "time": "2026-01-01T00:00:00Z",
46 "tree": "0000000000000000000000000000000000000000000000000000000000000000",
47 "paths": ["/srv/data"],
48 "hostname": "backup-host",
49 "username": "backup",
50 "uid": 1001,
51 "gid": 1001,
52 "program_version": "restic 0.17.0",
53 "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4",
54 "short_id": "c3d4e5f6"
55 }
56]`
57
58const snapshotFixtureManyPaths = `[
59 {
60 "time": "2026-06-15T14:22:33Z",
61 "tree": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
62 "paths": ["/home/user/Documents", "/home/user/Music", "/home/user/Pictures", "/home/user/Videos"],
63 "hostname": "workstation",
64 "username": "user",
65 "uid": 1000,
66 "gid": 1000,
67 "tags": ["weekly"],
68 "program_version": "restic 0.18.1",
69 "id": "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5",
70 "short_id": "d4e5f6a7"
71 }
72]`
73
74func TestParseSnapshots(t *testing.T) {
75 t.Parallel()
76
77 tests := []struct {
78 name string
79 json string
80 wantCount int
81 wantErr bool
82 }{
83 {
84 name: "normal with two snapshots",
85 json: snapshotFixtureNormal,
86 wantCount: 2,
87 wantErr: false,
88 },
89 {
90 name: "empty array",
91 json: snapshotFixtureEmpty,
92 wantCount: 0,
93 wantErr: false,
94 },
95 {
96 name: "single snapshot without tags",
97 json: snapshotFixtureSingleNoTags,
98 wantCount: 1,
99 wantErr: false,
100 },
101 {
102 name: "snapshot with many paths",
103 json: snapshotFixtureManyPaths,
104 wantCount: 1,
105 wantErr: false,
106 },
107 {
108 name: "invalid JSON",
109 json: `{not valid json`,
110 wantCount: 0,
111 wantErr: true,
112 },
113 {
114 name: "null",
115 json: `null`,
116 wantCount: 0,
117 wantErr: false,
118 },
119 }
120
121 for _, tt := range tests {
122 tt := tt
123 t.Run(tt.name, func(t *testing.T) {
124 t.Parallel()
125
126 snapshots, err := ParseSnapshots([]byte(tt.json))
127 if (err != nil) != tt.wantErr {
128 t.Fatalf("ParseSnapshots(): err=%v, wantErr=%v", err, tt.wantErr)
129 }
130 if err != nil {
131 return
132 }
133 if len(snapshots) != tt.wantCount {
134 t.Fatalf("ParseSnapshots(): got %d snapshots, want %d", len(snapshots), tt.wantCount)
135 }
136 })
137 }
138}
139
140func TestParseSnapshotsFields(t *testing.T) {
141 t.Parallel()
142
143 snapshots, err := ParseSnapshots([]byte(snapshotFixtureNormal))
144 if err != nil {
145 t.Fatalf("ParseSnapshots(): unexpected error: %v", err)
146 }
147 if len(snapshots) != 2 {
148 t.Fatalf("expected 2 snapshots, got %d", len(snapshots))
149 }
150
151 // First snapshot: has tags, single path, has parent.
152 s := snapshots[0]
153 if s.ID != "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" {
154 t.Errorf("ID = %q", s.ID)
155 }
156 if s.ShortID != "a1b2c3d4" {
157 t.Errorf("ShortID = %q", s.ShortID)
158 }
159 if s.Hostname != "myhost" {
160 t.Errorf("Hostname = %q", s.Hostname)
161 }
162 if len(s.Paths) != 1 || s.Paths[0] != "/home/user" {
163 t.Errorf("Paths = %v", s.Paths)
164 }
165 if len(s.Tags) != 2 || s.Tags[0] != "daily" || s.Tags[1] != "important" {
166 t.Errorf("Tags = %v", s.Tags)
167 }
168 expectedTime := time.Date(2026, 3, 15, 10, 30, 0, 123456789, time.FixedZone("", 3600))
169 if !s.Time.Equal(expectedTime) {
170 t.Errorf("Time = %v, want %v", s.Time, expectedTime)
171 }
172
173 // Second snapshot: no tags, no parent, multiple paths.
174 s2 := snapshots[1]
175 if s2.ShortID != "b2c3d4e5" {
176 t.Errorf("ShortID = %q", s2.ShortID)
177 }
178 if s2.Hostname != "server" {
179 t.Errorf("Hostname = %q", s2.Hostname)
180 }
181 if len(s2.Paths) != 2 {
182 t.Errorf("Paths = %v", s2.Paths)
183 }
184 if len(s2.Tags) != 0 {
185 t.Errorf("Tags = %v, want empty", s2.Tags)
186 }
187}
188
189func TestFormatSnapshotLine(t *testing.T) {
190 t.Parallel()
191
192 tests := []struct {
193 name string
194 snapshot Snapshot
195 want string
196 }{
197 {
198 name: "full snapshot with tags",
199 snapshot: Snapshot{
200 ShortID: "a1b2c3d4",
201 Time: time.Date(2026, 3, 15, 10, 30, 0, 0, time.UTC),
202 Hostname: "myhost",
203 Paths: []string{"/home/user"},
204 Tags: []string{"daily", "important"},
205 },
206 want: "a1b2c3d4 2026-03-15 10:30 myhost /home/user [daily, important]",
207 },
208 {
209 name: "no tags",
210 snapshot: Snapshot{
211 ShortID: "b2c3d4e5",
212 Time: time.Date(2026, 3, 14, 8, 0, 0, 0, time.UTC),
213 Hostname: "server",
214 Paths: []string{"/etc"},
215 },
216 want: "b2c3d4e5 2026-03-14 08:00 server /etc",
217 },
218 {
219 name: "multiple paths",
220 snapshot: Snapshot{
221 ShortID: "c3d4e5f6",
222 Time: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
223 Hostname: "backup-host",
224 Paths: []string{"/home", "/etc", "/srv"},
225 Tags: []string{"full"},
226 },
227 want: "c3d4e5f6 2026-01-01 00:00 backup-host /home, /etc, /srv [full]",
228 },
229 {
230 name: "empty paths",
231 snapshot: Snapshot{
232 ShortID: "d4e5f6a7",
233 Time: time.Date(2026, 6, 15, 14, 22, 0, 0, time.UTC),
234 Hostname: "box",
235 },
236 want: "d4e5f6a7 2026-06-15 14:22 box",
237 },
238 {
239 name: "empty hostname",
240 snapshot: Snapshot{
241 ShortID: "e5f6a7b8",
242 Time: time.Date(2026, 2, 28, 12, 0, 0, 0, time.UTC),
243 Paths: []string{"/data"},
244 },
245 want: "e5f6a7b8 2026-02-28 12:00 (unknown) /data",
246 },
247 }
248
249 for _, tt := range tests {
250 tt := tt
251 t.Run(tt.name, func(t *testing.T) {
252 t.Parallel()
253
254 got := FormatSnapshotLine(tt.snapshot)
255 if got != tt.want {
256 t.Errorf("FormatSnapshotLine():\n got %q\n want %q", got, tt.want)
257 }
258 })
259 }
260}