610804b98d
gates / gates (push) Successful in 11s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
134 lines
4.9 KiB
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
|
|
}
|