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
105 lines
4.8 KiB
Go
105 lines
4.8 KiB
Go
package resolution
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
func issuerShelf() map[string]mcat.Manifest {
|
|
return shelf(mcat.Manifest{Module: "public-acme", Version: "1", Provides: mcat.FromAnywhere("acme-ca"),
|
|
Serves: map[string]map[string]any{"acme-ca": {"at": "acme-v02.api.letsencrypt.org"}}}, mcat.Manifest{Module: "step-ca", Version: "1", Provides: mcat.FromAnywhere("acme-ca"),
|
|
Serves: map[string]map[string]any{"acme-ca": {"at": "novox.internal"}}}, mcat.Manifest{Module: "route-proxy", Version: "1", Requires: []string{"acme-ca"}},
|
|
)
|
|
}
|
|
|
|
func twoIssuersOnOneNode() map[string][]mcat.Provider {
|
|
return map[string][]mcat.Provider{"acme-ca": {
|
|
{Node: "novox", At: "novox.internal", Module: "public-acme", Serves: map[string]any{"at": "acme-v02.api.letsencrypt.org"}},
|
|
{Node: "novox", At: "novox.internal", Module: "step-ca", Serves: map[string]any{"at": "novox.internal"}},
|
|
}}
|
|
}
|
|
|
|
func TestTwoProvidersOnOneNodeAreRefusedWithBothNamed(t *testing.T) {
|
|
// The refusal must name the pair, because a node alone cannot tell them apart.
|
|
_, err := Resolve(issuerShelf(), []string{"route-proxy"}, reachable(), mcat.World{Offered: twoIssuersOnOneNode()})
|
|
if err == nil {
|
|
t.Fatal("two providers on one node were resolved by picking")
|
|
}
|
|
for _, want := range []string{"novox/public-acme", "novox/step-ca", "<node> <module>"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("the refusal does not say %q: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPinNamesTheModule(t *testing.T) {
|
|
got, err := Resolve(issuerShelf(), []string{"route-proxy"}, reachable(), mcat.World{Offered: twoIssuersOnOneNode(),
|
|
Pinned: map[string]mcat.Chosen{"acme-ca": {Node: "novox", Module: "public-acme"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Needs) != 1 || got.Needs[0].From != "novox" || got.Needs[0].Serves["at"] != "acme-v02.api.letsencrypt.org" {
|
|
t.Fatalf("the named module was not the one taken: %+v", got.Needs)
|
|
}
|
|
}
|
|
|
|
func TestARecordNamingOnlyTheNodeIsRefusedWhenThatNodeAnswersTwice(t *testing.T) {
|
|
// A pin from before the module was asked for. It once took the last one listed — a coin flip.
|
|
_, err := Resolve(issuerShelf(), []string{"route-proxy"}, reachable(), mcat.World{Offered: twoIssuersOnOneNode(), Pinned: map[string]mcat.Chosen{"acme-ca": {Node: "novox"}}})
|
|
if err == nil {
|
|
t.Fatal("a node that answers twice was resolved by picking")
|
|
}
|
|
if !strings.Contains(err.Error(), "provides it 2 times") || !strings.Contains(err.Error(), "pin workstation acme-ca novox <module>") {
|
|
t.Fatalf("the refusal does not ask for the module: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAPinNamingAModuleThatDoesNotProvideItIsRefused(t *testing.T) {
|
|
_, err := Resolve(issuerShelf(), []string{"route-proxy"}, reachable(), mcat.World{Offered: twoIssuersOnOneNode(), Pinned: map[string]mcat.Chosen{"acme-ca": {Node: "novox", Module: "gitea"}}})
|
|
if err == nil || !strings.Contains(err.Error(), "novox/gitea does not provide it") {
|
|
t.Fatalf("a module that does not provide it was not refused by name: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTwoProvidersBesideTheConsumerAreRefusedUntilOneIsNamed(t *testing.T) {
|
|
// The same ambiguity on the consumer's own machine. This was settled by a map walk — random,
|
|
// per plan — which is how novox's own proxy got its issuer.
|
|
_, err := Resolve(issuerShelf(), []string{"route-proxy", "public-acme", "step-ca"}, reachable(), mcat.World{})
|
|
if err == nil {
|
|
t.Fatal("two providers beside the consumer were resolved by picking")
|
|
}
|
|
if !strings.Contains(err.Error(), "workstation provides \"acme-ca\" 2 times") || !strings.Contains(err.Error(), "public-acme, step-ca") {
|
|
t.Fatalf("the refusal does not list them: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAPinSettlesTwoProvidersBesideTheConsumer(t *testing.T) {
|
|
got, err := Resolve(issuerShelf(), []string{"route-proxy", "public-acme", "step-ca"}, reachable(), mcat.World{Pinned: map[string]mcat.Chosen{"acme-ca": {Node: "workstation", Module: "public-acme"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var found bool
|
|
for _, n := range got.Needs {
|
|
if n.Name == "acme-ca" {
|
|
found = true
|
|
if n.Serves["at"] != "acme-v02.api.letsencrypt.org" {
|
|
t.Fatalf("the named module was not the one taken: %+v", n)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("no need for acme-ca was created: %+v", got.Needs)
|
|
}
|
|
}
|
|
|
|
func TestTheFirstPassDoesNotRefuseTwoProvidersBesideTheConsumer(t *testing.T) {
|
|
// The first pass answers only what a node offers. Refused there, the node vanishes from every
|
|
// other node's world — and the whole mesh loses its vault for an ambiguity one machine has to
|
|
// settle. The second pass is where it is refused, and the test above proves it is.
|
|
if _, err := Resolve(issuerShelf(), []string{"route-proxy", "public-acme", "step-ca"}, reachable(), mcat.World{Unchecked: true}); err != nil {
|
|
t.Fatalf("the first pass refused what only the second may: %v", err)
|
|
}
|
|
}
|