| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "os" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostwrite" |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | func TestPlan14RecoveryKnownRefusal(t *testing.T) { |
| 14 | for _, mode := range []string{"active", "failed", "inactive"} { |
| 15 | for _, kind := range []string{release.RefusalNotUp, release.RefusalWrongProgram, release.RefusalNotProven} { |
| 16 | for _, change := range []string{"same", "retry", "unsigned", "late-refusal"} { |
| 17 | t.Run(mode+"/"+kind+"/"+change, func(t *testing.T) { |
| 18 | e, u, c, source, pub := signedRecoveryFixture(t) |
| 19 | rt, err := e.root.reg.Get(observedHost) |
| 20 | if err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | h := seedHostRootHistory(t, *rt) |
| 24 | b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App} |
| 25 | p, err := release.Channel(t.Context(), e.store, pub, rt.Account, rt.App, rt.Channel) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | at := p.PromotedAt |
| 30 | if change == "retry" { |
| 31 | at = at.Add(-time.Second) |
| 32 | } |
| 33 | ref := release.Refusal{Host: rt.Host, App: rt.App, Version: p.Version, Kind: kind, PointerPromotedAt: at, RefusedAt: at} |
| 34 | add := func() { |
| 35 | if err := h.AddRefusal(b, ref); err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | } |
| 39 | if change != "late-refusal" { |
| 40 | add() |
| 41 | } |
| 42 | c.history = func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil } |
| 43 | n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} |
| 44 | if change == "unsigned" { |
| 45 | n.pub = make(ed25519.PublicKey, ed25519.PublicKeySize) |
| 46 | } |
| 47 | if change == "late-refusal" { |
| 48 | n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{before: add}} |
| 49 | } |
| 50 | if mode != "active" { |
| 51 | u.props.ActiveState, u.props.SubState, u.props.MainPID = mode, mode, 0 |
| 52 | } |
| 53 | handled, err := n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg) |
| 54 | accepted := change == "retry" && kind == release.RefusalNotUp |
| 55 | live, liveErr := release.LiveVersion(u.opt, rt.Host) |
| 56 | want := "v1" |
| 57 | effects := 0 |
| 58 | if accepted { |
| 59 | want = "v2" |
| 60 | if mode == "active" { |
| 61 | effects = 1 |
| 62 | } |
| 63 | } |
| 64 | if !handled || (err == nil) != accepted || liveErr != nil || live != want || len(u.effects) != effects { |
| 65 | t.Fatalf("handled=%v error=%v live=%s effects=%v", handled, err, live, u.effects) |
| 66 | } |
| 67 | refs, err := h.Refusals(b) |
| 68 | if err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | if accepted && len(refs) != 0 { |
| 72 | t.Fatalf("verified retry retained D1: %+v", refs) |
| 73 | } |
| 74 | if !accepted && len(refs) != 1 { |
| 75 | t.Fatalf("refused target cleared evidence: %+v", refs) |
| 76 | } |
| 77 | }) |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | func TestPlan14RecoveryOriginalRefusalWithUnknownHistory(t *testing.T) { |
| 84 | e, u, c, source, pub := signedRecoveryFixture(t) |
| 85 | old, err := c.host(t.Context(), observedHost) |
| 86 | if err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | p, err := release.Channel(t.Context(), e.store, pub, old.Route.Account, old.Route.App, old.Route.Channel) |
| 90 | if err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | disk, err := c.attempts() |
| 94 | if err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | b := release.HistoryBinding{Host: old.Route.Host, Account: old.Route.Account, App: old.Route.App} |
| 98 | a, err := captureRollback(disk, old, c.now(), unitSettleTimeout(func(string) string { return "5s" }), release.VersionPath(u.opt, b.Account, p.Version, b.App), c.identity, historyRollbackLink{Binding: b, Attempt: release.HistoryAttempt{ID: "failed", Source: old.Source, Version: p.Version, SHA256: "digest", Platform: release.AdoptPlatform(), Started: c.now(), PromotedAt: p.PromotedAt}, Independent: true}) |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | a.Failed, a.Settled = true, true |
| 103 | if err := writeRollbackState(disk, recoveryResultKey(a), a); err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | delete(disk.(*rollbackMemory).files, rollbackJournalKey(b.Host)) |
| 107 | c.history = func() (*release.RootHistoryPass, error) { return nil, os.ErrPermission } |
| 108 | n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} |
| 109 | handled, err := n.recover(t.Context(), b.Host, c, func(string) string { return "5s" }, e.root.reg) |
| 110 | live, _ := release.LiveVersion(u.opt, b.Host) |
| 111 | if !handled || err == nil || live != "v1" || len(u.effects) != 0 { |
| 112 | t.Fatalf("retained failure ignored: handled=%v err=%v live=%s effects=%v", handled, err, live, u.effects) |
| 113 | } |
| 114 | } |
| 115 | |