package broker import ( "strings" "testing" ) // Whether a mesh could move its bus. // // Every case here is a way of moving that leaves something behind, and the one that matters most is a // machine with no credential: after the move there is no bus to ask it over, so it is lost until // somebody walks to it. func aMeshReadyToMove() Readiness { return Readiness{ TheBus: "nats://127.0.0.1:5671", ServerStanding: true, Holder: "anchor", AccountsComposed: true, Nodes: []string{"anchor", "laptop"}, Credentialled: map[string]bool{"anchor": true, "laptop": true}, Modules: []string{"anchor/gitea"}, ModuleCredentialled: map[string]bool{"anchor/gitea": true}, } } func TestAMeshWithEverythingInPlaceIsReady(t *testing.T) { if why := NotReady(aMeshReadyToMove()); len(why) != 0 { t.Fatalf("a mesh with everything in place was refused: %v", why) } } // **A machine with no credential is the one that must stop this.** It keeps running and cannot come // back, and there is no bus left to tell it anything over — so the remedy has to happen before, and // the message says so. func TestAMachineWithNoCredentialStopsTheMove(t *testing.T) { r := aMeshReadyToMove() r.Credentialled = map[string]bool{"anchor": true} why := NotReady(r) if len(why) == 0 { t.Fatal("a machine that could not come back did not stop the move") } said := strings.Join(why, "\n") if !strings.Contains(said, "laptop") { t.Errorf("the refusal does not name the machine: %s", said) } if !strings.Contains(said, "before the move") { t.Errorf("the refusal does not say the remedy comes first: %s", said) } } // A bus nobody has raised, a seat nobody holds, and a user list nobody has been sent: each stops it, // and each names its own next step, because "not ready" that cannot be acted on is not an answer. func TestEachThingMissingNamesItsOwnRemedy(t *testing.T) { for _, c := range []struct { what string break_ func(*Readiness) says string }{ {"no address", func(r *Readiness) { r.TheBus = "" }, NATSVar}, {"no server", func(r *Readiness) { r.ServerStanding = false }, "carrying nothing"}, {"no holder", func(r *Readiness) { r.Holder = "" }, "mesh-broker"}, {"no user list", func(r *Readiness) { r.AccountsComposed = false }, "push anchor"}, {"a module with none", func(r *Readiness) { r.ModuleCredentialled = map[string]bool{} }, "anchor/gitea"}, } { r := aMeshReadyToMove() c.break_(&r) why := NotReady(r) if len(why) == 0 { t.Errorf("%s did not stop the move", c.what) continue } if !strings.Contains(strings.Join(why, "\n"), c.says) { t.Errorf("%s: the refusal does not mention %q: %v", c.what, c.says, why) } } } // What the move would do is written out rather than summarised, because this is the one step with // nothing to inspect afterwards — so reading it is the last chance to disagree. func TestWhatMovesNamesEveryMachineAndSaysTheOldBrokerStays(t *testing.T) { r := aMeshReadyToMove() r.OldBusHasOtherClients = true steps := strings.Join(WhatMoves(r), "\n") for _, want := range []string{"anchor", "laptop", "user list", "module runtime"} { if !strings.Contains(steps, want) { t.Errorf("the plan does not mention %q:\n%s", want, steps) } } // Said explicitly, so nobody reads the move as switching the old broker off — it stays serving // whatever else uses it, and that is a decision already taken. if !strings.Contains(steps, "not its retirement") { t.Errorf("the plan does not say the old broker stays:\n%s", steps) } }