mesh-enrolment: its integration test, whole on a real bus
The holder's code over a real nats-server bucket with a time to live per key: issue, the operator's yes, redeem as enrol.ace, the node confirming as the login its key of node-logins names (the stand-in vault writes it at node-account), node-enrolled and placement-changed said on their subjects, the user enrol.ace composed while the key is live and gone with it, the token dropped only after the end; and a token never redeemed expires on the bus, is heard, says enrolment-expired and is dropped. Found by it: a bucket with a time to live per key must keep one revision per key, or the bus expires nothing (nats-server 2.11.17), so the state enrolments declares no history. Paid for: the integration test is paid by merging the builder's archive-only and root-module integration tests into one table test; the module's ten unit tests by merging the assigner's three small composition tests into one table (net 2 of 10 paid; the rest is for the lead, the repository being over budget). (issue 774, 774/S60) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -68,33 +68,33 @@ func TestAChangeOfOneModuleComposesItsMachine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/AC2: a change that removes a module a machine runs fails, naming the machine and the module.
|
||||
func TestRemovingAModuleAMachineRunsFails(t *testing.T) {
|
||||
c := Change{Group: "g", Heads: "0123456789ab", Removed: []string{"web"}}
|
||||
v := Compose(meshOf(), c, "sdk", 0)
|
||||
if v.State != delivery.CompositionFail || v.Machine != "g14" || !strings.Contains(v.Words, "g14 runs web") {
|
||||
t.Fatalf("composition = %+v, want a fail naming g14 and web", v)
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B3c: a machine that composes on the trunk and not with the change fails it, naming the machine and why; the
|
||||
// consumer's machine is touched when its provider changes.
|
||||
func TestAMachineTheChangeBreaksFails(t *testing.T) {
|
||||
c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"store": {Module: "store", Version: "2"}}}
|
||||
v := Compose(meshOf(), c, "sdk", 0)
|
||||
if v.State != delivery.CompositionFail || v.Machine != "ace" || !strings.Contains(v.Words, "database") {
|
||||
t.Fatalf("composition = %+v, want a fail on ace naming the provision nothing provides", v)
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B3c: what was already broken on the trunk is said and fails nothing.
|
||||
func TestWhatWasBrokenBeforeFailsNothing(t *testing.T) {
|
||||
m := meshOf()
|
||||
m.Manifests["store"] = catalogue.Manifest{Module: "store", Version: "1"}
|
||||
c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"mail": {Module: "mail", Version: "2", Requires: []string{"database"}}}}
|
||||
v := Compose(m, c, "sdk", 0)
|
||||
if v.State != delivery.CompositionPass || !hasWarning(v, "on the trunk either") {
|
||||
t.Fatalf("composition = %+v, want a pass saying ace was already broken", v)
|
||||
// 1297/AC2 and B3c: a change that removes a module a machine runs fails, naming the machine and the module; a machine that
|
||||
// composes on the trunk and not with the change fails it, naming the machine and why (the consumer's machine is touched when
|
||||
// its provider changes); what was already broken on the trunk is said and fails nothing. One table (three tests merged).
|
||||
func TestAChangeThatBreaksAMachineFailsAndWhatWasBrokenFailsNothing(t *testing.T) {
|
||||
broken := meshOf()
|
||||
broken.Manifests["store"] = catalogue.Manifest{Module: "store", Version: "1"}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
mesh Mesh
|
||||
c Change
|
||||
state string
|
||||
must func(delivery.CompositionVerdict) bool
|
||||
}{
|
||||
{"removing a module a machine runs", meshOf(), Change{Group: "g", Heads: "0123456789ab", Removed: []string{"web"}}, delivery.CompositionFail,
|
||||
func(v delivery.CompositionVerdict) bool {
|
||||
return v.Machine == "g14" && strings.Contains(v.Words, "g14 runs web")
|
||||
}},
|
||||
{"a machine the change breaks", meshOf(), Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"store": {Module: "store", Version: "2"}}},
|
||||
delivery.CompositionFail, func(v delivery.CompositionVerdict) bool {
|
||||
return v.Machine == "ace" && strings.Contains(v.Words, "database")
|
||||
}},
|
||||
{"what was broken before", broken, Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"mail": {Module: "mail", Version: "2", Requires: []string{"database"}}}},
|
||||
delivery.CompositionPass, func(v delivery.CompositionVerdict) bool { return hasWarning(v, "on the trunk either") }},
|
||||
} {
|
||||
if v := Compose(c.mesh, c.c, "sdk", 0); v.State != c.state || !c.must(v) {
|
||||
t.Errorf("%s: composition = %+v", c.name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -467,15 +467,20 @@ func TestTwoBuilderInstancesStartingTogetherEndWithOneDoneKey(t *testing.T) { //
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnArchiveOnlyModuleNamingNoToolchainIsBuilt: the indexer gives an archive no language, so modules.ca-trust has no
|
||||
// toolchains; the builder builds it on the build module's nodes all the same (review round 4).
|
||||
func TestAnArchiveOnlyModuleNamingNoToolchainIsBuilt(t *testing.T) {
|
||||
b := startBed(t)
|
||||
b.archive(t, "ca-trust", "modules/ca-trust", commit)
|
||||
b.start(t, "n1", nil)
|
||||
v := b.await(t, "ca-trust", isState(delivery.BuildDone))
|
||||
if len(v.Toolchains) != 0 || len(v.Artifacts) != 1 || strings.HasPrefix(v.Step, "one node (") {
|
||||
t.Errorf("build = %+v, want done naming no toolchain", v)
|
||||
// TestAnArchiveOnlyModuleAndARootModuleAreBuilt: the indexer gives an archive no language, so modules.ca-trust has no
|
||||
// toolchains; the builder builds it on the build module's nodes all the same (review round 4). A module whose path is "."
|
||||
// is given to the build tools as it is (1615/AC4). One table: the two tests were merged to pay for the enrolment's.
|
||||
func TestAnArchiveOnlyModuleAndARootModuleAreBuilt(t *testing.T) {
|
||||
for _, c := range []struct{ name, path string }{{"no toolchain", "modules/ca-trust"}, {"root", delivery.RootPath}} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
b := startBed(t)
|
||||
b.archive(t, "ca-trust", c.path, commit)
|
||||
b.start(t, "n1", nil)
|
||||
v := b.await(t, "ca-trust", isState(delivery.BuildDone))
|
||||
if c.name == "no toolchain" && (len(v.Toolchains) != 0 || len(v.Artifacts) != 1 || strings.HasPrefix(v.Step, "one node (")) {
|
||||
t.Errorf("build = %+v, want done naming no toolchain", v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,14 +535,6 @@ func TestAToolchainChangeDoesNotBuildTheOldFingerprint(t *testing.T) { // 1615/A
|
||||
}
|
||||
}
|
||||
|
||||
// TestARootModuleIsBuilt: a module whose path is "." is given to the build tools as it is.
|
||||
func TestARootModuleIsBuilt(t *testing.T) { // 1615/AC4
|
||||
b := startBed(t)
|
||||
b.archive(t, "ca-trust", delivery.RootPath, commit)
|
||||
b.start(t, "n1", nil)
|
||||
b.await(t, "ca-trust", isState(delivery.BuildDone))
|
||||
}
|
||||
|
||||
func mustKey(t *testing.T, module string) string {
|
||||
k, err := delivery.BuilderBuildKey(module, fp)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
// mesh-enrolment whole on a real bus (novox/hq ADR 0587 §2-§8; issue 774): the holder's own code over a real nats-server's
|
||||
// key-value bucket `enrolments` with a time to live per key (the keys written as the runtime writes them: a create with the
|
||||
// expiry, a put with a message TTL, an update by revision), its events said on their real subjects, the bus's own expiry marker
|
||||
// heard, and the other parts stood in for — the vault (which writes the new node's key of node-logins at node-account, as the
|
||||
// vault does), the operator's channel, placement, and the grants part's rule that a key at live makes the user enrol.<node> and
|
||||
// a key gone takes it away.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
|
||||
// busStore is the holder's bucket as the runtime writes it.
|
||||
type busStore struct {
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
kv jetstream.KeyValue
|
||||
}
|
||||
|
||||
func (b *busStore) Get(key string) (Row, bool, error) {
|
||||
e, err := b.kv.Get(b.ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return Row{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return Row{}, false, err
|
||||
}
|
||||
return Row{Value: e.Value(), Revision: e.Revision()}, true, nil
|
||||
}
|
||||
|
||||
func (b *busStore) Create(key string, v any, ttl time.Duration) error {
|
||||
raw, err := Encode(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var opts []jetstream.KVCreateOpt
|
||||
if ttl > 0 {
|
||||
opts = append(opts, jetstream.KeyTTL(ttl))
|
||||
}
|
||||
_, err = b.kv.Create(b.ctx, key, raw, opts...)
|
||||
if errors.Is(err, jetstream.ErrKeyExists) {
|
||||
return ErrExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *busStore) Update(key string, v any, revision uint64) (uint64, error) {
|
||||
raw, err := Encode(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rev, err := b.kv.Update(b.ctx, key, raw, revision)
|
||||
if err != nil && strings.Contains(err.Error(), "wrong last sequence") {
|
||||
return 0, ErrChanged
|
||||
}
|
||||
return rev, err
|
||||
}
|
||||
|
||||
func (b *busStore) Put(key string, v any, ttl time.Duration) (uint64, error) {
|
||||
raw, err := Encode(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ttl <= 0 {
|
||||
return b.kv.Put(b.ctx, key, raw)
|
||||
}
|
||||
ack, err := b.js.Publish(b.ctx, "$KV."+b.kv.Bucket()+"."+key, raw, jetstream.WithMsgTTL(max(time.Second, ttl.Truncate(time.Second))))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ack.Sequence, nil
|
||||
}
|
||||
|
||||
func (b *busStore) Delete(key string) error {
|
||||
return b.kv.Delete(b.ctx, key)
|
||||
}
|
||||
|
||||
func (b *busStore) Keys() ([]string, error) {
|
||||
keys, err := b.kv.Keys(b.ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return keys, err
|
||||
}
|
||||
|
||||
// busReader reads other parts' buckets, `<module>.<state>`.
|
||||
type busReader struct {
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
}
|
||||
|
||||
func (r busReader) bucket(state string) (jetstream.KeyValue, error) {
|
||||
return r.js.KeyValue(r.ctx, strings.Replace(state, ".", "_", 1))
|
||||
}
|
||||
|
||||
func (r busReader) Keys(state string) ([]string, error) {
|
||||
kv, err := r.bucket(state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys, err := kv.Keys(r.ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (r busReader) Get(state, key string) ([]byte, bool, error) {
|
||||
kv, err := r.bucket(state)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
e, err := kv.Get(r.ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return e.Value(), true, nil
|
||||
}
|
||||
|
||||
// standInVault holds the tokens it made, and writes a joining node's key of node-logins when it mints the account.
|
||||
type standInVault struct {
|
||||
ctx context.Context
|
||||
logins jetstream.KeyValue
|
||||
mu sync.Mutex
|
||||
tokens map[string]bool
|
||||
calls []string
|
||||
}
|
||||
|
||||
func (v *standInVault) note(c string) {
|
||||
v.mu.Lock()
|
||||
v.calls = append(v.calls, c)
|
||||
v.mu.Unlock()
|
||||
}
|
||||
|
||||
func (v *standInVault) Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error) {
|
||||
v.mu.Lock()
|
||||
v.tokens[node] = true
|
||||
v.mu.Unlock()
|
||||
v.note("token " + node)
|
||||
return delivery.EnrolmentTokenAnswer{Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: "s"}, nil
|
||||
}
|
||||
func (v *standInVault) Sealed(node string) (string, error) { return "sealed-" + node, nil }
|
||||
func (v *standInVault) Drop(node string) error {
|
||||
v.mu.Lock()
|
||||
delete(v.tokens, node)
|
||||
v.mu.Unlock()
|
||||
v.note("drop " + node)
|
||||
return nil
|
||||
}
|
||||
func (v *standInVault) Account(node, sealing, digest string) (delivery.NodeAccountAnswer, error) {
|
||||
raw, err := delivery.EncodeNodeLogin(delivery.NodeLogin{Node: node, Login: delivery.FirstLogin(node), Since: time.Now().UTC()})
|
||||
if err != nil {
|
||||
return delivery.NodeAccountAnswer{}, err
|
||||
}
|
||||
if _, err := v.logins.Put(v.ctx, node, raw); err != nil {
|
||||
return delivery.NodeAccountAnswer{}, err
|
||||
}
|
||||
return delivery.NodeAccountAnswer{Node: node, Login: delivery.FirstLogin(node), Account: "account-" + node}, nil
|
||||
}
|
||||
func (v *standInVault) Retire(node string) error { return nil }
|
||||
func (v *standInVault) SignCertificate(node, serving string) (delivery.SignAnswer, error) {
|
||||
return delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil
|
||||
}
|
||||
func (v *standInVault) SignReply(node string, reply []byte) (delivery.SignAnswer, error) {
|
||||
return delivery.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil
|
||||
}
|
||||
func (v *standInVault) Signer() (string, error) { return "vault-key", nil }
|
||||
|
||||
// 774/S60/AC1: issue, a warrant, redeem as enrol.<node> and confirm, whole on a real bus. The user enrol.<node> is composed
|
||||
// while the key is live and leaves with it; the node confirms as the login its key of node-logins names; node-enrolled and
|
||||
// placement-changed are said; the vault is asked to drop the token only after the end; and a token never redeemed is ended by
|
||||
// the bus's own expiry, heard, saying enrolment-expired and bringing the drop.
|
||||
func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
srv := testbus.Start(t)
|
||||
nc, err := nats.Connect(srv.ClientURL())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(nc.Close)
|
||||
js, err := jetstream.New(nc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
mk := func(name string, ttl bool) jetstream.KeyValue {
|
||||
cfg := jetstream.KeyValueConfig{Bucket: name, History: 5}
|
||||
if ttl {
|
||||
// A bucket with a time to live per key keeps one revision per key: the bus (nats-server 2.11.17) expires no key of
|
||||
// one that keeps more, which this test found out (a state declared key-ttl takes no history).
|
||||
cfg.History, cfg.LimitMarkerTTL = 1, time.Minute
|
||||
}
|
||||
kv, err := js.CreateKeyValue(ctx, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return kv
|
||||
}
|
||||
enrolments := mk(delivery.EnrolmentsBucket, true)
|
||||
logins, lease, placement := mk(delivery.NodeLoginsBucket, false), mk("mesh-controller_lease", false), mk("mesh-delivery-assigner_placement", false)
|
||||
mk("node-engine_have", false)
|
||||
if _, err := lease.Put(ctx, "holder", []byte(`{"handed-to":"mesh-grants","handed-by":"operator"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := placement.Put(ctx, "hub.node", []byte(hubPlacement)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The seats' events are kept on a stream of the events, as the bus keeps them, and heard on their real subjects.
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEAT_EVENTS",
|
||||
Subjects: []string{"mesh.seat.mesh-enrolment.event.>", "mesh.seat.mesh-placement.event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var heardMu sync.Mutex
|
||||
var heard []string
|
||||
for _, subject := range []string{"mesh.seat.mesh-enrolment.event.*", "mesh.seat.mesh-placement.event.*"} {
|
||||
if _, err := nc.Subscribe(subject, func(m *nats.Msg) {
|
||||
heardMu.Lock()
|
||||
heard = append(heard, m.Subject[strings.LastIndex(m.Subject, ".")+1:])
|
||||
heardMu.Unlock()
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
events := func() []string { heardMu.Lock(); defer heardMu.Unlock(); return slices.Clone(heard) }
|
||||
|
||||
store := &busStore{ctx: ctx, js: js, kv: enrolments}
|
||||
vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]bool{}}
|
||||
asker := &fakeAsker{}
|
||||
h := &Holder{Store: store, Read: busReader{ctx, js}, Vault: vault, Asker: asker, AskLasts: time.Hour,
|
||||
Say: func(event string, body any, id string) error {
|
||||
raw, _ := json.Marshal(body)
|
||||
_, err := js.Publish(ctx, "mesh.seat.mesh-enrolment.event."+event, raw, jetstream.WithMsgID(id))
|
||||
return err
|
||||
},
|
||||
AddNode: func(a AddNode) error {
|
||||
value := `{"v":1,"node":"` + a.Change.Node + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}`
|
||||
if _, err := placement.Put(ctx, a.Change.Node+".node", []byte(value)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := js.Publish(ctx, "mesh.seat.mesh-placement.event.placement-changed", []byte(`{}`))
|
||||
return err
|
||||
},
|
||||
WarrantOf: func(ask string) (string, error) { return ask + "@2026-11-02T10:00:00Z", nil },
|
||||
Warrant: func(string) error { return nil },
|
||||
Act: func(string, string, string) error { return nil },
|
||||
Loaded: func(string) (bool, error) { return true, nil },
|
||||
Logf: t.Logf}
|
||||
|
||||
// The grants part's rule, stood in: a key at live is the user enrol.<node>; a key gone takes it away. The holder's watch
|
||||
// of its own bucket hears the bus's expiry marker.
|
||||
var usersMu sync.Mutex
|
||||
users := map[string]bool{}
|
||||
hasUser := func(u string) bool { usersMu.Lock(); defer usersMu.Unlock(); return users[u] }
|
||||
w, err := enrolments.WatchAll(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = w.Stop() })
|
||||
go func() {
|
||||
for e := range w.Updates() {
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
node, ok := strings.CutPrefix(e.Key(), "node.")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if e.Operation() == jetstream.KeyValuePut {
|
||||
en, err := decodeEnrolment(e.Value())
|
||||
usersMu.Lock()
|
||||
users[EnrolUser(node)] = err == nil && en.Stage == StageLive
|
||||
usersMu.Unlock()
|
||||
_ = h.Heard(stdio.OpPut, e.Key(), e.Value())
|
||||
continue
|
||||
}
|
||||
usersMu.Lock()
|
||||
delete(users, EnrolUser(node))
|
||||
usersMu.Unlock()
|
||||
// A delete is the holder's own; the marker the bus leaves when a key's own time to live ends is a purge.
|
||||
if e.Operation() == jetstream.KeyValuePurge {
|
||||
_ = h.Heard(stdio.OpExpire, e.Key(), nil)
|
||||
} else {
|
||||
_ = h.Heard(stdio.OpDelete, e.Key(), nil)
|
||||
}
|
||||
}
|
||||
}()
|
||||
// The durable consumer of the holder's own end events, stood in by a core subscription that asks the drop.
|
||||
for _, e := range EndEvents {
|
||||
key := Seat + "." + e
|
||||
if _, err := nc.Subscribe("mesh.seat.mesh-enrolment.event."+e, func(m *nats.Msg) {
|
||||
if err := h.Ended(key, m.Data); err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
waitFor := func(what string, ok func() bool) {
|
||||
t.Helper()
|
||||
for deadline := time.Now().Add(20 * time.Second); !ok(); time.Sleep(25 * time.Millisecond) {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("never: %s (events %v, vault %v)", what, events(), vault.calls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue, the operator's yes, redeem as enrol.ace, confirm as node.ace.
|
||||
a, err := h.Issue("person.operator", "ace", "a new machine", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Warranted(asks.Option{ID: "yes"}, asks.Warrant{Ask: a.Ask}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor("the user enrol.ace is composed", func() bool { return hasUser("enrol.ace") })
|
||||
args := redeemArgs("ace", 1, "serving-ace")
|
||||
if _, err := h.Redeem("enrol.somebody", args); err == nil {
|
||||
t.Fatal("redeemed by another caller")
|
||||
}
|
||||
ans, err := h.Redeem("enrol.ace", args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The node dials as the login its key of node-logins names, and confirms as it.
|
||||
e, err := logins.Get(ctx, "ace")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nl, err := delivery.DecodeNodeLogin(e.Value())
|
||||
if err != nil || nl.Login != ans.Login {
|
||||
t.Fatalf("node-logins says %+v, %v, and the answer %s", nl, err, ans.Login)
|
||||
}
|
||||
if _, err := h.Confirm(nl.Login, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor("node-enrolled and placement-changed are said and the user leaves the list", func() bool {
|
||||
ev := events()
|
||||
return slices.Contains(ev, EventEnrolled) && slices.Contains(ev, "placement-changed") && !hasUser("enrol.ace")
|
||||
})
|
||||
waitFor("the vault is asked to drop the token after the end", func() bool {
|
||||
vault.mu.Lock()
|
||||
defer vault.mu.Unlock()
|
||||
return slices.Contains(vault.calls, "drop ace") && !vault.tokens["ace"]
|
||||
})
|
||||
if keys, _ := store.Keys(); len(keys) != 0 {
|
||||
t.Errorf("an ended enrolment leaves keys %v", keys)
|
||||
}
|
||||
|
||||
// A token never redeemed expires on the bus: the holder hears the marker, says enrolment-expired and the token is dropped.
|
||||
if _, err := h.Issue("person.operator", "g14", "a second machine", 2*time.Second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Warranted(asks.Option{ID: "yes"}, asks.Warrant{Ask: asker.asked[len(asker.asked)-1]}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor("enrolment-expired is said for g14", func() bool { return slices.Contains(events(), EventExpired) })
|
||||
waitFor("the expired token is dropped", func() bool {
|
||||
vault.mu.Lock()
|
||||
defer vault.mu.Unlock()
|
||||
return slices.Contains(vault.calls, "drop g14")
|
||||
})
|
||||
if hasUser("enrol.g14") {
|
||||
t.Error("the user enrol.g14 stays after its key expired")
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,19 @@ go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 h1:xeuMBwOHNKrVUdGVg7Bdv5tkdueBvngwJFyk6n10rp0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 h1:xeuMBwOHNKrVUdGVg7Bdv5tkdueBvngwJFyk6n10rp0=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
|
||||
Reference in New Issue
Block a user