package delivery import ( "errors" "reflect" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // ADR 0549 §1, §2: the placement key and file, the value's refusals, its reading as the composition reads a node, what // a change says, and the catalogue's check of every file. func TestPlacement(t *testing.T) { t.Run("key and file", func(t *testing.T) { key, err := PlacementKey("g14") if err != nil || key != "placement.g14" { t.Fatalf("PlacementKey = %q, %v", key, err) } if n, err := ParsePlacementKey(key); err != nil || n != "g14" { t.Fatalf("ParsePlacementKey = %q, %v", n, err) } for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b"} { if _, err := ParsePlacementKey(bad); err == nil { t.Errorf("ParsePlacementKey(%q) took it", bad) } } if _, err := PlacementKey("a.b"); err == nil { t.Error("PlacementKey took a dotted node") } if PlacementBucket != "mesh-delivery-indexer_placement" || PlacementFile("ace") != "placement/ace.json" { t.Fatalf("bucket %q, file %q", PlacementBucket, PlacementFile("ace")) } for p, want := range map[string]string{"placement/ace.json": "ace", "./placement/ace.json": "ace", "placement/x/ace.json": "", "modules/ace.json": "", "placement/ace.yaml": ""} { if n, ok := PlacementFileNode(p); n != want || ok != (want != "") { t.Errorf("PlacementFileNode(%q) = %q, %v", p, n, ok) } } }) g14 := Placement{Node: "g14", Revision: "abc", Decided: NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}}, Modules: []PlacedModule{ {Module: "zsh"}, {Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "novox", Module: "postgres"}}}, }, Pins: map[string]Provider{"shell": {Node: "g14", Module: "zsh"}}, Holds: []Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}}, } novox := Placement{Node: "novox", Decided: NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []PlacedModule{{Module: "postgres", Ports: map[string]int{"5432": 5432}}}, Holds: []Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}} t.Run("value", func(t *testing.T) { raw, err := EncodePlacement(g14) if err != nil { t.Fatal(err) } back, err := DecodePlacement(raw) if err != nil || back.V != Version || !SamePlacement(back, g14) { t.Fatalf("round trip: %+v, %v", back, err) } moved := g14 moved.Revision = "def" if !SamePlacement(g14, moved) { t.Error("a new revision alone counts as a change") } if _, err := DecodePlacement([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) { t.Errorf("version 9: %v", err) } for name, bad := range map[string]string{ "module twice": `{"v":1,"node":"g14","modules":[{"module":"a"},{"module":"a"}]}`, "port not number": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"http":80}}]}`, "host port zero": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"80":0}}]}`, "binding no node": `{"v":1,"node":"g14","modules":[{"module":"a","bindings":{"pg":{"module":"pg"}}}]}`, "pin no module": `{"v":1,"node":"g14","modules":[],"pins":{"pg":{"node":"x"}}}`, "hold not placed": `{"v":1,"node":"g14","modules":[],"holds":[{"claim":"s","module":"a"}]}`, "node not a name": `{"v":1,"node":"g.14","modules":[]}`, "value not shaped": `{"v":1,"node":"g14","modules":{}}`, } { if _, err := DecodePlacement([]byte(bad)); err == nil { t.Errorf("%s: taken", name) } } }) t.Run("as the composition reads it", func(t *testing.T) { if got := g14.Assigned(); !reflect.DeepEqual(got, []string{"zsh", "app"}) { t.Errorf("Assigned = %v, want the file's order", got) } if got := g14.ChosenPins(); !reflect.DeepEqual(got, map[string]catalogue.Chosen{"shell": {Node: "g14", Module: "zsh"}}) { t.Errorf("ChosenPins = %v", got) } if got := g14.Bound(); !reflect.DeepEqual(got, map[string]map[string]catalogue.Chosen{"app": {"postgres": {Node: "novox", Module: "postgres"}}}) { t.Errorf("Bound = %v", got) } if got := g14.HostPorts(); !reflect.DeepEqual(got, map[string]map[int]int{"app": {8080: 18080}}) { t.Errorf("HostPorts = %v", got) } if got := g14.Held(); !reflect.DeepEqual(got, []catalogue.Held{{Claim: "node-shell", Scope: "node", Node: "g14", Module: "zsh"}}) { t.Errorf("Held = %v", got) } layered := PlacedModule{Module: "app", HasMesh: true, Layer: map[string]any{"expose": map[string]any{"80": "mesh"}}} want := []catalogue.Layer{ {From: catalogue.MeshWideLayer, Values: map[string]any{}}, {From: "g14", Values: map[string]any{"mode": "x"}, Placement: map[string]any{"expose": map[string]any{"80": "mesh"}}}, } if got := layered.Layers("g14", map[string]any{"mode": "x"}); !reflect.DeepEqual(got, want) { t.Errorf("Layers = %#v, want %#v", got, want) } if got := (PlacedModule{Module: "a"}).Layers("g14", map[string]any{"k": 1}); len(got) != 1 || got[0].From != "g14" { t.Errorf("values alone make the node's layer: %#v", got) } if got := (PlacedModule{Module: "a", HasLayer: true}).Layers("g14", nil); len(got) != 1 || got[0].Values == nil { t.Errorf("an empty layer of the node's own is kept, with empty values: %#v", got) } if got := (PlacedModule{Module: "a"}).Layers("g14", nil); got != nil { t.Errorf("no layer and no value is no layer: %#v", got) } empty := Placement{Node: "x"} if empty.ChosenPins() != nil || empty.Bound() != nil || empty.HostPorts() != nil || len(empty.Held()) != 0 { t.Error("an empty placement reads as something") } }) t.Run("what a change says", func(t *testing.T) { after := g14 after.Revision = "def" after.Modules = []PlacedModule{{Module: "app", Ports: map[string]int{"8080": 18081}}, {Module: "new"}} got := ChangeOf(g14, after) want := PlacementChange{Node: "g14", Revision: "def", Added: []string{"new"}, Removed: []string{"zsh"}, Changed: []string{"app"}} if !reflect.DeepEqual(got, want) { t.Errorf("ChangeOf = %+v, want %+v", got, want) } pinned := g14 pinned.Pins = nil if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) { t.Errorf("a pin change changes every module on the node: %+v", got) } if got := ChangeOf(Placement{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" { t.Errorf("a new node: %+v", got) } }) t.Run("the catalogue's check", func(t *testing.T) { held := func(m string) bool { return m != "ghost" } enrolled := func(n string) bool { return n != "stranger" } if r := CheckPlacements(map[string]Placement{"g14": g14, "novox": novox}, held, enrolled); len(r) != 0 { t.Fatalf("a sound placement is refused: %v", r) } clash := g14 clash.Modules = append([]PlacedModule{{Module: "ghost", Ports: map[string]int{"80": 18080}}}, g14.Modules...) clash.Modules[2] = PlacedModule{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "ace", Module: "postgres"}}} clash.Pins = map[string]Provider{"shell": {Node: "g14", Module: "bash"}} twice := novox twice.Node = "stranger" twice.Decided.Address = "10.77.0.2" r := CheckPlacements(map[string]Placement{"g14": clash, "novox": novox, "stranger": twice}, held, enrolled) for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080", "bound to ace/postgres", "pin of shell names g14/bash", "stranger is not an enrolled node", "mesh-store, held once for the mesh, is held on novox and stranger", "g14 and stranger both have the address 10.77.0.2", "novox, stranger are each placed as it"} { found := false for _, line := range r { found = found || strings.Contains(line, want) } if !found { t.Errorf("no refusal says %q; got %v", want, r) } } }) }