package runtime import ( "encoding/json" "sort" "strings" "testing" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/micro" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // novox/hq ADR 0197: what a runtime serves, it announces — the NATS services protocol's discovery, // read here with NATS's own types, each endpoint a subject actually served — and the announcement // follows a re-issued membership. func TestTheRuntimeAnnouncesWhatItServesInTheServicesProtocol(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil)) mesh.Issue(t, mt.MembershipOf("beta", "anchor", false, map[string][]string{"node-shelf": {"list", "clear"}})) nodeTools := connect(t, "node-tools", "anchor") stop, err := Run(nodeTools, []Served{ {"alpha", []string{mt.Fixture("many-alpha.serve.mjs")}}, {"beta", []string{mt.Fixture("many-beta.serve.mjs")}}, }, nil, (&mt.Logs{}).Logf) if err != nil { t.Fatal(err) } defer stop() nc, err := nats.Connect(mt.URL(t)) if err != nil { t.Fatal(err) } defer nc.Close() info := func() micro.Info { t.Helper() msg, err := nc.Request("$SRV.INFO.node-tools.anchor", nil, 2*time.Second) if err != nil { t.Fatal(err) } var i micro.Info if err := json.Unmarshal(msg.Data, &i); err != nil { t.Fatal(err) } return i } subjects := func(i micro.Info) string { var out []string for _, e := range i.Endpoints { out = append(out, e.Subject+"|"+e.QueueGroup+"|"+e.Metadata["kind"]+"|"+e.Metadata["module"]+"|"+e.Metadata["seat"]+"|"+e.Metadata["scope"]+"|"+e.Metadata["interchangeable"]) } sort.Strings(out) return strings.Join(out, "\n") } got := info() if got.Type != micro.InfoResponseType || got.Name != "node-tools" || got.ID != "anchor" || got.Version == "" { t.Errorf("identity: %+v", got.ServiceIdentity) } want := strings.Join([]string{ "mesh.mod.alpha.tool.one.anchor||tool|alpha|||false", "mesh.mod.alpha.tool.two.anchor||tool|alpha|||false", "mesh.mod.beta.tool.five.anchor||tool|beta|||false", "mesh.mod.beta.tool.four.anchor||tool|beta|||false", "mesh.mod.beta.tool.three.anchor||tool|beta|||false", "mesh.seat.node-shelf.tool.clear.anchor||seat|beta|node-shelf|node|false", "mesh.seat.node-shelf.tool.list.anchor||seat|beta|node-shelf|node|false", }, "\n") if s := subjects(got); s != want { t.Errorf("announced:\n%s\nwant:\n%s", s, want) } for _, e := range got.Endpoints { if e.Metadata["node"] != "anchor" || e.Metadata["description"] == "" || !json.Valid([]byte(e.Metadata["schema"])) { t.Errorf("endpoint metadata: %+v", e) } } // Every subject announced is answered. asker := connect(t, "console", "workstation") for _, e := range got.Endpoints { if _, err := asker.Ask("", map[string]any{}, e.Subject); err != nil { t.Errorf("announced %s and does not answer it: %v", e.Subject, err) } } // PING answers with the same identity, and a request for another service is not answered. if msg, err := nc.Request("$SRV.PING", nil, 2*time.Second); err != nil || !strings.Contains(string(msg.Data), micro.PingResponseType) { t.Errorf("ping: %v %v", msg, err) } if _, err := nc.Request("$SRV.INFO.somebody-else", nil, 300*time.Millisecond); err == nil { t.Error("answered a request for another service") } // alpha is re-issued a plain subject: the announcement says so, and says it is interchangeable. mesh.Issue(t, mt.MembershipOf("alpha", "anchor", true, nil)) var after string for i := 0; i < 40; i++ { after = subjects(info()) if strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") { break } time.Sleep(100 * time.Millisecond) } if !strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") || !strings.Contains(after, "mesh.mod.alpha.tool.one.anchor||tool|alpha|||true") { t.Errorf("after a re-issued membership:\n%s", after) } _ = bus.Served{} }