ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
129 lines
4.6 KiB
Go
129 lines
4.6 KiB
Go
package resolution
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// selfAware contributes a labelled route, binds the requirement, and writes its own name into a file.
|
|
func selfAware(label string) mcat.Manifest {
|
|
m := labelled("board", label, 8080)
|
|
m.Requires = []string{"reverse-proxy"}
|
|
m.Binds = map[string]string{"reverse-proxy": "/var/lib/board/route.json"}
|
|
m.Resources = []map[string]any{
|
|
{"id": "conf", "type": "file", "path": "/var/lib/board/app.conf",
|
|
"content": "root = https://${bound:reverse-proxy:name}/\ninternal = ${bound:reverse-proxy:internal-name}\n"},
|
|
}
|
|
return m
|
|
}
|
|
|
|
// servingProxy is proxy() as the catalogue's route providers are declared: the provision scoped to
|
|
// the mesh, serving nothing a consumer must know (route-adapter, route-proxy: `"serves": {"route": {}}`).
|
|
func servingProxy() mcat.Manifest {
|
|
p := proxy()
|
|
p.Provides = []mcat.Offer{{Name: "reverse-proxy", Scope: mcat.ScopeMesh}}
|
|
p.Serves = map[string]map[string]any{"reverse-proxy": {}}
|
|
return p
|
|
}
|
|
|
|
// nodeProxy is the same provider scoped to its node, whose answer on the same machine comes from
|
|
// `here` rather than from the mesh's needs — the other path a binding is written by.
|
|
func nodeProxy() mcat.Manifest {
|
|
p := proxy()
|
|
p.Serves = map[string]map[string]any{"reverse-proxy": {"scheme": "http"}}
|
|
return p
|
|
}
|
|
|
|
// onBoth is a node with a public domain and a private-network address, so both names compose.
|
|
func onBoth(domain string) mcat.Node {
|
|
n := withDomain(domain)
|
|
n.At = "anchor.internal"
|
|
return n
|
|
}
|
|
|
|
func fileAt(t *testing.T, out []map[string]any, path string) string {
|
|
t.Helper()
|
|
for _, r := range out {
|
|
if r["path"] == path {
|
|
return r["content"].(string)
|
|
}
|
|
}
|
|
t.Fatalf("nothing was declared at %s", path)
|
|
return ""
|
|
}
|
|
|
|
func TestAModuleIsToldTheNameItsProviderServes(t *testing.T) {
|
|
got, err := Resolve(shelf(servingProxy(), selfAware("git")), []string{"traefik", "board"},
|
|
onBoth("example.tld"), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := mustDeclare(t, got)
|
|
served := received(t, out)[0].Values
|
|
|
|
var binding map[string]any
|
|
if err := json.Unmarshal([]byte(fileAt(t, out, "/var/lib/board/route.json")), &binding); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if binding["name"] != served["name"] || binding["name"] != "git.example.tld" {
|
|
t.Fatalf("the module was told %v, the provider serves %v", binding["name"], served["name"])
|
|
}
|
|
if binding["internal-name"] != served["internal-name"] || binding["internal-name"] == nil {
|
|
t.Fatalf("internal name: module told %v, provider serves %v",
|
|
binding["internal-name"], served["internal-name"])
|
|
}
|
|
|
|
conf := fileAt(t, out, "/var/lib/board/app.conf")
|
|
want := "root = https://git.example.tld/\ninternal = " + served["internal-name"].(string) + "\n"
|
|
if conf != want {
|
|
t.Fatalf("the file was rendered as\n%s\nwant\n%s", conf, want)
|
|
}
|
|
}
|
|
|
|
func TestTheNameAModuleIsToldFollowsTheNodesDomain(t *testing.T) {
|
|
// The whole point: the same definition, two machines, two names — nothing edited.
|
|
for _, domain := range []string{"example.tld", "other.example"} {
|
|
got, err := Resolve(shelf(servingProxy(), selfAware("git")), []string{"traefik", "board"},
|
|
onBoth(domain), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := fileAt(t, mustDeclare(t, got), "/var/lib/board/app.conf")
|
|
if !strings.HasPrefix(conf, "root = https://git."+domain+"/") {
|
|
t.Fatalf("on %s the module wrote %q", domain, conf)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAModuleWithNoPublicNameIsNotToldOne(t *testing.T) {
|
|
// No public domain on the node: nothing composed, so no `name` — and a file asking for one is
|
|
// refused rather than rendered with a placeholder or an empty host.
|
|
m := selfAware("git")
|
|
m.Resources[0]["content"] = "root = https://${bound:reverse-proxy:name}/\n"
|
|
got, err := Resolve(shelf(servingProxy(), m), []string{"traefik", "board"}, workstation(), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Declaration(got, mcat.Rendering{}); err == nil ||
|
|
!strings.Contains(err.Error(), `"name"`) {
|
|
t.Fatalf("a file asking for a name that was never composed was not refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAModuleIsToldItsNameByANodeScopedProviderToo(t *testing.T) {
|
|
m := selfAware("git")
|
|
m.Resources[0]["content"] = "root = https://${bound:reverse-proxy:name}/\n"
|
|
got, err := Resolve(shelf(nodeProxy(), m), []string{"board"},
|
|
withDomain("example.tld"), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := fileAt(t, mustDeclare(t, got), "/var/lib/board/app.conf")
|
|
if !strings.HasPrefix(conf, "root = https://git.example.tld/") {
|
|
t.Fatalf("a same-machine, node-scoped answer did not tell the module its name: %q", conf)
|
|
}
|
|
}
|