package catalogue import ( "encoding/json" "strings" "testing" ) // A consumer of a provision that keeps its data stays bound where its data is (novox/hq ADR 0232, // issue 273). // // The incident, exactly: a machine runs its own store and five consumers of it; the mesh's store seat // is held by the store on another machine. Issue 258's rule — the seat's holder elsewhere answers // before this machine's own provider — re-bound all five to the holder, each was made a fresh, empty // database there, and nothing said so. var incidentConsumers = []string{"board", "listings", "workflows", "agents", "game"} func storeMesh() map[string]Manifest { shelf := map[string]Manifest{ "store": {Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh}}, Claims: []Claim{{Name: "mesh-store", Scope: ScopeMesh}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}, Grants: map[string]string{"postgres-database": "/grants"}}, "resolver": {Module: "resolver", Version: "1", Provides: []Offer{{Name: "wildcard-resolution", Scope: ScopeMesh}}, Claims: []Claim{{Name: "mesh-dns-resolver", Scope: ScopeMesh}}}, "network": {Module: "network", Version: "1", Requires: []string{"wildcard-resolution"}}, } for _, c := range incidentConsumers { shelf[c] = 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() World { w := World{Offered: map[string][]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 = []Held{ {Claim: "mesh-store", Scope: ScopeMesh, Node: "anchor", Module: "store"}, {Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "anchor", Module: "resolver"}, } w.Holdings = w.Held return w } var home = Node{Name: "home", At: "home.internal"} func homeAssigned() []string { return append([]string{"store", "resolver", "network"}, incidentConsumers...) } func boundFrom(t *testing.T, r Resolution, consumer, provision string) 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 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]Chosen{} for _, c := range incidentConsumers { w.Bound[c] = map[string]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]Chosen{"postgres-database": {Node: "anchor", Module: "store"}} w.Bound = map[string]map[string]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) World { w := storeWorld() w.Held = []Held{{Claim: "mesh-store", Scope: ScopeMesh, Node: holder, Module: "store"}} w.Holdings = w.Held return w } var laptop = Node{Name: "laptop", At: "laptop.internal"} func TestABoundConsumerStaysWhenTheSeatsHolderChanges(t *testing.T) { w := laptopWorld("home") w.Bound = map[string]map[string]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 != (Chosen{"anchor", "store"}) || k.Would != (Chosen{"home", "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]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]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]Chosen{"board": {"postgres-database": {Node: "gone", Module: "store"}}} if _, err := Resolve(storeMesh(), []string{"board"}, laptop, w); err != nil { t.Fatal(err) } } func TestAnOfferSaysWhetherItKeepsConsumerData(t *testing.T) { var o Offer if err := json.Unmarshal([]byte(`{"name":"wildcard-resolution","scope":"mesh","keeps-consumer-data":false}`), &o); err != nil { t.Fatal(err) } if o.KeepsConsumerData == nil || *o.KeepsConsumerData { t.Fatalf("read %+v", o) } out, err := json.Marshal(o) if err != nil || !strings.Contains(string(out), `"keeps-consumer-data":false`) { t.Fatalf("wrote %s, %v", out, err) } yes, no := true, false granting := Manifest{Module: "g", Provides: []Offer{{Name: "p", Scope: ScopeMesh}}, Grants: map[string]string{"p": "/g"}} if !granting.KeepsConsumerData("p") { t.Error("a provider granting each consumer a credential keeps what it writes, unsaid") } if (Manifest{Module: "r", Provides: []Offer{{Name: "p", Scope: ScopeMesh}}}).KeepsConsumerData("p") { t.Error("a provider granting nothing keeps nothing, unsaid") } granting.Provides[0].KeepsConsumerData = &no if granting.KeepsConsumerData("p") { t.Error("what an offer says, it gets") } said := Manifest{Module: "s", Provides: []Offer{{Name: "p", Scope: ScopeMesh, KeepsConsumerData: &yes}}} if !said.KeepsConsumerData("p") || !KeepsConsumerData(map[string]Manifest{"s": said, "r": {Module: "r", Provides: []Offer{{Name: "p", Scope: ScopeMesh, KeepsConsumerData: &no}}}}, "p") { t.Error("a name any provider says keeps data keeps data") } } // 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 = []Offer{{Name: "postgres-database", Scope: 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) } }