package main import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/carlosframework/platform/internal/rolloutwords" ) func TestInstalledAndServingCells(t *testing.T) { old := apiInstanceRecord{StagedVersion: "v1"} if installedCell(old) != "v1" || servingCell(old) != "—" { t.Errorf("no rollout: %q, %q", installedCell(old), servingCell(old)) } r := apiInstanceRecord{StagedVersion: "v1", Rollout: &apiRollout{Installed: "v2", Serving: "v1", Awake: true}} if installedCell(r) != "v2" || servingCell(r) != "v1" { t.Errorf("rollout cells = %q, %q", installedCell(r), servingCell(r)) } asleep := apiInstanceRecord{StagedVersion: "v1", Rollout: &apiRollout{Installed: "v2", Good: "v1"}} if servingCell(asleep) != "—" { t.Errorf("an asleep instance serves nothing: %q", servingCell(asleep)) } // A box that reports itself unavailable reports no serving build, even // if a stale one rides along. down := apiInstanceRecord{StagedVersion: "v1", Rollout: &apiRollout{Installed: "v2", Serving: "v1", Unavailable: "no-release-key"}} if installedCell(down) != "v2" || servingCell(down) != "—" { t.Errorf("unavailable cells = %q, %q", installedCell(down), servingCell(down)) } } func TestRolloutReason(t *testing.T) { for _, tc := range []struct { r *apiRollout want string }{ {nil, ""}, {&apiRollout{Installed: "v2", Serving: "v2", Awake: true}, ""}, {&apiRollout{Installed: "v2", Serving: "v1", Awake: true, HeldBy: "v2"}, fmt.Sprintf(rolloutwords.StateHeld, "v2")}, // Plan 5 reports no serving build for an asleep instance: its last // good build is what it ran. {&apiRollout{Installed: "v2", Good: "v1", HeldBy: "v2"}, fmt.Sprintf(rolloutwords.StateHeld, "v2")}, {&apiRollout{Installed: "v2", Good: "v1"}, fmt.Sprintf(rolloutwords.StateAsleep, "v2")}, {&apiRollout{Installed: "v2", Good: "v2"}, ""}, {&apiRollout{Installed: "v2", Serving: "v1", Awake: true}, rolloutwords.StateRollingOut}, {&apiRollout{Installed: "v2", Serving: "v2", Awake: true, ConfigPending: true}, rolloutwords.StateConfigPending}, // A box that cannot arm says why, and nothing else: awake and the // other flags are meaningless false, and a hold does not outrank it. {&apiRollout{Installed: "v2", Unavailable: "no-release-key"}, rolloutwords.Unavailable("no-release-key")}, {&apiRollout{Installed: "v2", Unavailable: "generations-off"}, rolloutwords.Unavailable("generations-off")}, {&apiRollout{Installed: "v2", Unavailable: "generations-off", HeldBy: "v2"}, rolloutwords.Unavailable("generations-off")}, } { if got := rolloutReason(tc.r); got != tc.want { t.Errorf("rolloutReason(%+v) = %q, want %q", tc.r, got, tc.want) } } if rolloutReason(&apiRollout{Installed: "v2", Unavailable: "no-release-key"}) == rolloutReason(&apiRollout{Installed: "v2", Unavailable: "generations-off"}) { t.Error("a refusing and a declined code read the same") } if anyHeld([]apiInstanceRecord{{}, {Rollout: &apiRollout{}}}) || !anyHeld([]apiInstanceRecord{{Rollout: &apiRollout{HeldBy: "v2"}}}) { t.Error("anyHeld is wrong") } } // TestRolloutWireShape pins the JSON the CLI reads from Task 8's console: the // rollout join on a list record (including the unavailable code) and held_until // on promote's 200. func TestRolloutWireShape(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "/promote") { fmt.Fprint(w, `{"result":"promoted","held":true,"held_until":"2026-10-08T10:00:00Z"}`) return } fmt.Fprint(w, `{"instances":[ {"host":"a.example","staged_version":"v1","rollout":{"box":"b1","installed":"v2","serving":"v1","good":"v1","awake":true,"held_by":"v2","reason":"health","config_pending":true,"scheduler_idle":true}}, {"host":"b.example","staged_version":"v1","rollout":{"box":"b2","installed":"v2","unavailable":"no-release-key"}}, {"host":"c.example","staged_version":"v1"}],"truncated":false,"total":3}`) })) defer ts.Close() s := newTestSession(ts.URL) recs, _, _, err := s.instanceList(context.Background(), apiAcct{Seg: "acct"}, "app", "", "") if err != nil { t.Fatalf("instanceList: %v", err) } want := &apiRollout{Box: "b1", Installed: "v2", Serving: "v1", Good: "v1", Awake: true, HeldBy: "v2", Reason: "health", ConfigPending: true, SchedulerIdle: true} if len(recs) != 3 || recs[0].Rollout == nil || *recs[0].Rollout != *want { t.Fatalf("rollout decoded as %+v", recs) } if recs[1].Rollout == nil || recs[1].Rollout.Unavailable != "no-release-key" { t.Errorf("unavailable not decoded: %+v", recs[1].Rollout) } if recs[2].Rollout != nil { t.Errorf("a record with no rollout decoded one: %+v", recs[2].Rollout) } // Flag off: installed prints the staged version, serving and reason a dash and nothing. if installedCell(recs[2]) != "v1" || servingCell(recs[2]) != "—" || rolloutReason(recs[2].Rollout) != "" { t.Errorf("no-rollout row: %q %q %q", installedCell(recs[2]), servingCell(recs[2]), rolloutReason(recs[2].Rollout)) } res, err := s.promote(context.Background(), apiAcct{Seg: "acct"}, "app", "v2", "stable", false, false) if err != nil { t.Fatalf("promote: %v", err) } if !res.Held || res.HeldUntil != "2026-10-08T10:00:00Z" { t.Errorf("promote result = %+v", res) } }