Files
restic-manager/internal/store/snapshots_test.go
T
steve b6f8de1dcc lint: drive baseline to zero, drop only-new-issues gate
Cleanup pass over the repo so CI can enforce lint going forward
without the only-new-issues escape hatch:

* gofumpt -w across the tree (31 hits, all formatting)
* misspell --fix (25 hits, US-locale spelling) — but reverted on
  api.JobCancelled = "cancelled" since that literal is the wire +
  DB CHECK constraint value, plus matched the case in store/fleet.go
  back to "cancelled" and added //nolint:misspell on both for the
  next time someone reaches for the auto-fix
* Wrap every `defer rows.Close()` / `defer stmt.Close()` /
  `defer res.Body.Close()` in `defer func() { _ = .Close() }()`
  to satisfy errcheck without losing the close itself
* websocket.Dial callers (1 prod, 4 tests) now capture + close the
  upgrade response Body — coder/websocket can return res with a nil
  Body on success, so the test deferred-closes guard against that
* Annotate the two genuine-by-design nilerr cases with //nolint
  comments explaining why nil-on-error is the contract (cookie
  missing = no session; ctx cancelled mid-backoff = clean shutdown)
* Add brief godoc on the 10 exported const groups + types that
  revive flagged (api.HostOS/HostArch/JobKind/JobStatus/LogStream/
  ErrorCode, restic.EventKind, store.Role, web.FS)
* Drop the unused (*Server).userByID method
* Inline the unparam baseView(active) — every UI page is under
  the dashboard primary nav today

Result: `golangci-lint run ./...` reports 0 issues. CI lint job
no longer needs only-new-issues: true; X-06 follow-up entry in
tasks.md removed.
2026-05-03 16:15:17 +01:00

156 lines
4.5 KiB
Go

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)
}
}