Test one node's resolution over an Input (issue 1362). Ported from mesh-control 6c2a8d62 bindings_test.go and bindings_integration_test.go (store-free part); the credential cases are new, from the rules in plan.go. Written after the code, checked by breaking each rule in turn and seeing a test fail. Unit level 1.05 s of 2 s.

This commit is contained in:
2026-10-20 06:28:49 +00:00
parent 7ba9bcd873
commit fa08c5c308
+327
View File
@@ -0,0 +1,327 @@
package compose
import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/overlay"
)
// storeManifests is mesh-controller's bindings_test.go storeManifests (cmd/mesh-controller at 6c2a8d62): a store holding the
// mesh-store seat, a resolver holding mesh-dns-resolver, a network that requires wildcard resolution, and a board that
// requires a database.
func storeManifests() []catalogue.Manifest {
return []catalogue.Manifest{
{Module: "store", Version: "1",
Provides: []catalogue.Offer{{Name: "postgres-database", Scope: catalogue.ScopeMesh}},
Claims: []catalogue.Claim{{Name: "mesh-store", Scope: catalogue.ScopeMesh}},
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}},
Grants: map[string]string{"postgres-database": "/var/lib/mesh/store/grants"}},
{Module: "resolver", Version: "1",
Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}},
Claims: []catalogue.Claim{{Name: "mesh-dns-resolver", Scope: catalogue.ScopeMesh}}},
{Module: "network", Version: "1", Requires: []string{"wildcard-resolution"}},
{Module: "board", Version: "1", Requires: []string{"postgres-database"}},
}
}
func need(t *testing.T, plan catalogue.Resolution, consumer, provision string) catalogue.Needed {
t.Helper()
for _, n := range plan.Needs {
if n.For == consumer && n.Name == provision {
return n
}
}
t.Fatalf("no %s for %s: %+v", provision, consumer, plan.Needs)
return catalogue.Needed{}
}
// theStoreMesh is the mesh of mesh-controller's bindings_integration_test.go
// TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt: the anchor's store and resolver hold the mesh's seats on
// record; the laptop runs its own of each, and a network and a board.
func theStoreMesh(t *testing.T) Input {
t.Helper()
in := aMesh(t)
for _, m := range storeManifests() {
in.Catalogue[m.Module] = m
}
anchor, laptop := nodeOf(t, &in, "anchor"), nodeOf(t, &in, "laptop")
anchor.Assigned = append(anchor.Assigned, "store", "resolver")
laptop.Assigned = append(laptop.Assigned, "store", "resolver", "network", "board")
in.Holdings = []catalogue.Held{
{Claim: "mesh-store", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "store", Site: "here"},
{Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "resolver", Site: "here"},
}
return in
}
// 1362/AC7 and AC4 (bindings_integration_test.go): a consumer of a provision that keeps its data stays beside its data, one that
// does not follows the seat; the laptop's store taken away, the consumer bound there on record is refused, not moved to the
// anchor's empty one; a person's pin moves it.
func TestAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) {
in := theStoreMesh(t)
got, err := Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
if n := need(t, got.Plan, "board", "postgres-database"); n.From != "laptop" || n.Module != "store" || !n.KeepsData {
t.Fatalf("the consumer was bound to %s/%s (keeps data: %v); its data is beside it", n.From, n.Module, n.KeepsData)
}
if n := need(t, got.Plan, "network", "wildcard-resolution"); n.From != "anchor" || n.KeepsData {
t.Fatalf("the resolver was bound to %s (keeps data: %v); its seat is held on anchor (issue 258)", n.From, n.KeepsData)
}
// Sent, and recorded: the binding to data. The laptop's store taken away: refused, not moved to the anchor's empty one.
laptop := nodeOf(t, &in, "laptop")
laptop.Bound = map[string]map[string]catalogue.Chosen{"board": {"postgres-database": {Node: "laptop", Module: "store"}}}
laptop.Assigned = []string{overlay.Name, "resolver", "network", "board"}
_, err = Resolve(in, "laptop")
if err == nil || !IsUnresolvable(err) ||
!strings.Contains(err.Error(), "board on laptop is bound to laptop/store") ||
!strings.Contains(err.Error(), "pin laptop postgres-database anchor store") {
t.Fatalf("the consumer's store went and it was answered elsewhere: %v", err)
}
// A person pins the anchor's: it moves, and the resolution says where it was bound (Kept is empty, as it moves).
laptop.Pins = map[string]catalogue.Chosen{"postgres-database": {Node: "anchor", Module: "store"}}
got, err = Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
if n := need(t, got.Plan, "board", "postgres-database"); n.From != "anchor" {
t.Fatalf("pinned to the anchor and bound to %s", n.From)
}
}
// 1362/AC4: a data-keeping consumer bound on record to a provider elsewhere is kept on that provider, and the resolution says
// so (Resolution.Kept) when the mesh would answer it otherwise.
func TestADataKeepingConsumerBoundElsewhereIsKeptThereAndSaid(t *testing.T) {
in := theStoreMesh(t)
laptop := nodeOf(t, &in, "laptop")
laptop.Assigned = []string{overlay.Name, "resolver", "network", "board"} // no store of its own
laptop.Bound = map[string]map[string]catalogue.Chosen{"board": {"postgres-database": {Node: "anchor", Module: "store"}}}
got, err := Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
if n := need(t, got.Plan, "board", "postgres-database"); n.From != "anchor" || !n.KeepsData {
t.Fatalf("the consumer is bound to %s (keeps data: %v), not where it was sent", n.From, n.KeepsData)
}
if len(got.Plan.Kept) != 0 {
t.Errorf("a binding that was kept where it is is said as a kept move: %+v", got.Plan.Kept)
}
// And bound somewhere else on record while the mesh would answer from the anchor: kept, and said.
laptop.Bound = map[string]map[string]catalogue.Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}}
home := Node{Name: "home", Place: Place{Site: "there", Address: "10.77.0.7"}, Assigned: []string{overlay.Name, "store"},
Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}}
in.Nodes = append(in.Nodes, home)
got, err = Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
if n := need(t, got.Plan, "board", "postgres-database"); n.From != "home" {
t.Errorf("the consumer was moved from where it was sent: %s", n.From)
}
}
// A provider module whose offer says only a person gives its credential (ADR 0337), and a consumer of it.
func outsideObjects() catalogue.Manifest {
m := objects()
m.Provides[0].Credential = &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}
return m
}
func theObjectsMesh(t *testing.T, provider catalogue.Manifest) Input {
t.Helper()
in := aMesh(t)
in.Catalogue[provider.Module], in.Catalogue["files"] = provider, files()
nodeOf(t, &in, "anchor").Assigned = append(nodeOf(t, &in, "anchor").Assigned, provider.Module)
nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "files")
nodeOf(t, &in, "anchor").SealingKey = "anchor-key"
nodeOf(t, &in, "laptop").SealingKey = "laptop-key"
return in
}
func theFilesNeed(t *testing.T, got Resolved) catalogue.Needed {
t.Helper()
return need(t, got.Plan, "files", "s3-bucket")
}
// 1362/AC1: a pair credential the Input names as existing with no value, and one it names not at all, are composed with a
// stand-in and named foreseen, and nothing is made; one with a value is composed with it and foreseen is empty.
func TestAPairCredentialNotHeldIsComposedWithAStandInAndNamedForeseen(t *testing.T) {
mk := func() Input { return theObjectsMesh(t, objects()) }
for name, creds := range map[string][]Credential{
"no entry": nil,
"held with no value": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialHeld}},
"to be made": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialWouldMake}},
} {
in := mk()
in.Credentials = creds
got, err := Resolve(in, "laptop")
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if n := theFilesNeed(t, got); n.Sealed != catalogue.ForeseenSealed {
t.Errorf("%s: the credential is %q, not the stand-in", name, n.Sealed)
}
if len(got.Foreseen) != 1 || got.Foreseen[0] != "files on laptop needs s3-bucket from anchor" {
t.Errorf("%s: foreseen is %v", name, got.Foreseen)
}
}
in := mk()
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor",
State: CredentialHeld, Sealed: "sealed-to-laptop"}}
got, err := Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
if n := theFilesNeed(t, got); n.Sealed != "sealed-to-laptop" || len(got.Foreseen) != 0 {
t.Errorf("a held credential: %q foreseen %v", n.Sealed, got.Foreseen)
}
unchanged(t, mk, func(in Input) {
if _, err := Resolve(in, "laptop"); err != nil {
t.Fatal(err)
}
})
}
// 1362/AC2: a credential only a person gives and nobody has is not given and not made: the consumer still resolves without it
// (novox/hq ADR 0337 clause 4), said with the controller's words; one given is composed with it.
func TestACredentialOnlyAPersonGivesIsNotMadeAndTheConsumerStillResolves(t *testing.T) {
for name, creds := range map[string][]Credential{
"no entry": nil,
"to be made (never, for one issued outside)": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialWouldMake}},
"not given": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialNotGiven,
Why: "the value held was made by the mesh, which objects's software would refuse"}},
} {
in := theObjectsMesh(t, outsideObjects())
in.Credentials = creds
got, err := Resolve(in, "laptop")
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if n := theFilesNeed(t, got); n.Sealed != "" {
t.Errorf("%s: a value stands in for what nobody gave: %q", name, n.Sealed)
}
if len(got.Foreseen) != 0 || len(got.NotGiven) != 1 || !strings.Contains(got.NotGiven[0], "files on laptop holds no s3-bucket from anchor: ") {
t.Errorf("%s: foreseen %v not given %v", name, got.Foreseen, got.NotGiven)
}
if len(got.Plan.Modules) == 0 {
t.Errorf("%s: the machine did not resolve", name)
}
}
in := theObjectsMesh(t, outsideObjects())
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor",
State: CredentialHeld, Sealed: "given-and-sealed"}}
got, err := Resolve(in, "laptop")
if err != nil || theFilesNeed(t, got).Sealed != "given-and-sealed" || len(got.NotGiven) != 0 {
t.Fatalf("a value a person gave: %+v %v", got.NotGiven, err)
}
}
// 1362/AC3: a credential the send would refuse, a given value sealed to a key a machine no longer has, refuses the resolution with
// today's words, and is not a node whose set does not compose.
func TestACredentialTheSendWouldRefuseRefusesTheResolutionInTheSendsWords(t *testing.T) {
why := "files's \"s3-bucket\" credential from anchor was accepted from a person, and a sealing key at one end has changed since. " +
"The mesh cannot re-seal a value it does not hold: accept it again with `secret accept laptop files s3-bucket --provider anchor`"
in := theObjectsMesh(t, objects())
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor",
State: CredentialRefused, Why: why}}
_, err := Resolve(in, "laptop")
want := "files on laptop needs s3-bucket from anchor and no credential could be made for it: " + why
if err == nil || err.Error() != want {
t.Fatalf("got %v, want %q", err, want)
}
if IsUnresolvable(err) {
t.Error("a refused credential is a node whose set does not compose")
}
}
// A record-answered need is filled with the licence key sealed to this holder, and the manager holder is handed the manager node's
// public sealing key (novox/hq ADR 0050) and no other.
func TestARecordAnsweredNeedGetsItsKeyAndTheManagerItsPublicKey(t *testing.T) {
in := aMesh(t)
in.Catalogue["assistant"] = catalogue.Manifest{Module: "assistant", Version: "1", Requires: []string{LicenceProvision}}
laptop := nodeOf(t, &in, "laptop")
laptop.Assigned = append(laptop.Assigned, "assistant")
laptop.SealingKey = "laptop-key"
in.Licences = []Licence{{Name: "personal", Serves: map[string]any{"model": "x"},
Holders: []Holder{{Node: "laptop", Module: "assistant"}},
Keys: map[Holder]string{{Node: "laptop", Module: "assistant"}: "key-sealed-to-laptop"}}}
got, err := Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
n := need(t, got.Plan, "assistant", LicenceProvision)
if !n.ByRecord || n.Sealed != "key-sealed-to-laptop" || n.Manager || n.Serves["manager_public_key"] != nil {
t.Fatalf("the record-answered need is %+v", n)
}
in.Licences[0].Manager = Holder{Node: "laptop", Module: "assistant"}
got, err = Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
n = need(t, got.Plan, "assistant", LicenceProvision)
if !n.Manager || n.Serves["manager_public_key"] != "laptop-key" || n.Serves["model"] != "x" {
t.Fatalf("the manager is %+v", n)
}
if len(got.Foreseen) != 0 || len(got.NotGiven) != 0 {
t.Errorf("a record-answered need needs no pair credential: %v %v", got.Foreseen, got.NotGiven)
}
}
// 1362/AC5: a node whose own set does not compose returns an error callers can tell from an unreadable Input.
func TestANodeWhoseOwnSetDoesNotComposeIsToldApartFromAnUnreadableInput(t *testing.T) {
in := aMesh(t)
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
laptop := nodeOf(t, &in, "laptop")
laptop.Assigned = append(laptop.Assigned, "rival-one", "rival-two")
if _, err := Resolve(in, "laptop"); err == nil || !IsUnresolvable(err) {
t.Fatalf("a set that does not compose: %v", err)
}
if _, err := Resolve(in, "nowhere"); err == nil || IsUnresolvable(err) || !errors.Is(err, ErrNoSuchNode) {
t.Fatalf("a node the mesh does not have: %v", err)
}
in.Catalogue = nil
if _, err := Resolve(in, "anchor"); err == nil || IsUnresolvable(err) {
t.Fatalf("an Input without the catalogue: %v", err)
}
}
// 1362/AC6: settings are returned for every resolved module that has layers in the Input, including one pulled in by another.
func TestSettingsAreReturnedForEveryResolvedModuleIncludingOneThatWasPulledIn(t *testing.T) {
in := aMesh(t)
in.Catalogue["lib"] = catalogue.Manifest{Module: "lib", Version: "1", Provides: catalogue.Offers("blob")}
in.Catalogue["app"] = catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"blob"}}
laptop := nodeOf(t, &in, "laptop")
laptop.Assigned = append(laptop.Assigned, "app")
laptop.Settings = map[string][]catalogue.Layer{
"lib": {{From: "mesh", Values: map[string]any{"x": 1}}},
"app": {{From: "laptop", Values: map[string]any{"y": 2}}},
"other": {{From: "laptop", Values: map[string]any{"z": 3}}}, // not resolved here: no settings come of it
"empty": nil,
}
got, err := Resolve(in, "laptop")
if err != nil {
t.Fatal(err)
}
pulled := false
for _, m := range got.Plan.Modules {
pulled = pulled || m.Module == "lib"
}
if !pulled {
t.Fatalf("lib was not pulled in: %+v", got.Plan.Modules)
}
if len(got.Settings) != 2 || got.Settings["lib"][0].Values["x"] != 1 || got.Settings["app"][0].Values["y"] != 2 {
t.Errorf("settings are %+v", got.Settings)
}
// The settings returned are copies: changing them does not change the Input.
got.Settings["app"][0].From = "changed"
if laptop.Settings["app"][0].From != "laptop" {
t.Error("the settings returned share the Input's layers")
}
}