package store import ( "context" "testing" "time" ) // makeSnapHost inserts a minimal host row that snapshot tests can hang // off. Returns the host id. func makeSnapHost(t *testing.T, s *Store) string { t.Helper() const id = "01HSNAPHOST00000000000000" if err := s.CreateHost(context.Background(), Host{ ID: id, Name: "snap-host", OS: "linux", Arch: "amd64", AgentVersion: "dev", ResticVersion: "0.16.0", ProtocolVersion: 1, EnrolledAt: time.Now().UTC(), }, "tokenhash", ""); err != nil { t.Fatalf("create host: %v", err) } return id } func TestReplaceHostSnapshotsRoundTrip(t *testing.T) { t.Parallel() s := openTestStore(t) hostID := makeSnapHost(t, s) ctx := context.Background() now := time.Now().UTC().Truncate(time.Second) in := []Snapshot{ { ID: "deadbeef" + "00000000000000000000000000000000000000000000000000000000", ShortID: "deadbeef", Time: now.Add(-2 * time.Hour), Hostname: "snap-host", Paths: []string{"/etc", "/home"}, Tags: []string{"daily"}, SizeBytes: 4096, FileCount: 12, }, { ID: "cafef00d" + "00000000000000000000000000000000000000000000000000000000", ShortID: "cafef00d", Time: now.Add(-1 * time.Hour), Hostname: "snap-host", Paths: []string{"/etc"}, SizeBytes: 8192, FileCount: 24, }, } if err := s.ReplaceHostSnapshots(ctx, hostID, in, now); err != nil { t.Fatalf("replace: %v", err) } out, err := s.ListSnapshotsByHost(ctx, hostID) if err != nil { t.Fatalf("list: %v", err) } if len(out) != 2 { t.Fatalf("want 2 snapshots, got %d", len(out)) } // Ordered by time DESC — most recent first. if out[0].ShortID != "cafef00d" { t.Errorf("want most-recent first; got %q", out[0].ShortID) } if got := len(out[0].Paths); got != 1 { t.Errorf("paths roundtrip lost: %v", out[0].Paths) } if out[1].Tags == nil || out[1].Tags[0] != "daily" { t.Errorf("tags roundtrip lost: %v", out[1].Tags) } // Host snapshot_count is updated atomically. h, err := s.GetHost(ctx, hostID) if err != nil { t.Fatalf("get host: %v", err) } if h.SnapshotCount != 2 { t.Errorf("host snapshot_count = %d, want 2", h.SnapshotCount) } } func TestReplaceHostSnapshotsIsAuthoritative(t *testing.T) { t.Parallel() s := openTestStore(t) hostID := makeSnapHost(t, s) ctx := context.Background() mk := func(id, short string, tOff time.Duration) Snapshot { return Snapshot{ ID: id, ShortID: short, Time: time.Now().UTC().Add(tOff), Hostname: "snap-host", Paths: []string{"/x"}, } } first := []Snapshot{ mk("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaa", -3*time.Hour), mk("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "bbbbbbbb", -2*time.Hour), mk("cccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "cccccccc", -1*time.Hour), } if err := s.ReplaceHostSnapshots(ctx, hostID, first, time.Now().UTC()); err != nil { t.Fatalf("replace 1: %v", err) } // Subsequent forget+prune on the host: only one snapshot remains. second := []Snapshot{ mk("cccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "cccccccc", -1*time.Hour), } if err := s.ReplaceHostSnapshots(ctx, hostID, second, time.Now().UTC()); err != nil { t.Fatalf("replace 2: %v", err) } out, err := s.ListSnapshotsByHost(ctx, hostID) if err != nil { t.Fatalf("list: %v", err) } if len(out) != 1 || out[0].ShortID != "cccccccc" { t.Errorf("after second replace, want [cccccccc], got %+v", out) } h, _ := s.GetHost(ctx, hostID) if h.SnapshotCount != 1 { t.Errorf("snapshot_count should track replacement: %d", h.SnapshotCount) } } func TestReplaceHostSnapshotsEmpty(t *testing.T) { t.Parallel() s := openTestStore(t) hostID := makeSnapHost(t, s) ctx := context.Background() // First a non-empty replace. if err := s.ReplaceHostSnapshots(ctx, hostID, []Snapshot{ { ID: "1111111111111111111111111111111111111111111111111111111111111111", ShortID: "11111111", Time: time.Now().UTC(), Hostname: "snap-host", Paths: []string{"/x"}, }, }, time.Now().UTC()); err != nil { t.Fatalf("replace 1: %v", err) } // Then empty — host has been wiped. if err := s.ReplaceHostSnapshots(ctx, hostID, nil, time.Now().UTC()); err != nil { t.Fatalf("replace empty: %v", err) } out, err := s.ListSnapshotsByHost(ctx, hostID) if err != nil { t.Fatalf("list: %v", err) } if len(out) != 0 { t.Errorf("want empty, got %d", len(out)) } h, _ := s.GetHost(ctx, hostID) if h.SnapshotCount != 0 { t.Errorf("snapshot_count should reset to 0, got %d", h.SnapshotCount) } }