From 2dac3bedef320e85da95fe1ba86c8c8d9d249513 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 26 Mar 2026 22:58:27 +0000 Subject: [PATCH] test(recovery): cover downstream malformed metadata consumers --- .../api/reporting_recovery_resilience_test.go | 65 ++++++++++++++ .../recovery_rollups_resilience_test.go | 88 +++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 internal/api/reporting_recovery_resilience_test.go create mode 100644 internal/monitoring/recovery_rollups_resilience_test.go diff --git a/internal/api/reporting_recovery_resilience_test.go b/internal/api/reporting_recovery_resilience_test.go new file mode 100644 index 000000000..a0ba9d72c --- /dev/null +++ b/internal/api/reporting_recovery_resilience_test.go @@ -0,0 +1,65 @@ +package api + +import ( + "context" + "testing" + "time" + + "github.com/rcourtman/pulse-go-rewrite/internal/recovery" +) + +func TestReportingHandlers_ListBackupsForReport_ToleratesMalformedPersistedMetadata(t *testing.T) { + boolPtr := func(v bool) *bool { return &v } + + recoveryHandler, dbPath := newRecoveryHandlerWithPersistedPoint(t, recovery.RecoveryPoint{ + ID: "report-point-bad-json", + Provider: recovery.ProviderProxmoxPBS, + Kind: recovery.KindBackup, + Mode: recovery.ModeRemote, + Outcome: recovery.OutcomeSuccess, + SubjectResourceID: "vm-123", + SubjectRef: &recovery.ExternalRef{ + Type: "proxmox-vm", + Name: "Archive VM", + }, + RepositoryRef: &recovery.ExternalRef{ + Type: "pbs-datastore", + Name: "fast-store", + }, + CompletedAt: timePtr(time.Date(2026, 2, 20, 9, 0, 0, 0, time.UTC)), + Verified: boolPtr(true), + Immutable: boolPtr(true), + Details: map[string]any{ + "storage": "fast-store", + "volid": "vm/123/2026-02-20T09:00:00Z", + }, + }) + corruptRecoveryRowJSON(t, dbPath, "report-point-bad-json", true, true, true) + + handler := NewReportingHandlers(nil, recoveryHandler.manager) + start := time.Date(2026, 2, 20, 0, 0, 0, 0, time.UTC) + end := time.Date(2026, 2, 20, 23, 59, 59, 0, time.UTC) + + backups := handler.listBackupsForReport(context.Background(), "default", "vm-123", start, end) + if len(backups) != 1 { + t.Fatalf("expected exactly 1 backup, got %d", len(backups)) + } + if backups[0].Type != "pbs" { + t.Fatalf("backup type = %q, want %q", backups[0].Type, "pbs") + } + if backups[0].Storage != "" { + t.Fatalf("backup storage = %q, want empty after malformed details degradation", backups[0].Storage) + } + if backups[0].VolID != "" { + t.Fatalf("backup volid = %q, want empty after malformed details degradation", backups[0].VolID) + } + if !backups[0].Verified { + t.Fatal("expected verified flag to survive malformed metadata degradation") + } + if !backups[0].Protected { + t.Fatal("expected protected flag to survive malformed metadata degradation") + } + if !backups[0].Timestamp.Equal(time.Date(2026, 2, 20, 9, 0, 0, 0, time.UTC)) { + t.Fatalf("backup timestamp = %s, want %s", backups[0].Timestamp, time.Date(2026, 2, 20, 9, 0, 0, 0, time.UTC)) + } +} diff --git a/internal/monitoring/recovery_rollups_resilience_test.go b/internal/monitoring/recovery_rollups_resilience_test.go new file mode 100644 index 000000000..4bfea4aba --- /dev/null +++ b/internal/monitoring/recovery_rollups_resilience_test.go @@ -0,0 +1,88 @@ +package monitoring + +import ( + "context" + "database/sql" + "path/filepath" + "testing" + "time" + + "github.com/rcourtman/pulse-go-rewrite/internal/config" + "github.com/rcourtman/pulse-go-rewrite/internal/recovery" + recoverymanager "github.com/rcourtman/pulse-go-rewrite/internal/recovery/manager" + + _ "modernc.org/sqlite" +) + +func TestMonitor_ListBackupRollupsForAlerts_ToleratesMalformedPersistedMetadata(t *testing.T) { + baseDir := t.TempDir() + mtp := config.NewMultiTenantPersistence(baseDir) + manager := recoverymanager.New(mtp) + store, err := manager.StoreForOrg("default") + if err != nil { + t.Fatalf("StoreForOrg(default): %v", err) + } + + completedAt := time.Date(2026, 2, 21, 8, 30, 0, 0, time.UTC) + if err := store.UpsertPoints(context.Background(), []recovery.RecoveryPoint{ + { + ID: "monitor-point-bad-json", + Provider: recovery.ProviderTrueNAS, + Kind: recovery.KindBackup, + Mode: recovery.ModeRemote, + Outcome: recovery.OutcomeSuccess, + SubjectRef: &recovery.ExternalRef{ + Type: "truenas-dataset", + Name: "tank/apps", + }, + CompletedAt: &completedAt, + }, + }); err != nil { + t.Fatalf("UpsertPoints(): %v", err) + } + + persistence, err := mtp.GetPersistence("default") + if err != nil { + t.Fatalf("GetPersistence(default): %v", err) + } + corruptMonitorRecoveryRowJSON( + t, + filepath.Join(persistence.DataDir(), "recovery", "recovery.db"), + "monitor-point-bad-json", + ) + + m := &Monitor{recoveryManager: manager} + rollups, err := m.listBackupRollupsForAlerts(context.Background()) + if err != nil { + t.Fatalf("listBackupRollupsForAlerts() error = %v, want graceful degradation", err) + } + if len(rollups) != 1 { + t.Fatalf("expected exactly 1 rollup, got %d", len(rollups)) + } + if rollups[0].SubjectRef != nil { + t.Fatalf("expected malformed item ref to be omitted, got %#v", rollups[0].SubjectRef) + } + if rollups[0].Display.SubjectLabel != "tank/apps" { + t.Fatalf("display.subjectLabel = %q, want %q", rollups[0].Display.SubjectLabel, "tank/apps") + } + if rollups[0].Display.ItemType != "dataset" { + t.Fatalf("display.itemType = %q, want %q", rollups[0].Display.ItemType, "dataset") + } + if rollups[0].LastOutcome != recovery.OutcomeSuccess { + t.Fatalf("lastOutcome = %q, want %q", rollups[0].LastOutcome, recovery.OutcomeSuccess) + } +} + +func corruptMonitorRecoveryRowJSON(t *testing.T, dbPath string, rowID string) { + t.Helper() + + db, err := sql.Open("sqlite", dbPath) + if err != nil { + t.Fatalf("sql.Open(%q): %v", dbPath, err) + } + t.Cleanup(func() { _ = db.Close() }) + + if _, err := db.ExecContext(context.Background(), "UPDATE recovery_points SET subject_ref_json = '{' WHERE id = ?", rowID); err != nil { + t.Fatalf("corrupt recovery row json: %v", err) + } +}