package resolution import ( "strings" "testing" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) var incidentConsumers = []string{"board", "listings", "workflows", "agents", "game"} func storeMesh() map[string]mcat.Manifest { shelf := map[string]mcat.Manifest{ "store": {Module: "store", Version: "1", Provides: []mcat.Offer{{Name: "postgres-database", Scope: mcat.ScopeMesh}}, Claims: []mcat.Claim{{Name: "mesh-store", Scope: mcat.ScopeMesh}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}, Grants: map[string]string{"postgres-database": "/grants"}}, "resolver": {Module: "resolver", Version: "1", Provides: []mcat.Offer{{Name: "wildcard-resolution", Scope: mcat.ScopeMesh}}, Claims: []mcat.Claim{{Name: "mesh-dns-resolver", Scope: mcat.ScopeMesh}}}, "network": {Module: "network", Version: "1", Requires: []string{"wildcard-resolution"}}, } for _, c := range incidentConsumers { shelf[c] = mcat.Manifest{Module: c, Version: "1", Requires: []string{"postgres-database"}} } return shelf } // storeWorld is the rest of the mesh as the home server's plan sees it: the anchor runs a store and a // resolver and holds both seats; the home server runs its own of each. func storeWorld() mcat.World { w := mcat.World{Offered: map[string][]mcat.Provider{ "postgres-database": { {Node: "anchor", At: "anchor.internal", Module: "store", Serves: map[string]any{"port": 5432}}, {Node: "home", At: "home.internal", Module: "store", Serves: map[string]any{"port": 5434}}, }, "wildcard-resolution": { {Node: "anchor", At: "anchor.internal", Module: "resolver"}, {Node: "home", At: "home.internal", Module: "resolver"}, }, }} w.Held = []mcat.Held{ {Claim: "mesh-store", Scope: mcat.ScopeMesh, Node: "anchor", Module: "store"}, {Claim: "mesh-dns-resolver", Scope: mcat.ScopeMesh, Node: "anchor", Module: "resolver"}, } w.Holdings = w.Held return w } var home = mcat.Node{Name: "home", At: "home.internal"} func homeAssigned() []string { return append([]string{"store", "resolver", "network"}, incidentConsumers...) } func boundFrom(t *testing.T, r mcat.Resolution, consumer, provision string) mcat.Needed { t.Helper() for _, n := range r.Needs { if n.For == consumer && n.Name == provision { return n } } t.Fatalf("no binding of %s for %s: %+v", consumer, provision, r.Needs) return mcat.Needed{} } func TestTheIncidentAMachinesOwnStoreKeepsItsConsumersWhenTheSeatIsHeldElsewhere(t *testing.T) { got, err := Resolve(storeMesh(), homeAssigned(), home, storeWorld()) if err != nil { t.Fatal(err) } for _, c := range incidentConsumers { if n := boundFrom(t, got, c, "postgres-database"); n.From != "home" || n.Module != "store" { t.Errorf("%s bound to %s/%s; its data is on the store beside it (issue 273)", c, n.From, n.Module) } } // And the resolver, which keeps nothing of anybody's, still answers from the seat's holder (258). if n := boundFrom(t, got, "network", "wildcard-resolution"); n.From != "anchor" { t.Errorf("the resolver bound to %s; the seat is held on anchor (issue 258)", n.From) } if len(got.Kept) != 0 { t.Errorf("nothing was moved, and %d kept: %+v", len(got.Kept), got.Kept) } } func TestTheIncidentWithEveryConsumerOnRecordStillMovesNothing(t *testing.T) { w := storeWorld() w.Bound = map[string]map[string]mcat.Chosen{} for _, c := range incidentConsumers { w.Bound[c] = map[string]mcat.Chosen{"postgres-database": {Node: "home", Module: "store"}} } got, err := Resolve(storeMesh(), homeAssigned(), home, w) if err != nil { t.Fatal(err) } for _, c := range incidentConsumers { if n := boundFrom(t, got, c, "postgres-database"); n.From != "home" { t.Errorf("%s bound to %s", c, n.From) } } if len(got.Kept) != 0 { t.Errorf("kept %+v", got.Kept) } } func TestAPinElsewhereStillMovesAStoresConsumers(t *testing.T) { w := storeWorld() w.Pinned = map[string]mcat.Chosen{"postgres-database": {Node: "anchor", Module: "store"}} w.Bound = map[string]map[string]mcat.Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}} got, err := Resolve(storeMesh(), homeAssigned(), home, w) if err != nil { t.Fatal(err) } if n := boundFrom(t, got, "board", "postgres-database"); n.From != "anchor" || n.Module != "store" { t.Errorf("bound to %s/%s; a person pinned it to anchor", n.From, n.Module) } if len(got.Kept) != 0 { t.Errorf("a pin is a person's move, not one to keep: %+v", got.Kept) } } // A consumer on a machine with no store of its own, bound to one store, when the seat moves to // another: the holder would answer, and the binding stays. func laptopWorld(holder string) mcat.World { w := storeWorld() w.Held = []mcat.Held{{Claim: "mesh-store", Scope: mcat.ScopeMesh, Node: holder, Module: "store"}} w.Holdings = w.Held return w } var laptop = mcat.Node{Name: "laptop", At: "laptop.internal"} func TestABoundConsumerStaysWhenTheSeatsHolderChanges(t *testing.T) { w := laptopWorld("home") w.Bound = map[string]map[string]mcat.Chosen{"board": {"postgres-database": {Node: "anchor", Module: "store"}}} got, err := Resolve(storeMesh(), []string{"board"}, laptop, w) if err != nil { t.Fatal(err) } n := boundFrom(t, got, "board", "postgres-database") if n.From != "anchor" || n.At != "anchor.internal" || n.Serves["port"] != 5432 { t.Fatalf("bound to %s at %s %v; its data is on anchor", n.From, n.At, n.Serves) } if len(got.Kept) != 1 { t.Fatalf("the move was not said: %+v", got.Kept) } k := got.Kept[0] if k.Bound != (mcat.Chosen{Node: "anchor", Module: "store"}) || k.Would != (mcat.Chosen{Node: "home", Module: "store"}) || k.Consumer != "board" { t.Fatalf("kept %+v", k) } for _, want := range []string{"would move board's postgres-database from anchor/store to home/store", "its data is on anchor/store", "pin laptop postgres-database home store", "move the data first"} { if !strings.Contains(k.String(), want) { t.Errorf("%q does not say %q", k.String(), want) } } // Without a record it is a binding being made, and the holder answers it as before. w.Bound = nil got, err = Resolve(storeMesh(), []string{"board"}, laptop, w) if err != nil { t.Fatal(err) } if n := boundFrom(t, got, "board", "postgres-database"); n.From != "home" { t.Errorf("a new consumer bound to %s; the seat is held on home", n.From) } } func TestABoundConsumerWhoseProviderIsGoneIsRefusedNotMoved(t *testing.T) { w := laptopWorld("anchor") w.Offered["postgres-database"] = w.Offered["postgres-database"][:1] // only anchor's store is left w.Bound = map[string]map[string]mcat.Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}} _, err := Resolve(storeMesh(), []string{"board"}, laptop, w) if err == nil { t.Fatal("bound to home's store, which is gone, and answered by anchor's — the silent move") } for _, want := range []string{"board on laptop is bound to home/store", "home/store no longer provides it", "pin laptop postgres-database anchor store"} { if !strings.Contains(err.Error(), want) { t.Errorf("%q does not say %q", err, want) } } } func TestAMachinesOwnStoreUnassignedRefusesItsBoundConsumers(t *testing.T) { w := storeWorld() w.Bound = map[string]map[string]mcat.Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}} _, err := Resolve(storeMesh(), []string{"board"}, home, w) if err == nil || !strings.Contains(err.Error(), "store no longer runs on home") { t.Fatalf("got %v", err) } } // The first pass asks only what a machine offers, and is never refused by a binding. func TestTheFirstPassKeepsNoBinding(t *testing.T) { w := storeWorld() w.Unchecked = true w.Bound = map[string]map[string]mcat.Chosen{"board": {"postgres-database": {Node: "gone", Module: "store"}}} if _, err := Resolve(storeMesh(), []string{"board"}, laptop, w); err != nil { t.Fatal(err) } } // An offer saying it keeps nothing is answered by the seat's holder as the resolver is, even where it // grants a credential. func TestAStoreSayingItKeepsNothingFollowsTheSeat(t *testing.T) { shelf := storeMesh() no := false s := shelf["store"] s.Provides = []mcat.Offer{{Name: "postgres-database", Scope: mcat.ScopeMesh, KeepsConsumerData: &no}} shelf["store"] = s got, err := Resolve(shelf, homeAssigned(), home, storeWorld()) if err != nil { t.Fatal(err) } if n := boundFrom(t, got, "board", "postgres-database"); n.From != "anchor" { t.Errorf("bound to %s; it keeps nothing, and the seat is held on anchor", n.From) } }