package composer import ( "encoding/json" "errors" "os" "reflect" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" ) const demo = `{"module": "demo", "version": "1", "claims": [{"name": "node-files"}]}` // A manifest named by its digest goes on the wire as {"digest": ...}, and Fill puts the bytes the indexer keeps in its // place; a raw manifest goes as itself. func TestAManifestIsItsBytesOrItsDigest(t *testing.T) { r := Request{V: Version, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{ "demo": {Digest: "d1"}, "raw": {Raw: json.RawMessage(`{"module":"raw","version":"1"}`)}}, Kept: map[string]map[string]Manifest{"laptop": {"demo": {Digest: "d1"}}}}}} wire, err := json.Marshal(r) if err != nil { t.Fatal(err) } if !strings.Contains(string(wire), `"demo":{"digest":"d1"}`) || !strings.Contains(string(wire), `"raw":{"module":"raw","version":"1"}`) { t.Fatalf("on the wire: %s", wire) } back, err := Decode(wire) if err != nil { t.Fatal(err) } if _, err := Run(back); !errors.Is(err, ErrUnfilled) { t.Fatalf("a digest left unfilled runs: %v", err) } asked := 0 if err := Fill(&back, func(d string) ([]byte, error) { asked++ if d != "d1" { return nil, errors.New("no such digest") } return []byte(demo), nil }); err != nil { t.Fatal(err) } if asked != 2 || string(back.Cases[0].Manifests["demo"].Raw) != demo || string(back.Cases[0].Kept["laptop"]["demo"].Raw) != demo { t.Fatalf("filled %d times: %+v", asked, back.Cases[0]) } bad := Request{V: Version, Cases: []Case{{Name: "c", Manifests: map[string]Manifest{"x": {Digest: "nope"}}}}} if err := Fill(&bad, func(string) ([]byte, error) { return nil, errors.New("not there") }); err == nil || !strings.Contains(err.Error(), "nope") { t.Fatalf("a digest the indexer does not hold: %v", err) } } // The input travels as JSON and comes back equal, so the command composes what its caller meant. func TestTheInputTravelsAsJSON(t *testing.T) { in := compose.Input{ Nodes: []compose.Node{{Name: "anchor", Assigned: []string{"nats"}, Capabilities: map[string]bool{"systemd": true}, Place: compose.Place{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1"}, Pins: map[string]catalogue.Chosen{"bus": {Node: "anchor", Module: "nats"}}, Settings: map[string][]catalogue.Layer{"nats": {{Values: map[string]any{"port": float64(4222)}}}}}}, Holdings: []catalogue.Held{{Claim: "mesh-broker", Scope: "mesh", Node: "anchor", Module: "nats"}}, Issued: []compose.Issued{{Provision: "bus", Consumer: "laptop", ConsumerModule: "x", Provider: "anchor"}}, } raw, err := json.Marshal(Request{V: Version, Input: in}) if err != nil { t.Fatal(err) } back, err := Decode(raw) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(back.Input, in) { t.Fatalf("the input came back as\n%+v\nwant\n%+v", back.Input, in) } } // The seats a node's assigned modules claim are held there when the request says the holdings come from the claims. func TestHoldingsComeFromTheClaims(t *testing.T) { m, err := catalogue.ParseManifest([]byte(demo)) if err != nil { t.Fatal(err) } got := HoldingsOf(map[string]catalogue.Manifest{"demo": m}, []compose.Node{{Name: "b", Assigned: []string{"demo"}}, {Name: "a", Assigned: []string{"demo"}}}) if len(got) != 2 || got[0].Node != "a" || got[0].Claim != "node-files" || got[0].Scope != "node" { t.Fatalf("holdings = %+v", got) } } // A request the command cannot run is refused, never answered as a verdict: another version, a case named twice, an // unknown depth. func TestARequestItCannotRunIsRefused(t *testing.T) { if _, err := Decode([]byte(`{"v": 2}`)); err == nil { t.Error("a request of version 2 is read") } if _, err := Run(Request{V: Version, Cases: []Case{{Name: "a"}, {Name: "a"}}}); err == nil { t.Error("a case named twice runs") } if _, err := Run(Request{V: Version, Depth: "deep"}); err == nil { t.Error("an unknown depth runs") } v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}}) if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" { t.Fatalf("a request with no machines: %+v, %v", v, err) } } // A manifest whose config value falls back to a predefined key (novox/hq ADR 0348 ยง7) is read, not refused for the // list of predefined keys at the catalogue's root that a run is never handed with its manifests: the merge gate judges // the fallback in the catalogue. xterm's module.json from mesh-catalog at 835bf22b falls back to "font-size"; a group // touching xterm or pacman failed its composition on this (group check 18df81380db3). A list in the request is the one // judged against, so a fallback it does not name is still refused. func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) { raw, err := os.ReadFile("../catalogue/testdata/catalogue-835bf22b-xterm.module.json") if err != nil { t.Fatal(err) } if !strings.Contains(string(raw), `"predefined"`) { t.Fatal("the fixture names no predefined key") } r := Request{V: Version, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"xterm": {Raw: raw}}}}} v, err := Run(r) if err != nil { t.Fatal(err) } if got := v.Cases[0].Manifests["xterm"]; !got.Accepted { t.Fatalf("xterm: %+v, want accepted", got) } r.Predefined = catalogue.PredefinedList{"something-else": {Type: "string", Description: "x"}} if v, err = Run(r); err != nil { t.Fatal(err) } if got := v.Cases[0].Manifests["xterm"]; got.Accepted || !strings.Contains(got.Refused, "not on the list of predefined keys") { t.Fatalf("xterm judged against a list without its key: %+v, want refused naming the list", got) } }