package console // claim_invariant_regionha_test.go covers what region HA (CARLOS_REGION_HA, // Config.RegionHA) adds to a host-claim console's routes, on a console built // with it on: the operator's box and group role doors (region HA spec 2 §7, // apiboxroles.go). With region HA off they are not registered, so the base // console's census (testdata/claim-routes.txt) is main's (Astra 2a final // review, P3). Each is a nonApp entry, probed here on a region-HA console. import ( "context" "net/http" "os" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/release" ) // claimRegionHAOn builds the console the way cmd/console builds it with // CARLOS_REGION_HA on (the switch is read before NewServer). func claimRegionHAOn(c *Config) { c.RegionHA = true } // claimRegionHAFixtures are the entries that exist only on a region-HA // console. var claimRegionHAFixtures = map[string]claimFixture{ "route POST /api/cli/system/boxes/{box}/role": claimNonApp("operator box role (region HA spec 2 §7): one signed roster write to control/host-actors.json; names no app"), "route POST /api/cli/system/groups/{region}/role": claimNonApp("operator group role (region HA spec 2 §7): one signed roster write to control/host-actors.json, the group and the boxes it enrolled; names no app"), } // claimRegionHAProbes drive the entries above: one signed roster write each. var claimRegionHAProbes = map[string]claimProbe{ "route POST /api/cli/system/boxes/{box}/role": { do: claimAPIReply(http.MethodPost, "/api/cli/system/boxes/box-ie-default/role", `{"role":"legacy"}`), body: []string{`"role":"legacy"`}, effect: func(t *testing.T, cx *claimCase, r claimReply) string { o, err := cx.claimFixtureDirectory().Read(context.Background()) if err != nil || o.Roster.Boxes["box-ie-default"].Role != hostactors.RoleLegacy { return "box-ie-default does not have the legacy role in the signed roster" } return "" }, }, "route POST /api/cli/system/groups/{region}/role": { setup: func(t *testing.T, cx *claimCase) { ctx := context.Background() o, err := cx.claimFixtureDirectory().Read(ctx) if err != nil { t.Fatal(err) } next := hostactors.Clone(o.Roster) g := next.Regions["ie"] g.GroupName, g.GroupARN = "routers", "arn:aws:autoscaling:eu-west-1:123456789012:autoScalingGroup:routers" next.Regions["ie"] = g b := next.Boxes["box-ie-default"] b.Group = "routers" next.Boxes["box-ie-default"] = b next.Revision++ raw, err := release.SignRootEnvelope(release.RootSigner(cx.priv), next) if err != nil { t.Fatal(err) } if _, err = cx.rel.Put(ctx, hostactors.Key, raw, o.ETag); err != nil { t.Fatal(err) } }, do: claimAPIReply(http.MethodPost, "/api/cli/system/groups/ie/role", `{"role":"legacy"}`), body: []string{`"boxes":["box-ie-default"]`}, effect: func(t *testing.T, cx *claimCase, r claimReply) string { o, err := cx.claimFixtureDirectory().Read(context.Background()) if err != nil || o.Roster.Regions["ie"].Role != hostactors.RoleLegacy || o.Roster.Boxes["box-ie-default"].Role != hostactors.RoleLegacy { return "the group in ie and its box do not have the legacy role in the signed roster" } return "" }, }, } // claimRegionHAMerged is the fixture table a region-HA console must match // exactly, and claimRegionHAMergedProbes its probe table. func claimRegionHAMerged() map[string]claimFixture { merged := map[string]claimFixture{} for k, v := range claimFixtures { merged[k] = v } for k, v := range claimRegionHAFixtures { merged[k] = v } return merged } func claimRegionHAMergedProbes() map[string]claimProbe { merged := map[string]claimProbe{} for k, v := range claimProbes { merged[k] = v } for k, v := range claimRegionHAProbes { merged[k] = v } return merged } // TestClaimRegionHAConsoleRouteCensus: a region-HA console serves exactly // what an enabled console serves plus the entries in // testdata/claim-routes-region-ha.txt, and every one has a fixture and a // probe. The base console registers none of them. func TestClaimRegionHAConsoleRouteCensus(t *testing.T) { b, err := os.ReadFile("testdata/claim-routes-region-ha.txt") if err != nil { t.Fatal(err) } extra := strings.Split(strings.TrimSpace(string(b)), "\n") base := claimInventoryKeys(newClaimCase(t).srv.claimEntries.snapshot()) for _, k := range extra { if slices.Contains(base, k) { t.Fatalf("%s is registered with region HA off", k) } if _, ok := claimRegionHAFixtures[k]; !ok { t.Errorf("%s has no region HA fixture", k) } } cx := newClaimCaseWith(t, claimRegionHAOn) if !cx.srv.cfg.RegionHA || !cx.srv.cfg.HostClaim { t.Fatal("the region HA console is not built with both switches on") } want := append(slices.Clone(base), extra...) slices.Sort(want) entries := cx.srv.claimEntries.snapshot() got := claimInventoryKeys(entries) if !slices.Equal(got, want) { for _, k := range got { if !slices.Contains(want, k) { t.Errorf("new with region HA: %s", k) } } for _, k := range want { if !slices.Contains(got, k) { t.Errorf("missing with region HA: %s", k) } } t.FailNow() } for _, p := range claimFixtureCensus(entries, claimRegionHAMerged()) { t.Error(p) } for _, p := range claimProbeCensus(claimRegionHAMerged(), claimRegionHAMergedProbes()) { t.Error(p) } for key := range claimRegionHAProbes { if _, ok := claimProbes[key]; ok { t.Errorf("%s is probed on the base console too", key) } } } // TestClaimRegionHAEntriesAreProbed drives every region-HA-only entry's // probe on a console built with region HA on. func TestClaimRegionHAEntriesAreProbed(t *testing.T) { runClaimProbes(t, claimRegionHAFixtures, claimRegionHAProbes, 0, claimRegionHAOn) }