package host import ( "os" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func TestPlan14IndependentRecoveryRetainsMintBarrier(t *testing.T) { e, u, c, source, pub := signedRecoveryFixture(t) rt, err := e.root.reg.Get(observedHost) if err != nil { t.Fatal(err) } h := seedHostRootHistory(t, *rt) b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App} u.crashNew = true n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} if _, err := n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg); err == nil { t.Fatal("failed forward repair reported success") } if refs, err := c.mintRefusals(h, b); err != nil || len(refs) != 1 || refs[0].Version != "v2" { t.Fatalf("recovery denial lost: %+v %v", refs, err) } if err := checkedUnitRestart(u, u.opt, func(string) string { return "5s" }, nil, c)(*rt); err != nil { t.Fatal(err) } if refs, err := c.mintRefusals(h, b); err != nil || len(refs) != 1 || refs[0].Version != "v2" { t.Fatalf("ordinary restart erased recovery denial: %+v %v", refs, err) } disk, err := c.attempts() if err != nil { t.Fatal(err) } names, err := disk.Names() if err != nil { t.Fatal(err) } for _, name := range names { if strings.HasSuffix(name, ".recovery") { if err := disk.Remove(name); err != nil { t.Fatal(err) } } } if refs, err := c.mintRefusals(h, b); err != nil || len(refs) != 1 || refs[0].SHA256 == "" { t.Fatalf("removing old recovery erased retained digest: %+v %v", refs, err) } foreign := b foreign.App = "another" if err := c.retainRecoveryHistory(h, foreign); err != nil { t.Fatalf("unrelated app blocked: %v", err) } } func TestPlan14RecoveryMintEvidence(t *testing.T) { for _, state := range []string{"verified", "pending", "malformed", "unsynced"} { t.Run(state, func(t *testing.T) { e, _, c, source, pub := signedRecoveryFixture(t) rt, err := e.root.reg.Get(observedHost) if err != nil { t.Fatal(err) } h := seedHostRootHistory(t, *rt) n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} if _, err := n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg); err != nil { t.Fatal(err) } s, err := c.attempts() if err != nil { t.Fatal(err) } a, err := readRollbackAttempt(s, rt.Host) if err != nil { t.Fatal(err) } switch state { case "pending": a.Settled = false if err := writeRollbackState(s, rollbackJournalKey(rt.Host), a); err != nil { t.Fatal(err) } case "malformed": if err := s.Write(recoveryResultKey(a), []byte("{")); err != nil { t.Fatal(err) } case "unsynced": s.(*rollbackMemory).failWrite = true } refs, err := c.mintRefusals(h, release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}) if state == "verified" { if err != nil || len(refs) != 0 { t.Fatalf("verified recovery blocked mint: %+v %v", refs, err) } } else if err == nil { t.Fatalf("%s granted mint", state) } }) } } func TestPlan14IndependentRecoveryRecordsHistory(t *testing.T) { for _, state := range []string{"success", "rollback", "failed rollback"} { failed := state != "success" t.Run(state, func(t *testing.T) { e, u, c, source, pub := signedRecoveryFixture(t) rt, err := e.root.reg.Get(observedHost) if err != nil { t.Fatal(err) } h := seedHostRootHistory(t, *rt) c.history = func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil } u.crashNew = failed u.badRollback = state == "failed rollback" n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} handled, err := n.recover(t.Context(), observedHost, c, func(string) string { return "5s" }, e.root.reg) if !handled || (err != nil) != failed { t.Fatalf("outcome: %v %v", handled, err) } disk, err := c.attempts() if err != nil { t.Fatal(err) } a, err := readRollbackAttempt(disk, observedHost) if err != nil || !a.Settled || a.Verified != (state != "failed rollback") { t.Fatalf("wrong retained verification: %+v %v", a, err) } refs, err := h.Refusals(release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}) if err != nil || len(refs) != map[bool]int{false: 0, true: 1}[failed] { t.Fatalf("settlement did not retain outcome: %+v %v", refs, err) } }) } }