package localapi import ( "context" "encoding/json" "io" "log/slog" "testing" "time" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" ) // R-517 — the per-tier truth on GET /backup/status. BIGNIGHT: a successful 8.9 GB local backup, // then a failed PBS attempt on a storage that did not exist; the page (fed by the single latest // record) showed the 0-byte failure as "up to date" and the remote copy as present. func tierStatesOf(t *testing.T, srv *Server) []TierBackupState { t.Helper() w := do(t, srv.Handler(), "GET", "/backup/status", "A", "") var resp struct { Data BackupStatusResponse `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decode: %v (%s)", err, w.Body.String()) } return resp.Data.Tiers } func tierStatesServer(t *testing.T, st *fakeStore, targets []hub.StorageTarget) *Server { t.Helper() srv, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: &fakeGuests{}, Backups: &fakeBackups{}, Store: st, Storage: fakeStorage{targets: targets}, Tokens: staticTokens{"A": 8200}, BackupTiers: []BackupTier{ {TargetID: "local", Cadence: 24 * time.Hour, Primary: true, Service: &fakeBackups{}}, {TargetID: "felhom-pbs", Cadence: 7 * 24 * time.Hour, Service: &fakeBackups{}}, }, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatal(err) } srv.baseCtx = context.Background() srv.now = func() time.Time { return testNow } return srv } // RED-PROOF (run 2026-09-15, recorded in REPORT.md): with tierBackupStates filling LastSuccess from // pickLatestBackup(ctx, vmid, false, …) — an ATTEMPT — the pbs tier's last_success became the failed // 0-byte record and this failed at "pbs tier reports a failed attempt as its last success". func TestBackupStatus_TierStates_FailedTierNeverStandsInForSuccess(t *testing.T) { st := &fakeStore{backups: []hub.Backup{ {TargetID: "local", VMID: 8200, Success: true, SizeBytes: 8877619753, StartedAt: "2026-06-10T11:03:23Z"}, {TargetID: "felhom-pbs", VMID: 8200, Success: false, Error: "storage 'felhom-pbs' does not exist", StartedAt: "2026-06-10T11:09:59Z"}, }} srv := tierStatesServer(t, st, []hub.StorageTarget{{Name: "local", Type: "local"}}) // PBS storage ABSENT tiers := tierStatesOf(t, srv) if len(tiers) != 2 { t.Fatalf("want 2 tiers, got %+v", tiers) } local, pbs := tiers[0], tiers[1] if local.Target != "local" || local.LastSuccess == nil || local.LastSuccess.SizeBytes != 8877619753 || local.Storage != StoragePresencePresent { t.Fatalf("local tier lost its successful backup: %+v", local) } if pbs.LastSuccess != nil { t.Fatalf("pbs tier reports a failed attempt as its last success: %+v", pbs.LastSuccess) } if pbs.LastAttempt == nil || pbs.LastAttempt.Success || pbs.LastAttempt.Error == "" { t.Fatalf("pbs tier's failed attempt is not reported as failed: %+v", pbs.LastAttempt) } if pbs.Storage != StoragePresenceAbsent { t.Fatalf("pbs storage should read absent, got %q", pbs.Storage) } // The pre-R-517 field is unchanged (compat): still the newest record across targets. w := do(t, srv.Handler(), "GET", "/backup/status", "A", "") var resp struct { Data BackupStatusResponse `json:"data"` } _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp.Data.Backup == nil || resp.Data.Backup.TargetID != "felhom-pbs" { t.Fatalf("untargeted .backup changed meaning: %+v", resp.Data.Backup) } } // /backup/tiers advertises storage presence, tri-state. func TestBackupTiers_StoragePresence(t *testing.T) { srv := tierStatesServer(t, &fakeStore{}, []hub.StorageTarget{{Name: "local"}}) w := do(t, srv.Handler(), "GET", "/backup/tiers", "A", "") var resp struct { Data BackupTiersResponse `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatal(err) } got := map[string]string{} for _, ti := range resp.Data.Tiers { got[ti.Target] = ti.Storage } if got["local"] != "present" || got["felhom-pbs"] != "absent" { t.Fatalf("storage presence wrong: %v", got) } // An unreadable storage view is "unknown", never "absent". srv.storage = tierErrStorage{} if p := srv.storagePresence(context.Background(), "felhom-pbs"); p != StoragePresenceUnknown { t.Fatalf("unreadable storage view must be unknown, got %q", p) } } type tierErrStorage struct{} func (tierErrStorage) Observe(context.Context) ([]hub.StorageTarget, error) { return nil, context.DeadlineExceeded } // A targeted request keeps the pre-R-517 bytes (no tiers array). func TestBackupStatus_TargetedHasNoTiers(t *testing.T) { srv := tierStatesServer(t, &fakeStore{}, []hub.StorageTarget{{Name: "local"}}) w := do(t, srv.Handler(), "GET", "/backup/status?target=local", "A", "") if json.Valid(w.Body.Bytes()) && containsKey(w.Body.Bytes(), "tiers") { t.Fatalf("targeted status grew a tiers array: %s", w.Body.String()) } } func containsKey(b []byte, key string) bool { var m struct { Data map[string]json.RawMessage `json:"data"` } _ = json.Unmarshal(b, &m) _, ok := m.Data[key] return ok }