Files
felhom-agent/internal/localapi/backup_tier_states_test.go

134 lines
4.9 KiB
Go

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
}