| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/registry" |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | "github.com/carlosframework/platform/internal/rollout" |
| 11 | "github.com/carlosframework/platform/internal/status" |
| 12 | ) |
| 13 | |
| 14 | func TestPlan14RootRuntimeFailureAndCompletion(t *testing.T) { |
| 15 | for _, outcome := range []string{"failed", "healthy", "crash", "not-ready", "replaced-after-ready"} { |
| 16 | t.Run(outcome, func(t *testing.T) { |
| 17 | state := testRootFailureStore(t) |
| 18 | book, err := rollout.InitializeRootFailures(state, "box/en/1", nil) |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | rt := registry.Route{Host: "h.example.com", Account: "bab", App: "km"} |
| 23 | saved := rootUpdateRecord{Schema: 1, Installation: "install", Route: rt, Consumed: &release.Consumed{Resolved: &release.ResolvedRequest{ID: 7, Target: "v1"}}} |
| 24 | p := rootUpdateSource{state: state, book: book} |
| 25 | if outcome == "not-ready" { |
| 26 | p.ready = func(registry.Route) error { return errors.New("not ready") } |
| 27 | } |
| 28 | a, err := p.beginRuntime(&saved, func() error { return nil }, "v1", "digest", "attempt") |
| 29 | if err != nil { |
| 30 | t.Fatal(err) |
| 31 | } |
| 32 | if outcome == "crash" { |
| 33 | book, err = rollout.OpenRootFailures(state, "box/en/1") |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | if !book.HeldDown("bab", "km", "v1", rt.Host) { |
| 38 | t.Fatal("crash erased root intent") |
| 39 | } |
| 40 | return |
| 41 | } |
| 42 | var failure error |
| 43 | if outcome == "failed" { |
| 44 | failure = errors.New("readiness failed") |
| 45 | } |
| 46 | var verify func() error |
| 47 | if outcome == "replaced-after-ready" { |
| 48 | verify = func() error { return errors.New("invocation replaced") } |
| 49 | } |
| 50 | if err = a("invocation", failure, verify); (err != nil) != (outcome == "not-ready" || outcome == "replaced-after-ready") { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | if outcome == "failed" || outcome == "not-ready" || outcome == "replaced-after-ready" { |
| 54 | if !book.HeldDown("bab", "km", "v1", rt.Host) { |
| 55 | t.Fatal("root runtime failure lost") |
| 56 | } |
| 57 | if saved.Build.State == nil || saved.Build.State.Completed != 7 || !saved.Build.State.CompletedFailed { |
| 58 | t.Fatal("failed request not completed", saved.Build) |
| 59 | } |
| 60 | } else { |
| 61 | if saved.Build.State == nil || saved.Build.State.Completed != 7 || saved.Build.State.CompletedFailed || saved.Build.State.LastExecuted != "v1" { |
| 62 | t.Fatal("healthy request not completed", saved.Build) |
| 63 | } |
| 64 | if saved.Served["v1"].Invocation != "invocation" { |
| 65 | t.Fatal("served evidence missing") |
| 66 | } |
| 67 | } |
| 68 | }) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | func TestPlan14RootRetryProjection(t *testing.T) { |
| 73 | state := testRootFailureStore(t) |
| 74 | book, err := rollout.InitializeRootFailures(state, "box/en/1", nil) |
| 75 | if err != nil { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | rt := registry.Route{Host: "h.example.com", Account: "bab", App: "km"} |
| 79 | if _, err = book.Record(rollout.FailureRecord{Account: rt.Account, App: rt.App, Host: rt.Host, Build: "v1", HeldDown: []string{rt.Host}, At: time.Now()}); err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | p := rootUpdateSource{book: book} |
| 83 | projected := p.retryStatus(rootUpdateRecord{Route: rt}, "v1") |
| 84 | if projected.HeldBook != book.ID() || projected.HeldEpoch == 0 || projected.RetryV != status.HostRetryV { |
| 85 | t.Fatal("wrong retry authority", projected) |
| 86 | } |
| 87 | seed, public, _ := release.GenerateKey() |
| 88 | priv, _ := release.ParseSigningKey(seed, "test") |
| 89 | pub, _ := release.ParsePublicKey(public, "test") |
| 90 | wire, err := release.SignHostRetry(release.RootSigner(priv), rt.Account, rt.App, rt.Host, projected.HeldBy, projected.HeldBook, projected.HeldEpoch, time.Now()) |
| 91 | if err != nil { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | if err = book.VerifiedRetry(pub, wire, rt.Account, rt.App, rt.Host, time.Now()); err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | if book.HeldDown(rt.Account, rt.App, "v1", rt.Host) { |
| 98 | t.Fatal("projected retry refused") |
| 99 | } |
| 100 | if _, err = book.Record(rollout.FailureRecord{Account: rt.Account, App: rt.App, Host: rt.Host, Build: "v1", HeldDown: []string{rt.Host}}); err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | if err = book.VerifiedRetry(pub, wire, rt.Account, rt.App, rt.Host, time.Now()); err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | if !book.HeldDown(rt.Account, rt.App, "v1", rt.Host) { |
| 107 | t.Fatal("stale projection cleared new failure") |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | func TestPlan14RootFailureDeliveryCannotHoldActivation(t *testing.T) { |
| 112 | for _, ready := range []bool{false, true} { |
| 113 | t.Run(map[bool]string{false: "unready", true: "ready"}[ready], func(t *testing.T) { |
| 114 | book, err := rollout.InitializeRootFailures(testRootFailureStore(t), "box/en/1", nil) |
| 115 | if err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | saved := rootUpdateRecord{Route: registry.Route{Host: "h.example.com", Account: "bab", App: "km"}, Installation: "install"} |
| 119 | delivered, verified := 0, 0 |
| 120 | p := rootUpdateSource{book: book, publish: func(string, rootRetryStatusRecord) error { delivered++; return errors.New("delivery unavailable") }, ready: func(registry.Route) error { |
| 121 | if !ready { |
| 122 | return errors.New("unready") |
| 123 | } |
| 124 | return nil |
| 125 | }} |
| 126 | finish, err := p.beginRuntime(&saved, func() error { return nil }, "v1", "digest", "attempt") |
| 127 | if err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | err = finish("invocation", nil, func() error { verified++; return nil }) |
| 131 | if (err == nil) != ready || delivered != 1 { |
| 132 | t.Fatal("delivery became launch authority or readiness was waived", err, delivered) |
| 133 | } |
| 134 | if ready && (verified != 1 || saved.Served["v1"].Invocation != "invocation") { |
| 135 | t.Fatal("settlement lost") |
| 136 | } |
| 137 | if !ready && !book.HeldDown("bab", "km", "v1", "h.example.com") { |
| 138 | t.Fatal("failure attribution lost") |
| 139 | } |
| 140 | }) |
| 141 | } |
| 142 | } |
| 143 | |