Provisions were named after roles: provides "database", requires "database". Nothing distinguished engines, so a module written against PostgreSQL could be matched to a provider of SQL Server, resolve as satisfied, deploy, and fail on its first query — with nothing connecting that error back to a match made elsewhere by something that believed it had done its job. The failure is in the direction that hides. Refusing on ambiguity exists precisely so this does not happen, and the generic name walked around it: with one provider of each name nothing is ambiguous, so nothing is asked. How it got in: every resolver test had exactly one provider per name, so no mismatch was expressible and none was caught. The fixtures agreed with the design — the same fault as the imagined test output in 04-ISSUES/005, at the level of a name. Refused rather than documented, because the old naming *was* the documented convention. Providing database/db/sql/sql-database is now a parse error naming what to write instead. The rule is about coupling, not specificity everywhere: route and resolver stay role-named, because a consumer genuinely cannot tell which proxy answered. novox/hq ADR 0027.
119 lines
3.9 KiB
Go
119 lines
3.9 KiB
Go
package link_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
|
|
"github.com/novox/mesh-control/internal/inventory"
|
|
"github.com/novox/mesh-control/internal/link"
|
|
)
|
|
|
|
// What a node says when it joins, as that node's own code writes it.
|
|
//
|
|
// The two ends are separate structs in separate repositories, and a field renamed on one side
|
|
// fails silently: enrolment succeeds, a key is simply absent, and the node looks joined until the
|
|
// first thing sealed to it cannot be opened — by which time nobody is looking at enrolment.
|
|
//
|
|
// Skipped unless MESH_ENROL names the file the host's suite wrote:
|
|
//
|
|
// mesh-host: MESH_ENROL_OUT=/tmp/enrol.json go test ./internal/link/
|
|
// mesh-control: MESH_ENROL=/tmp/enrol.json make check
|
|
//
|
|
// **What this does not cover**, said so nobody reads more into a pass than is there: the full
|
|
// enrolment path also issues a broker account, and that needs a broker. What is checked here is
|
|
// the shape the two sides agree on and that a key which arrives this way can actually be sealed
|
|
// to — which is the part that was newly wired and the part that fails quietly.
|
|
func TestWhatANodeSaysWhenItJoinsIsWhatThisMeshReads(t *testing.T) {
|
|
path := os.Getenv("MESH_ENROL")
|
|
if path == "" {
|
|
t.Skip("set MESH_ENROL to an enrolment request written by the host's suite")
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var request link.EnrolRequest
|
|
if err := json.Unmarshal(raw, &request); err != nil {
|
|
t.Fatalf("this mesh cannot read what a node sends:\n%v", err)
|
|
}
|
|
for what, got := range map[string]string{
|
|
"node": request.Node,
|
|
"secret": request.Secret,
|
|
"overlay key": request.OverlayKey,
|
|
"sealing key": request.SealingKey,
|
|
} {
|
|
if got == "" {
|
|
t.Fatalf("the %s did not survive the crossing — a field name differs", what)
|
|
}
|
|
}
|
|
if len(request.PublicKey) != ed25519.PublicKeySize {
|
|
t.Fatalf("the identity arrived as %d bytes", len(request.PublicKey))
|
|
}
|
|
|
|
// And that a key arriving this way is one the mesh can actually seal to. Recording it is not
|
|
// the same as it being usable, and "recorded" is what a shape check on its own would prove.
|
|
inv := liveInventory(t)
|
|
ctx := context.Background()
|
|
node, err := inv.AddNode(ctx, request.Node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := inv.AddNode(ctx, "the-other-end")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordSealingKey(ctx, node.ID, request.SealingKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordSealingKey(ctx, other.ID, request.SealingKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
secret, err := inv.SecretFor(ctx, "postgres-database", request.Node, "the-other-end")
|
|
if err != nil {
|
|
t.Fatalf("nothing could be sealed to a key that arrived from a real node: %v", err)
|
|
}
|
|
|
|
// Opened with the private half the host's suite kept, so this asserts the node could read it
|
|
// rather than that a blob exists.
|
|
privateRaw, err := os.ReadFile(path + ".sealing-private")
|
|
if err != nil {
|
|
t.Skipf("no private half beside %s, so this can only check the shape", path)
|
|
}
|
|
private, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(privateRaw)))
|
|
if err != nil || len(private) != 32 {
|
|
t.Fatal("the private half beside the request is not a key")
|
|
}
|
|
public, err := base64.StdEncoding.DecodeString(request.SealingKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pub, priv [32]byte
|
|
copy(pub[:], public)
|
|
copy(priv[:], private)
|
|
blob, err := base64.StdEncoding.DecodeString(secret.ForConsumer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := box.OpenAnonymous(nil, blob, &pub, &priv); !ok {
|
|
t.Fatal("the node could not open what this mesh sealed to the key it sent")
|
|
}
|
|
}
|
|
|
|
// liveInventory is a database of its own for this test, skipping where there is none.
|
|
func liveInventory(t *testing.T) *inventory.Inventory {
|
|
t.Helper()
|
|
if os.Getenv("MESH_TEST_POSTGRES") == "" {
|
|
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
|
|
}
|
|
return inventory.ForTest(t)
|
|
}
|