Files
mesh-controller/internal/link/movesecret_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

91 lines
3.6 KiB
Go

package link
import (
"context"
"crypto/ed25519"
"encoding/json"
"sort"
"strings"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/testbus"
)
// The move's ask and answer keep the engine's field names (mesh-host internal/link,
// TestTheMovesAskKeepsItsFieldNamesAndContext): the row's fields beside a hand-over's, under a context of its own.
func TestTheMovesAskAndAnswerKeepTheirFieldNames(t *testing.T) {
keys := func(v any) string {
raw, _ := json.Marshal(v)
var m map[string]any
_ = json.Unmarshal(raw, &m)
var out []string
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return strings.Join(out, " ")
}
ask := MoveSecretAsk{Module: "m", Name: "n", Provider: "p", Provision: "q", Consumer: "c", ConsumerModule: "cm",
Local: "l", Origin: "made", Generation: "g", MadeAt: time.Unix(1, 0).UTC(), Sealed: "s", SealedTo: "k"}
if got := keys(ask); got != "consumer consumer_module generation local made_at module name origin provider "+
"provision sealed sealed_to" {
t.Fatalf("the ask's fields are %q", got)
}
answer := MoveSecretAnswer{Said: "s", Refused: "r", Secret: "own/a/b/c", Generation: 1, Unchanged: true}
if got := keys(answer); got != "generation refused said secret unchanged" {
t.Fatalf("the answer's fields are %q", got)
}
if broker.AskMoveSecretSubject("laptop") != "mesh.node.laptop.ask.move-secret" ||
MoveSecretContext != "novox-mesh move-secret v1\n" {
t.Fatalf("the subject %q, the context %q", broker.AskMoveSecretSubject("laptop"), MoveSecretContext)
}
}
// A move is asked of the machine on its own subject, signed under its own context — never a hand-over's — naming
// no path and carrying the row beside the hand-over's fields; the engine's answer comes back whole.
func TestAMoveIsAskedOfTheMachineSignedUnderItsOwnContext(t *testing.T) {
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(conn.Close)
signer, public := testSigner(t)
var heard HandOverAsk
var row MoveSecretAsk
engine, err := conn.Subscribe(broker.AskMoveSecretSubject("laptop"), func(msg *nats.Msg) {
var signed SignedHandOver
_ = json.Unmarshal(msg.Data, &signed)
if ed25519.Verify(public, append([]byte(HandOverContext), signed.Ask...), signed.Signature) ||
!ed25519.Verify(public, append([]byte(MoveSecretContext), signed.Ask...), signed.Signature) {
_ = msg.Respond([]byte(`{"refused":"not signed as a move"}`))
return
}
_ = json.Unmarshal(signed.Ask, &heard)
_ = json.Unmarshal(signed.Ask, &row)
body, _ := json.Marshal(MoveSecretAnswer{Said: "moved", Secret: "own/laptop/" + row.Module + "/" + row.Name,
Generation: 1})
_ = msg.Respond(body)
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = engine.Unsubscribe() })
a, err := AskMoveSecret(context.Background(), conn, signer, "laptop", "the move",
MoveSecretAsk{Module: "postgres", Name: "superuser-password", Origin: "made", Sealed: "c2Vh"}, 5*time.Second)
if err != nil || a.Secret != "own/laptop/postgres/superuser-password" || a.Generation != 1 {
t.Fatalf("answered %+v, %v", a, err)
}
if heard.Node != "laptop" || heard.Path != "" || heard.Nonce == "" || heard.Expires.IsZero() || heard.By != "the move" ||
row.Sealed != "c2Vh" || row.Origin != "made" {
t.Fatalf("the engine heard %+v %+v", heard, row)
}
if _, err := AskMoveSecret(context.Background(), conn, signer, "anchor", "the move", MoveSecretAsk{}, 2*time.Second); err == nil ||
!strings.Contains(err.Error(), "nothing was moved") {
t.Fatalf("a machine with no engine listening: %v", err)
}
}