package hostops import ( "context" "crypto/sha256" "encoding/hex" "errors" "github.com/carlosframework/platform/internal/alerting" "github.com/carlosframework/platform/internal/hostclaim" "strings" "testing" "time" ) func TestPlan12ReportingFailureStillRunsRecovery(t *testing.T) { f := newResolveFixture(t, true) o, err := hostclaim.Update(t.Context(), f.raw, f.moving, func(c *hostclaim.Claim) error { ref := hostclaim.FenceRef{Enrollment: strings.Repeat("a", 32), SHA256: strings.Repeat("b", 64), Key: "control/box-fences/" + strings.Repeat("a", 32) + ".json"} sum := sha256.Sum256([]byte(c.Op.ID + "\x00box-b\x00" + ref.Enrollment + "\x00" + ref.SHA256)) id := "auto-" + hex.EncodeToString(sum[:16]) c.LossRefs = []string{id} c.EvidenceRefs = []hostclaim.AutomaticLoss{{ID: id, Box: "box-b", Enrollment: ref.Enrollment, Fence: ref, ObservedAt: f.now.Format(time.RFC3339Nano), Op: c.Op.ID, Kind: hostclaim.KindLanding, Phase: hostclaim.PhaseProbing, Role: "target", Incarnation: c.Holder.Incarnation, Stamp: c.Holder.Stamp, Timers: hostclaim.TimersUnknown}} return nil }) // The raw fixture below supplies a retained evidence row; validation of the // row belongs to ReportAutomaticLosses, whose failure is the point here. if err != nil { _ = o t.Fatal(err) } noWork := func(context.Context) error { return nil } wrong := func(context.Context, hostclaim.Observed) error { t.Fatal("generic recovery"); return nil } failure := errors.New("loss signer unavailable") calls := 0 pages := 0 alerting.Arm(&alerting.Sinks{WebhookURL: "http://unused", Post: func(_ string, _ []byte) error { pages++; return nil }}) t.Cleanup(func() { alerting.Arm(nil) }) r := Recovery{Controller: Controller{Store: f.raw, Control: f.w, Authority: &f.authority, Now: func() time.Time { return f.now }, NewID: func() string { return "recovery" }}, Purge: wrong, Placement: wrong, SweepRecords: noWork, SweepPurges: noWork, SweepAdmissions: noWork} r.ReportAutomaticLosses = func(ctx context.Context, host string) error { before, err := hostclaim.Read(ctx, f.raw, host) if err != nil { return err } _, err = hostclaim.Update(ctx, f.raw, before, func(c *hostclaim.Claim) error { if c.Op.Journal == nil { c.Op.Journal = map[string]string{} } c.Op.Journal["report-attempt"] = "failed" return nil }) if err != nil { return err } return failure } r.Moves = func(ctx context.Context, g *MoveRecoveryGrant) error { calls++ fresh, err := hostclaim.Read(ctx, f.raw, g.Observation().Host) if err != nil { return err } if fresh.ETag != g.Observation().ClaimETag || fresh.Claim.Op.Journal["report-attempt"] != "failed" { t.Fatal("recovery used observation before report") } return nil } if err := f.w.RunRecovery(t.Context(), r.Pass); !errors.Is(err, failure) || calls != 1 || pages != 1 { t.Fatalf("pass=%v recovery calls=%d", err, calls) } }