package host import ( "context" "github.com/carlosframework/platform/internal/provision" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func TestPlan14SignedActiveFixIgnoresHistory(t *testing.T) { for _, fault := range []string{"unavailable", "missing", "malformed", "unwritable", "refused", "pending", "unknown bake", "claims on"} { t.Run(fault, 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) b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App} switch fault { case "missing": h.Store.(*rollbackMemory).files = map[string][]byte{} case "malformed": for k := range h.Store.(*rollbackMemory).files { h.Store.(*rollbackMemory).files[k] = []byte("{") } case "unwritable": h.Store.(*rollbackMemory).failWrite = true case "refused": if err := h.AddRefusal(b, release.Refusal{Host: rt.Host, App: rt.App, Version: "v2", Kind: release.RefusalWrongProgram, RefusedAt: time.Now()}); err != nil { t.Fatal(err) } case "pending": if err := h.Begin(b, release.HistoryAttempt{ID: "unsettled", Version: "v0", SHA256: "abcd", Source: "old-source", Platform: release.AdoptPlatform(), Started: time.Now(), PromotedAt: time.Now()}); err != nil { t.Fatal(err) } } deps := boundRootHistory(h, source.resolve, e.root.obs) if fault == "unavailable" { deps = nil } if fault == "unknown bake" { if err := h.Store.Remove("bake.json"); err != nil { t.Fatal(err) } if _, err := h.LoadBake([]release.HistoryBinding{b}, time.Now()); err == nil { t.Fatal("fixture unexpectedly has bake completeness") } } c.history = func() (*release.RootHistoryPass, error) { if deps == nil { return nil, os.ErrPermission } return deps, nil } n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} if fault == "claims on" { n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{}} } err = release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, nil, release.WithRootHistory(deps), release.WithRecovery(func(ctx context.Context, rt registry.Route) (bool, error) { return n.recover(ctx, rt.Host, c, func(string) string { return "5s" }, e.root.reg) })) if fault == "refused" { live, liveErr := release.LiveVersion(u.opt, rt.Host) if err == nil || liveErr != nil || live != "v1" || len(u.effects) != 0 { t.Fatalf("known refusal ignored: live=%s effects=%v error=%v", live, u.effects, err) } return } if err != nil || len(u.effects) != 1 || u.exe != release.VersionPath(u.opt, rt.Account, "v2", rt.App) { t.Fatalf("history prevented active repair: effects=%v error=%v", u.effects, err) } got, err := e.root.reg.Get(rt.Host) if err != nil || got.Version != "v2" { t.Fatalf("missing verified version: %+v %v", got, err) } if err := release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, nil, release.WithRootHistory(deps), release.WithRecovery(func(ctx context.Context, rt registry.Route) (bool, error) { return n.recover(ctx, rt.Host, c, func(string) string { return "5s" }, e.root.reg) })); err != nil || len(u.effects) != 1 { t.Fatalf("repair replay: %v %v", err, u.effects) } }) } } func TestPlan14SignedActiveFixAuthority(t *testing.T) { for _, attack := range []string{"unsigned target", "unsigned predecessor", "changed artifact", "changed record", "changed unit", "changed pointer", "changed invocation", "operator stop", "held claim", "untrusted live", "capture failure", "rollback", "rollback fails"} { t.Run(attack, func(t *testing.T) { e, u, c, source, pub := signedRecoveryFixture(t) rt, err := e.root.reg.Get(observedHost) if err != nil { t.Fatal(err) } n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} mutate := func() {} switch attack { case "unsigned target": n.pub = make([]byte, 32) case "unsigned predecessor": if err := e.store.Delete(t.Context(), release.PromotionRecordKey(rt.Account, rt.App, "v1")); err != nil { t.Fatal(err) } case "changed artifact": mutate = func() { if err := os.WriteFile(release.VersionPath(u.opt, rt.Account, "v2", rt.App), []byte("wrong"), 0755); err != nil { t.Fatal(err) } } case "changed record": mutate = func() { rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host) if err != nil { t.Fatal(err) } rec.RecordSeq++ plan14PutRecord(t, e.store, *rec) } case "changed unit": mutate = func() { if err := os.WriteFile(u.props.FragmentPath, []byte("wrong"), 0644); err != nil { t.Fatal(err) } } case "changed pointer": mutate = func() { if err := e.store.Delete(t.Context(), release.ChannelKey(rt.Account, rt.App, rt.Channel)); err != nil { t.Fatal(err) } } case "changed invocation": mutate = func() { u.props.InvocationID += "other" } case "operator stop": mutate = func() { u.props.ActiveState, u.props.SubState, u.props.MainPID = "inactive", "dead", 0 } case "held claim": n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{deny: true}} case "untrusted live": n.readLive = func(string) (string, error) { return "", os.ErrPermission } case "capture failure": disk, _ := c.attempts() disk.(*rollbackMemory).failWrite = true case "rollback", "rollback fails": u.crashNew = true u.badRollback = attack == "rollback fails" } if strings.HasPrefix(attack, "changed ") || attack == "operator stop" { n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{before: mutate}} } handled, err := n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg) if !handled || err == nil { t.Fatalf("attack accepted: %v %v", handled, err) } if attack == "rollback" || attack == "rollback fails" { if len(u.effects) != 3 { t.Fatalf("rollback absent: %v %v", u.effects, err) } if attack == "rollback" && u.exe != release.VersionPath(u.opt, rt.Account, "v1", rt.App) { t.Fatal("wrong recovered executable") } count := len(u.effects) _, _ = n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg) if len(u.effects) != count { t.Fatalf("repeated failed attempt: %v", u.effects) } } else { live, liveErr := release.LiveVersion(u.opt, rt.Host) if len(u.effects) != 0 || liveErr != nil || live != "v1" { t.Fatalf("unauthorized effect: %v live=%s error=%v", u.effects, live, liveErr) } } }) } } func TestPlan14SignedFixSupersedesOlderAttempt(t *testing.T) { e, u, c, source, pub := signedRecoveryFixture(t) b, err := c.host(t.Context(), observedHost) if err != nil { t.Fatal(err) } h := seedHostRootHistory(t, b.Route) c.history = func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil } disk, err := c.attempts() if err != nil { t.Fatal(err) } _, err = captureRollback(disk, b, c.now(), unitSettleTimeout(func(string) string { return "5s" }), release.VersionPath(u.opt, b.Route.Account, "obsolete", b.Route.App), c.identity, historyRollbackLink{Binding: release.HistoryBinding{Host: b.Route.Host, Account: b.Route.Account, App: b.Route.App}, Attempt: release.HistoryAttempt{ID: "older-attempt", Version: "obsolete", Source: b.Source, SHA256: "old-digest", Platform: release.AdoptPlatform(), Started: c.now(), PromotedAt: c.now()}, Independent: true}) if err != nil { t.Fatal(err) } 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 || len(u.effects) != 1 || u.exe != release.VersionPath(u.opt, b.Route.Account, "v2", b.Route.App) { t.Fatalf("old attempt blocked new signed fix: %v %v %v", handled, err, u.effects) } if _, err := h.Refusals(release.HistoryBinding{Host: b.Route.Host, Account: b.Route.Account, App: b.Route.App}); err == nil { t.Fatal("superseded ambiguous outcome erased by success") } } func TestPlan14ERecoveryBindingRefusalKeepsPromotedFix(t *testing.T) { e, u, c, source, pub := signedRecoveryFixture(t) c.legacy = func() (bool, error) { return true, nil } c.legacyReady = c.ready // Refuse the final readiness binding after the process effect. original := c.host c.host = func(ctx context.Context, host string) (proofBinding, error) { if len(u.effects) > 0 { return proofBinding{}, os.ErrPermission } return original(ctx, host) } 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) live, liveErr := release.LiveVersion(u.opt, observedHost) if handled || err != nil || liveErr != nil || live != "v1" || len(u.effects) != 0 { t.Fatalf("no-census recovery selected effects: handled=%v err=%v live=%s/%v effects=%v", handled, err, live, liveErr, u.effects) } rt, err := e.root.reg.Get(observedHost) if err != nil { t.Fatal(err) } if _, _, err := release.StageSignedHead(t.Context(), e.store, pub, nil, u.opt, *rt); err != nil { t.Fatal(err) } if err := release.PointLive(u.opt, observedHost, rt.Account, "v2", rt.App); err != nil { t.Fatal(err) } if err := legacyCheckedUnitRestart(u, u.opt, func(string) string { return "5s" }, *rt, nil, c); err != nil { t.Fatal(err) } live, liveErr = release.LiveVersion(u.opt, observedHost) if liveErr != nil || live != "v2" || len(u.effects) != 1 { t.Fatalf("main outcome changed: %s/%v %v", live, liveErr, u.effects) } }