package console // claim_invariant_moves_test.go covers what the Phase 1 moves switch // (CARLOS_MOVES_PHASE1, Config.MovesEnabled) adds to the console, on a console // built with the switch on: // // - one route, the operator's resolve door, registered only with the switch // (registerMovesPhase1Routes). It is intentionally unavailable: with the // switch on its body is not built, so a valid operator request for a real // instance is answered the exact sentence below and writes nothing. That is // a refused fixture, not a probe: only an exact status and message from // claimUnsupported may stand for a route that does not work yet. // - one worker, PublishMovesConsole (control/moves-console.json), registered // in every console and a no-op with the switch off. Its probe is in // claim_invariant_probes_worker_test.go, with the switch turned on. import ( "net/http" "os" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/moves" ) // claimMoveResolveUnavailableMsg is what the resolve door answers an operator // until Plan 4 Task 10 and Plan 6 Task 2 give it its body. const claimMoveResolveUnavailableMsg = "nothing to resolve yet" const claimMoveResolveKey = "route POST /api/cli/apps/{account}/{app}/instances/{host}/move/resolve" // claimMovesOn builds the console the way cmd/console builds it with the // switch on (applyMovesSwitch runs before NewServer). func claimMovesOn(c *Config) { c.MovesEnabled = true } // claimMovesFixtures are the entries that exist only on a moves-on console. var claimMovesFixtures = map[string]claimFixture{ claimMoveResolveKey: {refused: &claimRefusalFixture{ apps: claimRefs("bab/titogo"), setup: func(t *testing.T, cx *claimCase) { cx.movableInstance(t) }, do: claimRefusedAPI(http.MethodPost, "/api/cli/apps/bab/titogo/instances/acme.go.tito.io/move/resolve", `{}`), status: http.StatusConflict, msg: claimMoveResolveUnavailableMsg, }}, } // claimMovesMerged is the fixture table a moves-on console must match exactly. func claimMovesMerged() map[string]claimFixture { merged := map[string]claimFixture{} for k, v := range claimFixtures { merged[k] = v } for k, v := range claimMovesFixtures { merged[k] = v } return merged } // TestClaimMovesConsoleRouteCensus: a moves-on console serves exactly what an // enabled console serves plus the entries in testdata/claim-routes-moves.txt, // and every one has a fixture. func TestClaimMovesConsoleRouteCensus(t *testing.T) { base := claimInventoryKeys(newClaimCase(t).srv.claimEntries.snapshot()) if slices.Contains(base, claimMoveResolveKey) { t.Fatalf("%s is registered with the moves switch off", claimMoveResolveKey) } cx := newClaimCaseWith(t, claimMovesOn) if !cx.srv.cfg.MovesEnabled || !cx.srv.cfg.HostClaim { t.Fatal("the moves console is not built with both switches on") } b, err := os.ReadFile("testdata/claim-routes-moves.txt") if err != nil { t.Fatal(err) } want := append(slices.Clone(base), strings.Split(strings.TrimSpace(string(b)), "\n")...) 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 the moves switch: %s", k) } } for _, k := range want { if !slices.Contains(got, k) { t.Errorf("missing with the moves switch: %s", k) } } t.FailNow() } for _, p := range claimFixtureCensus(entries, claimMovesMerged()) { t.Error(p) } for key, f := range claimMovesFixtures { if f.door == nil && f.refused == nil { t.Errorf("%s: a moves-only entry is a door or an exact refusal; a probe table is the base console's", key) } } } // TestClaimMovesEntriesAreCovered drives every moves-only entry on a moves-on // console. func TestClaimMovesEntriesAreCovered(t *testing.T) { for key, f := range claimMovesFixtures { t.Run(key, func(t *testing.T) { if f.refused == nil { t.Skip("a door is driven by TestClaimConsoleMutationInvariant") } for _, p := range runClaimRefusal(t, f.refused, claimMovesOn) { t.Error(p) } }) } } // With the switch off the worker publishes nothing: the entry is registered in // every console, and only the switch makes it write. func TestClaimPublishMovesConsoleIsInertWithTheSwitchOff(t *testing.T) { cx := newClaimCase(t) if cx.srv.cfg.MovesEnabled { t.Fatal("the base console has the moves switch on") } m := cx.rec.mark() claimWorkerReply("PublishMovesConsole")(t, cx) cx.quiesce() if cx.bucketHas(moves.ConsoleKey) { t.Fatalf("%s published with the moves switch off", moves.ConsoleKey) } if events := cx.rec.since(m); len(events) != 0 { t.Fatalf("the worker wrote with the moves switch off: %v", events) } } // TestClaimMovesConsoleBindsTheMovePass (moves Plan 5 Task 7): a moves-on // claim console binds its move pass as claim Plan 10's Recovery.Moves hook, // with the lost target's stamp; it adds no route and no worker (the pass runs // inside StartClaimRecoveryPass). A moves-off console binds neither, so the // recovery pass skips move claims as main does. func TestClaimMovesConsoleBindsTheMovePass(t *testing.T) { off := newClaimCase(t).srv if off.cfg.ClaimMoveRecovery != nil || off.cfg.HostClaimRecovery == nil || off.cfg.HostClaimRecovery.Moves != nil { t.Fatal("a moves-off console bound the move pass") } on := newClaimCaseWith(t, claimMovesOn).srv if on.cfg.ClaimMoveRecovery == nil || on.cfg.HostClaimRecovery == nil || on.cfg.HostClaimRecovery.Moves == nil { t.Fatal("a moves-on console did not bind the move pass") } if on.cfg.HostClaimRecovery.MoveFenceAuthority == nil { t.Fatal("the move pass has no fence authority for its conversions") } }