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
164 lines
6.4 KiB
Go
164 lines
6.4 KiB
Go
package resolution
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// a web module with one routed endpoint, the shape almost every routed module in the catalogue has.
|
|
func aRoutedWeb() mcat.Manifest {
|
|
return mcat.Manifest{
|
|
Module: "web",
|
|
Listens: []mcat.Listening{{Port: 3000, From: mcat.FromMesh}},
|
|
Contributes: map[string]map[string]any{
|
|
"route": {"label": "app", "port": 3000},
|
|
},
|
|
}
|
|
}
|
|
|
|
func reachSet(reach string) mcat.SettingsBy {
|
|
return mcat.SettingsBy{"web": {{From: "node anchor",
|
|
Placement: map[string]any{mcat.ReachSetting: map[string]any{"3000": reach}}}}}
|
|
}
|
|
|
|
// namesFor renders the contribution a routed module makes and returns the two names it carries.
|
|
func namesFor(t *testing.T, m mcat.Manifest, settings mcat.SettingsBy) (public, internal string) {
|
|
t.Helper()
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{m},
|
|
PublicDomain: "example.test", At: "anchor.internal"}
|
|
given, err := resolutionContributions(r, settings, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("contributions: %v", err)
|
|
}
|
|
for _, c := range given["route"] {
|
|
p, _ := c.Values["name"].(string)
|
|
i, _ := c.Values["internal-name"].(string)
|
|
return p, i
|
|
}
|
|
t.Fatal("the module contributed no route")
|
|
return "", ""
|
|
}
|
|
|
|
// **The filter reads the same value — for an endpoint the proxy does not serve.**
|
|
//
|
|
// A routed endpoint's port is how the proxy reaches it and nothing else (ADR 0045): a public service
|
|
// listens from the mesh, only the proxy reaches it, and it is exposed by name. So on a routed
|
|
// endpoint the reach asks for a name and the port keeps what the manifest said.
|
|
func TestAnUnroutedEndpointsPortFollowsItsReach(t *testing.T) {
|
|
// The same module with its route taken away: now the port is the only way in, so reach governs it.
|
|
bare := aRoutedWeb()
|
|
bare.Contributes = nil
|
|
|
|
for _, c := range []struct{ reach, want string }{
|
|
{mcat.ReachInternal, mcat.FromMesh},
|
|
{mcat.ReachPublic, mcat.FromEverywhere},
|
|
{mcat.ReachBoth, mcat.FromEverywhere},
|
|
{mcat.ReachMachine, mcat.FromMachine},
|
|
} {
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{bare}}
|
|
rules, err := Rules(r, mcat.Rendering{Settings: reachSet(c.reach)})
|
|
if err != nil {
|
|
t.Fatalf("%s: rules: %v", c.reach, err)
|
|
}
|
|
found := false
|
|
for _, rule := range rules {
|
|
if rule.Port == 3000 {
|
|
found = true
|
|
if rule.From != c.want {
|
|
t.Fatalf("reach %q made the filter say %q, want %q", c.reach, rule.From, c.want)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("reach %q produced no rule for the port", c.reach)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A public name does not open the machine's port**, which is the case that found this.
|
|
//
|
|
// A module whose routed name must be public and whose machine-side port must not be had no way to say
|
|
// so while one value drove both. Under one value it could not be expressed; the port would reopen.
|
|
func TestAPublicNameLeavesARoutedPortAsTheManifestSaid(t *testing.T) {
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{aRoutedWeb()},
|
|
PublicDomain: "example.test", At: "anchor.internal"}
|
|
rules, err := Rules(r, mcat.Rendering{Settings: reachSet(mcat.ReachPublic)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, rule := range rules {
|
|
if rule.Port == 3000 && rule.From != mcat.FromMesh {
|
|
t.Fatalf("a public reach opened a routed port to %q; the proxy is how it is reached",
|
|
rule.From)
|
|
}
|
|
}
|
|
// And the name it asked for is there, so the reach was not simply ignored.
|
|
public, internal := namesFor(t, aRoutedWeb(), reachSet(mcat.ReachPublic))
|
|
if public != "app.example.test" || internal != "" {
|
|
t.Fatalf("names are %q and %q, want the public one only", public, internal)
|
|
}
|
|
}
|
|
|
|
// A reach for a port the module does not listen on reaches nothing, and is refused where it is
|
|
// written rather than accepted and ignored.
|
|
func TestAReachForAPortTheModuleDoesNotListenOnIsRefused(t *testing.T) {
|
|
_, err := Reaches(aRoutedWeb(), []mcat.Layer{{From: "node anchor",
|
|
Placement: map[string]any{mcat.ReachSetting: map[string]any{"9999": mcat.ReachInternal}}}})
|
|
if err == nil || !strings.Contains(err.Error(), "reaches nothing") {
|
|
t.Fatalf("a reach naming an undeclared port was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
// A value that is not a reach is refused, and the refusal names the four so a reader is one edit from
|
|
// right. "mesh" is the tempting wrong answer, because that is the filter's word for nearly the same
|
|
// thing.
|
|
func TestAValueThatIsNotAReachIsRefused(t *testing.T) {
|
|
for _, wrong := range []string{"mesh", "anywhere", "private", "true"} {
|
|
_, err := Reaches(aRoutedWeb(), []mcat.Layer{{From: "node anchor",
|
|
Placement: map[string]any{mcat.ReachSetting: map[string]any{"3000": wrong}}}})
|
|
if err == nil || !strings.Contains(err.Error(), "a reach is") {
|
|
t.Fatalf("%q was accepted as a reach: %v", wrong, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A port that says both reach and expose is refused.** They say the same thing in different words,
|
|
// and accepting both would have the filter follow one while the names followed the other — the
|
|
// disagreement ADR 0138 exists to remove, reintroduced by the migration away from the older word.
|
|
func TestReachAndExposeForOnePortAreRefused(t *testing.T) {
|
|
_, err := Reaches(aRoutedWeb(), []mcat.Layer{{From: "node anchor", Placement: map[string]any{mcat.ReachSetting: map[string]any{"3000": mcat.ReachInternal}, mcat.ExposeSetting: map[string]any{"3000": mcat.FromEverywhere}}}})
|
|
if err == nil || !strings.Contains(err.Error(), "same thing in different") {
|
|
t.Fatalf("a port set both ways was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
// A path-level refusal carries no port: it is a rule about a name, not an endpoint, and it inherits
|
|
// whatever that name turned out to be. Narrowing the endpoint must not silently drop it.
|
|
func TestARuleWithNoPortIsLeftAlone(t *testing.T) {
|
|
m := aRoutedWeb()
|
|
m.ContributesMany = map[string]map[string]map[string]any{
|
|
"route": {"refused": {"label": "app", "path": "/internal", "deny": true}},
|
|
}
|
|
r := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{m},
|
|
PublicDomain: "example.test", At: "anchor.internal"}
|
|
given, err := resolutionContributions(r, reachSet(mcat.ReachInternal), nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sawDeny bool
|
|
for _, c := range given["route"] {
|
|
if deny, _ := c.Values["deny"].(bool); deny {
|
|
sawDeny = true
|
|
// It keeps both, because it named no endpoint to be narrowed by.
|
|
if c.Values["name"] == nil || c.Values["internal-name"] == nil {
|
|
t.Fatalf("the path rule lost a name it shadows: %v", c.Values)
|
|
}
|
|
}
|
|
}
|
|
if !sawDeny {
|
|
t.Fatal("the path rule was dropped")
|
|
}
|
|
}
|