package pbs import ( "bytes" "context" "log/slog" "net/http" "net/http/httptest" "strings" "testing" ) // verifyServer is a fake PBS that answers verify POSTs, task status, and a snapshot list whose // verification.state is controlled by `state` ("" → no verification field → none). func verifyServer(t *testing.T, state string) (*httptest.Server, string) { t.Helper() verification := "" if state != "" { verification = `"verification":{"state":"` + state + `","upid":"UPID:dooplex:0:0:0:0:verify:ds:u:"},` } body := `{"data":[{"backup-type":"ct","backup-id":"9001","backup-time":1781014713,"size":10,"owner":"u",` + verification + `"files":[{"filename":"root.pxar.didx","crypt-mode":"encrypt","size":10}]}]}` return newPBSTestServer(t, func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/verify"): w.Write([]byte(`{"data":"UPID:dooplex:0:0:0:0:verify:ds:u:"}`)) case strings.Contains(r.URL.Path, "/status"): w.Write([]byte(`{"data":{"status":"stopped","exitstatus":"OK","node":"dooplex"}}`)) default: // snapshots w.Write([]byte(body)) } }) } func TestVerifyLoop_RecordsState(t *testing.T) { ts, fp := verifyServer(t, "ok") host, port := hostPort(t, ts.URL) c, _ := NewClient(Config{Server: host, Port: port, Fingerprint: fp, TokenID: "u@pbs!t", Secret: "s"}) store := NewSnapshotStore() loop := NewVerifyLoop(VerifyLoopOptions{ Targets: func(context.Context) ([]Target, error) { return []Target{{Datastore: "ds", Client: c}}, nil }, Store: store, }) loop.RunOnce(context.Background()) snaps := store.PBSSnapshots(context.Background()) if len(snaps) != 1 || snaps[0].VerifyState != VerifyOK { t.Fatalf("loop should record 1 ok snapshot, got %+v", snaps) } } func TestVerifyLoop_FailedVerifyRecorded(t *testing.T) { ts, fp := verifyServer(t, "failed") host, port := hostPort(t, ts.URL) c, _ := NewClient(Config{Server: host, Port: port, Fingerprint: fp, TokenID: "u@pbs!t", Secret: "s"}) store := NewSnapshotStore() NewVerifyLoop(VerifyLoopOptions{ Targets: func(context.Context) ([]Target, error) { return []Target{{Datastore: "ds", Client: c}}, nil }, Store: store, }).RunOnce(context.Background()) if s := store.PBSSnapshots(context.Background()); len(s) != 1 || s[0].VerifyState != VerifyFailed { t.Fatalf("failed verify must be recorded, got %+v", s) } } // TestVerifyLoop_NeverLogsSecret runs a full cycle with the loop's logger capturing output and // asserts the token secret never appears in any log line. func TestVerifyLoop_NeverLogsSecret(t *testing.T) { const secret = "tok-secret-DO-NOT-LOG-7f3a" ts, fp := verifyServer(t, "ok") host, port := hostPort(t, ts.URL) c, _ := NewClient(Config{Server: host, Port: port, Fingerprint: fp, TokenID: "felhom@pbs!n100", Secret: secret}) var buf bytes.Buffer logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) loop := NewVerifyLoop(VerifyLoopOptions{ Targets: func(context.Context) ([]Target, error) { return []Target{{Datastore: "ds", Client: c}}, nil }, Store: NewSnapshotStore(), Logger: logger, }) loop.RunOnce(context.Background()) if strings.Contains(buf.String(), secret) { t.Fatalf("the verify loop logged the token secret") } } func TestVerifyLoop_DisabledByNegativeCadence(t *testing.T) { loop := NewVerifyLoop(VerifyLoopOptions{ Targets: func(context.Context) ([]Target, error) { return nil, nil }, Store: NewSnapshotStore(), Cadence: -1, }) ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- loop.Run(ctx) }() cancel() if err := <-done; err != nil { t.Fatalf("disabled loop Run should return nil on cancel, got %v", err) } }