snapshots_test.go

  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}