@@ -0,0 +1,260 @@
+package restic
+
+import (
+ "testing"
+ "time"
+)
+
+// resolveFixtureTOML-style: embed test JSON as string constants so tests
+// are self-contained and don't depend on external files.
+
+const snapshotFixtureNormal = `[
+ {
+ "time": "2026-03-15T10:30:00.123456789+01:00",
+ "parent": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
+ "tree": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
+ "paths": ["/home/user"],
+ "hostname": "myhost",
+ "username": "user",
+ "uid": 1000,
+ "gid": 1000,
+ "excludes": ["*.tmp"],
+ "tags": ["daily", "important"],
+ "program_version": "restic 0.18.1",
+ "id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
+ "short_id": "a1b2c3d4"
+ },
+ {
+ "time": "2026-03-14T08:00:00Z",
+ "tree": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
+ "paths": ["/home/user", "/etc"],
+ "hostname": "server",
+ "username": "root",
+ "uid": 0,
+ "gid": 0,
+ "program_version": "restic 0.18.1",
+ "id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3",
+ "short_id": "b2c3d4e5"
+ }
+]`
+
+const snapshotFixtureEmpty = `[]`
+
+const snapshotFixtureSingleNoTags = `[
+ {
+ "time": "2026-01-01T00:00:00Z",
+ "tree": "0000000000000000000000000000000000000000000000000000000000000000",
+ "paths": ["/srv/data"],
+ "hostname": "backup-host",
+ "username": "backup",
+ "uid": 1001,
+ "gid": 1001,
+ "program_version": "restic 0.17.0",
+ "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4",
+ "short_id": "c3d4e5f6"
+ }
+]`
+
+const snapshotFixtureManyPaths = `[
+ {
+ "time": "2026-06-15T14:22:33Z",
+ "tree": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "paths": ["/home/user/Documents", "/home/user/Music", "/home/user/Pictures", "/home/user/Videos"],
+ "hostname": "workstation",
+ "username": "user",
+ "uid": 1000,
+ "gid": 1000,
+ "tags": ["weekly"],
+ "program_version": "restic 0.18.1",
+ "id": "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5",
+ "short_id": "d4e5f6a7"
+ }
+]`
+
+func TestParseSnapshots(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ json string
+ wantCount int
+ wantErr bool
+ }{
+ {
+ name: "normal with two snapshots",
+ json: snapshotFixtureNormal,
+ wantCount: 2,
+ wantErr: false,
+ },
+ {
+ name: "empty array",
+ json: snapshotFixtureEmpty,
+ wantCount: 0,
+ wantErr: false,
+ },
+ {
+ name: "single snapshot without tags",
+ json: snapshotFixtureSingleNoTags,
+ wantCount: 1,
+ wantErr: false,
+ },
+ {
+ name: "snapshot with many paths",
+ json: snapshotFixtureManyPaths,
+ wantCount: 1,
+ wantErr: false,
+ },
+ {
+ name: "invalid JSON",
+ json: `{not valid json`,
+ wantCount: 0,
+ wantErr: true,
+ },
+ {
+ name: "null",
+ json: `null`,
+ wantCount: 0,
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ snapshots, err := ParseSnapshots([]byte(tt.json))
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("ParseSnapshots(): err=%v, wantErr=%v", err, tt.wantErr)
+ }
+ if err != nil {
+ return
+ }
+ if len(snapshots) != tt.wantCount {
+ t.Fatalf("ParseSnapshots(): got %d snapshots, want %d", len(snapshots), tt.wantCount)
+ }
+ })
+ }
+}
+
+func TestParseSnapshotsFields(t *testing.T) {
+ t.Parallel()
+
+ snapshots, err := ParseSnapshots([]byte(snapshotFixtureNormal))
+ if err != nil {
+ t.Fatalf("ParseSnapshots(): unexpected error: %v", err)
+ }
+ if len(snapshots) != 2 {
+ t.Fatalf("expected 2 snapshots, got %d", len(snapshots))
+ }
+
+ // First snapshot: has tags, single path, has parent.
+ s := snapshots[0]
+ if s.ID != "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" {
+ t.Errorf("ID = %q", s.ID)
+ }
+ if s.ShortID != "a1b2c3d4" {
+ t.Errorf("ShortID = %q", s.ShortID)
+ }
+ if s.Hostname != "myhost" {
+ t.Errorf("Hostname = %q", s.Hostname)
+ }
+ if len(s.Paths) != 1 || s.Paths[0] != "/home/user" {
+ t.Errorf("Paths = %v", s.Paths)
+ }
+ if len(s.Tags) != 2 || s.Tags[0] != "daily" || s.Tags[1] != "important" {
+ t.Errorf("Tags = %v", s.Tags)
+ }
+ expectedTime := time.Date(2026, 3, 15, 10, 30, 0, 123456789, time.FixedZone("", 3600))
+ if !s.Time.Equal(expectedTime) {
+ t.Errorf("Time = %v, want %v", s.Time, expectedTime)
+ }
+
+ // Second snapshot: no tags, no parent, multiple paths.
+ s2 := snapshots[1]
+ if s2.ShortID != "b2c3d4e5" {
+ t.Errorf("ShortID = %q", s2.ShortID)
+ }
+ if s2.Hostname != "server" {
+ t.Errorf("Hostname = %q", s2.Hostname)
+ }
+ if len(s2.Paths) != 2 {
+ t.Errorf("Paths = %v", s2.Paths)
+ }
+ if len(s2.Tags) != 0 {
+ t.Errorf("Tags = %v, want empty", s2.Tags)
+ }
+}
+
+func TestFormatSnapshotLine(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ snapshot Snapshot
+ want string
+ }{
+ {
+ name: "full snapshot with tags",
+ snapshot: Snapshot{
+ ShortID: "a1b2c3d4",
+ Time: time.Date(2026, 3, 15, 10, 30, 0, 0, time.UTC),
+ Hostname: "myhost",
+ Paths: []string{"/home/user"},
+ Tags: []string{"daily", "important"},
+ },
+ want: "a1b2c3d4 2026-03-15 10:30 myhost /home/user [daily, important]",
+ },
+ {
+ name: "no tags",
+ snapshot: Snapshot{
+ ShortID: "b2c3d4e5",
+ Time: time.Date(2026, 3, 14, 8, 0, 0, 0, time.UTC),
+ Hostname: "server",
+ Paths: []string{"/etc"},
+ },
+ want: "b2c3d4e5 2026-03-14 08:00 server /etc",
+ },
+ {
+ name: "multiple paths",
+ snapshot: Snapshot{
+ ShortID: "c3d4e5f6",
+ Time: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
+ Hostname: "backup-host",
+ Paths: []string{"/home", "/etc", "/srv"},
+ Tags: []string{"full"},
+ },
+ want: "c3d4e5f6 2026-01-01 00:00 backup-host /home, /etc, /srv [full]",
+ },
+ {
+ name: "empty paths",
+ snapshot: Snapshot{
+ ShortID: "d4e5f6a7",
+ Time: time.Date(2026, 6, 15, 14, 22, 0, 0, time.UTC),
+ Hostname: "box",
+ },
+ want: "d4e5f6a7 2026-06-15 14:22 box",
+ },
+ {
+ name: "empty hostname",
+ snapshot: Snapshot{
+ ShortID: "e5f6a7b8",
+ Time: time.Date(2026, 2, 28, 12, 0, 0, 0, time.UTC),
+ Paths: []string{"/data"},
+ },
+ want: "e5f6a7b8 2026-02-28 12:00 (unknown) /data",
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ got := FormatSnapshotLine(tt.snapshot)
+ if got != tt.want {
+ t.Errorf("FormatSnapshotLine():\n got %q\n want %q", got, tt.want)
+ }
+ })
+ }
+}