Files
mesh-catalog/modules/claude-licence-manager/cmd/manager/manager_test.go
T
jschoubben b1da6344a8 Join 11: merge origin/main (the licence manager in parts, issue 1409)
The conflict in the old cmd/claude-licence-manager/tools_test.go resolves to both sides: main
moved it to cmd/manager, and the join's count of binding.changed.<consumer> under its family
goes into the moved TestResyncSaysEachOnce. The whole-module test's runtime refused
binding.changed.<consumer> because the manifest names the family binding.changed: it now
accepts a key that extends a named family by further parts, as A-134 says the event.

Builds: design 47 G A-134

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 14:05:51 +01:00

693 lines
26 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
. "claude-licence-manager/internal/licences"
)
var t0 = time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC)
// stubVendor rotates like the real one is presumed to: each refresh token exchanges once.
type stubVendor struct {
mu sync.Mutex
live map[string]bool
exchanged []string
issued int
account string
now func() time.Time
// usage is what the usage endpoint answers, and usageErr its failure; nil answers a session window alone.
usage map[string]any
usageErr error
}
func (v *stubVendor) Refresh(_ context.Context, g FullGrant) Refreshed {
v.mu.Lock()
defer v.mu.Unlock()
v.exchanged = append(v.exchanged, g.RefreshToken)
if !v.live[g.RefreshToken] {
return Refreshed{Status: 400, Reason: `{"error":"invalid_grant"}`}
}
delete(v.live, g.RefreshToken)
v.issued++
next := fmt.Sprintf("rt-%d", v.issued)
v.live[next] = true
g.AccessToken = fmt.Sprintf("at-%d", v.issued)
g.RefreshToken = next
g.ExpiresAt = v.now().Add(8 * time.Hour).UnixMilli()
exp := v.now().Add(30 * 24 * time.Hour).UnixMilli()
g.RefreshTokenExpiresAt = &exp
return Refreshed{OK: true, Grant: g, Account: v.account}
}
func (v *stubVendor) Usage(context.Context, string) (map[string]any, error) {
if v.usageErr != nil {
return nil, v.usageErr
}
if v.usage != nil {
return v.usage, nil
}
return map[string]any{"five_hour": map[string]any{"utilization": 12.0}}, nil
}
// memGrants is the vault as the manager sees it: every licence's two secrets by name, the newest generation kept.
type memGrants struct {
mu sync.Mutex
held map[string]heldGrant
puts []string
// refuseAccess is the vault refusing a licence's access token to the manager's machine, as it does for a licence no
// binding there names (§17).
refuseAccess bool
}
type heldGrant struct {
generation int64
value string
}
func (g *memGrants) Put(_ context.Context, key, part string, generation int64, value string) error {
g.mu.Lock()
defer g.mu.Unlock()
name := SecretName(key, part)
if h, ok := g.held[name]; ok && h.generation == generation && h.value == value {
return nil // the vault keeps what it holds, unchanged
} else if ok && h.generation >= generation {
return fmt.Errorf("%s is held at generation %d, and generation %d is not newer", name, h.generation, generation)
}
g.held[name] = heldGrant{generation, value}
g.puts = append(g.puts, fmt.Sprintf("%s@%d", name, generation))
return nil
}
func (g *memGrants) Get(_ context.Context, key, part string) (string, error) {
g.mu.Lock()
defer g.mu.Unlock()
h, ok := g.held[SecretName(key, part)]
if g.refuseAccess && part == PartAccess {
return "", fmt.Errorf("no consumer on this machine is bound to the licence %s", key)
}
if !ok {
return "", fmt.Errorf("the vault holds no secret named %s", SecretName(key, part))
}
return h.value, nil
}
type miniMesh struct {
grants *memGrants
m *Manager
store *BusStore
usage *memLog
audit *memLog
licences *memState
bindings *memState
leases *memState
offered *memState
vendor *stubVendor
logins map[string]FullGrant // node → what its credentials file holds
events []string
now time.Time
keys KeyPair
askDown bool
}
func newMesh(t *testing.T) *miniMesh {
t.Helper()
mm := &miniMesh{logins: map[string]FullGrant{}, now: t0,
grants: &memGrants{held: map[string]heldGrant{}}}
now := func() time.Time { return mm.now }
mm.usage, mm.audit = newMemLog(now), newMemLog(now)
mm.licences, mm.bindings = newMemState(now), newMemState(now)
mm.leases, mm.offered = newMemState(now), newMemState(now)
mm.store = &BusStore{LicencesState: mm.licences, BindingsState: mm.bindings, LeasesState: mm.leases,
OfferedState: mm.offered, UsageLog: mm.usage, AuditLog: mm.audit, Now: now}
mm.vendor = &stubVendor{live: map[string]bool{}, now: now}
mm.keys, _ = GenerateKeyPair()
mm.m = &Manager{
Store: mm.store, Vendor: mm.vendor, Grants: mm.grants, Keys: mm.keys,
AskGrant: func(_ context.Context, node, publicKey string) (GrantAnswer, error) {
if mm.askDown {
return GrantAnswer{}, fmt.Errorf("503 no responders")
}
g, ok := mm.logins[node]
if !ok {
return GrantAnswer{}, nil
}
raw, _ := json.Marshal(g)
box, err := Seal(string(raw), publicKey)
return GrantAnswer{Sealed: &box, Fingerprint: Fingerprint(g.RefreshToken)}, err
},
Emit: func(event string, body map[string]any) error {
raw, _ := json.Marshal(body)
mm.events = append(mm.events, event+" "+string(raw))
return nil
},
Now: now, Log: func(string, ...any) {}, Holder: "test", Settings: Defaults,
}
return mm
}
// bound is a consumer's value in the state bindings, as its readers decode it.
func (mm *miniMesh) bound(consumer string) BindingState {
var b BindingState
if e, _ := mm.bindings.Get(consumer); e != nil {
_ = json.Unmarshal(e.Value, &b)
}
return b
}
func (mm *miniMesh) report(node, rt string, at time.Time, account string) Holdings {
h := Holdings{Node: node, Identity: &Identity{AccountUUID: account, EmailAddress: account + "@example.org"},
Kind: "subscription", ChangedAt: at.Format(time.RFC3339Nano)}
if rt != "" {
h.Refresh.Present, h.Refresh.Fingerprint = true, Fingerprint(rt)
}
return h
}
func (mm *miniMesh) login(node, rt string, valid bool, at time.Time) Holdings {
mm.logins[node] = FullGrant{AccessToken: "local-" + node, RefreshToken: rt, ExpiresAt: mm.now.Add(time.Hour).UnixMilli()}
if valid {
mm.vendor.live[rt] = true
}
return mm.report(node, rt, at, "acct-1")
}
const licence1 = "acct-1@example.org"
func TestAManagerWithNoLicenceAdoptsTheNewestLoginThatRefreshesAndNeverExchangesTheRest(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
older := mm.login("server", "rt-server", true, t0.Add(-time.Hour))
newest := mm.login("laptop", "rt-laptop", true, t0.Add(-time.Minute))
adopted, err := mm.m.Consider(ctx, []Holdings{older, newest})
if err != nil || len(adopted) != 1 || adopted[0] != licence1 {
t.Fatalf("adopted %v, %v", adopted, err)
}
if strings.Join(mm.vendor.exchanged, ",") != "rt-laptop" {
t.Fatalf("exchanged %v: an older login was exchanged although a newer one refreshed", mm.vendor.exchanged)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-server")); o != Skipped {
t.Fatalf("the older login is %q, not skipped", o)
}
// A first binding follows the login: both nodes reported this account and were bound to nothing.
bs, _ := mm.store.Bindings(ctx)
if len(bs) != 2 || mm.bound("laptop").Licence != licence1 || mm.bound("server").Licence != licence1 {
t.Fatalf("bindings %v, state %v", bs, mm.bindings.values)
}
// The adoption is said, and so is each binding it made, once (novox/hq issue 625; design 39 bus-and-switch §21).
events := strings.Join(mm.events, "|")
for _, want := range []string{"licence.adopted ", `"consumer":"laptop"`, `"consumer":"server"`} {
if strings.Count(events, want) != 1 {
t.Fatalf("events say %s other than once: %v", want, mm.events)
}
}
}
func TestALoginThatDoesNotRefreshAdoptsNothingAndIsNotTriedAgain(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
dead := mm.login("laptop", "rt-dead", false, t0)
if adopted, _ := mm.m.Consider(ctx, []Holdings{dead}); len(adopted) != 0 {
t.Fatalf("adopted %v", adopted)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-dead")); o != Dead {
t.Fatalf("outcome %q", o)
}
if ls, _ := mm.store.Licences(ctx); len(ls) != 0 {
t.Fatalf("licences %v", ls)
}
if !strings.Contains(strings.Join(mm.events, "|"), "licence.refused") {
t.Fatalf("events %v", mm.events)
}
_, _ = mm.m.Consider(ctx, []Holdings{dead})
if len(mm.vendor.exchanged) != 1 {
t.Fatalf("a dead refresh token was exchanged %d times", len(mm.vendor.exchanged))
}
}
func TestANewerLoginReplacesTheGrantHeldAndAnOlderOneDoesNot(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0.Add(-time.Minute))})
before, _ := mm.store.Licence(ctx, licence1)
// §2: a licence says where it was adopted from, and a newer login keeps the latest reading the key holds.
pct := 42.0
before.Usage = &UsageReading{SessionPct: &pct, ReadAt: t0.Format(time.RFC3339Nano)}
if before.AdoptedFrom != "laptop" || mm.store.SaveLicence(ctx, *before) != nil {
t.Fatalf("the adopted licence is %+v", before)
}
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("server", "rt-old", true, t0.Add(-24*time.Hour))})
if strings.Join(mm.vendor.exchanged, ",") != "rt-a" {
t.Fatalf("an older login was exchanged: %v", mm.vendor.exchanged)
}
mm.now = t0.Add(10 * time.Minute)
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("desktop", "rt-new", true, t0.Add(10*time.Minute))})
after, _ := mm.store.Licence(ctx, licence1)
if after.RefreshFingerprint == before.RefreshFingerprint || mm.vendor.exchanged[len(mm.vendor.exchanged)-1] != "rt-new" {
t.Fatalf("a newer login did not win: %v", mm.vendor.exchanged)
}
if after.AdoptedFrom != "desktop" || after.Usage == nil || after.Usage.SessionPct == nil || *after.Usage.SessionPct != 42 {
t.Fatalf("after a newer login the licence is %+v", after)
}
if ls, _ := mm.store.Licences(ctx); len(ls) != 1 {
t.Fatalf("one account became %d licences", len(ls))
}
}
func TestAReportNamingAnotherAccountThanTheVendorAnsweredForIsRefused(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
mm.vendor.account = "someone-else"
if adopted, _ := mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)}); len(adopted) != 0 {
t.Fatalf("adopted %v", adopted)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-a")); o != Refused {
t.Fatalf("outcome %q", o)
}
}
func TestTwoRefreshRunsStartedTogetherRotateAGrantOnce(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
mm.now = t0.Add(5 * time.Hour)
second := &Manager{Store: mm.m.Store, Vendor: mm.m.Vendor, Grants: mm.m.Grants, Keys: mm.m.Keys,
AskGrant: mm.m.AskGrant, Emit: mm.m.Emit, Now: mm.m.Now, Log: mm.m.Log, Holder: "another run",
Settings: mm.m.Settings}
var wg sync.WaitGroup
results := make([]map[string]any, 2)
for i, m := range []*Manager{mm.m, second} {
wg.Add(1)
go func() { defer wg.Done(); results[i], _ = m.Rotate(ctx, licence1, false) }()
}
wg.Wait()
n := 0
for _, r := range results {
if r["refreshed"] == true {
n++
}
}
if n != 1 {
t.Fatalf("rotated %d times: %v", n, results)
}
if r, _ := second.Rotate(ctx, licence1, false); r["reason"] != "not due" {
t.Fatalf("a run just after was %v", r)
}
}
func TestARotationAndASwitchEachGiveANewerGeneration(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
g0 := mm.bound("laptop").Generation
_, _ = mm.m.Rotate(ctx, licence1, true)
g1 := mm.bound("laptop").Generation
if g1 <= g0 {
t.Fatalf("a rotation went from %d to %d", g0, g1)
}
file := filepath.Join(t.TempDir(), "key")
_ = os.WriteFile(file, []byte("sk-ant-api03-key-0123456789abcdefghij\n"), 0o600)
if _, err := mm.m.AdoptKey(ctx, "api", file); err != nil {
t.Fatal(err)
}
if _, err := mm.m.Bind(ctx, "laptop", "api"); err != nil {
t.Fatal(err)
}
if mm.bound("laptop").Licence != "api" || mm.bound("laptop").Generation <= g1 {
t.Fatalf("a switch to a licence rotated less often went backwards: %v", mm.bound("laptop"))
}
if _, err := mm.m.Release(ctx, "laptop"); err != nil {
t.Fatal(err)
}
if _, ok := mm.bindings.values["laptop"]; ok {
t.Fatal("a released consumer still has a binding in the state")
}
}
// design 39 bus-and-switch §18, B7, B19: no answer of current holds a token; it names the licence's key and health.
func TestCurrentHoldsNoToken(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
answer, err := mm.m.Current(ctx, "laptop")
if err != nil || answer["kind"] != "subscription" || answer["key"] != KeyFor(licence1) || answer["health"] != "ok" ||
answer["accessExpiresAt"] == nil || answer["identity"] == nil {
t.Fatalf("%v %v", answer, err)
}
raw, _ := json.Marshal(answer)
for _, token := range []string{"rt-", "at-", "sealed"} {
if strings.Contains(string(raw), token) {
t.Fatalf("current answered %s: %s", token, raw)
}
}
l, _ := mm.store.Licence(ctx, licence1)
for failures, health := range map[int]string{0: "ok", 3: "failing"} {
l.Failures = failures
if got := Health(*l, mm.now, mm.m.Settings); got != health {
t.Errorf("%d failures: %s, want %s", failures, got, health)
}
}
l.Failures, l.RefreshExpiresAt = 0, mm.now.Add(48*time.Hour).UnixMilli()
if got := Health(*l, mm.now, mm.m.Settings); got != "expiring" {
t.Errorf("a refresh token expiring in two days: %s", got)
}
if a, _ := mm.m.Current(ctx, "nobody"); a != nil {
t.Fatalf("a consumer bound to nothing was answered %v", a)
}
if bs, _ := mm.m.BindingsOf(ctx, "laptop"); len(bs) != 1 || bs[0].Consumer != "laptop" {
t.Fatalf("bindings of laptop: %v", bs)
}
// design 39 bus-and-switch B25: the verb bindings answers each binding's licence key beside the §3 fields.
if bs, _ := mm.m.BindingsOf(ctx, ""); len(bs) == 0 || bs[0].Key != KeyFor(bs[0].Licence) {
t.Fatalf("bindings answered %+v", bs)
}
}
func TestNothingTheManagerPublishesKeepsOrListsCarriesAToken(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-secret-1", true, t0)})
_, _ = mm.m.Rotate(ctx, licence1, true)
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("server", "rt-secret-dead", false, t0.Add(time.Hour))})
_ = mm.m.ReadUsage(ctx)
ls, _ := mm.m.Licences(ctx)
bs, _ := mm.store.Bindings(ctx)
everything, _ := json.Marshal([]any{mm.events, mm.bindings.values, mm.licences.values, ls, bs})
for _, token := range []string{"rt-secret", "rt-1", "rt-2", "at-1", "at-2", "local-"} {
if strings.Contains(string(everything), token) {
t.Fatalf("%s was published: %s", token, everything)
}
}
row, _ := mm.store.Licence(ctx, licence1)
if raw, _ := json.Marshal(row); strings.Contains(string(raw), "rt-") || strings.Contains(string(raw), "at-") {
t.Fatalf("the licence keeps a token: %s", raw)
}
}
func TestANodeThatDoesNotAnswerIsAskedAgainAndOneWithNoLoginIsSettled(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
r := mm.login("laptop", "rt-a", true, t0)
mm.askDown = true
if adopted, _ := mm.m.Consider(ctx, []Holdings{r}); len(adopted) != 0 {
t.Fatalf("adopted %v from a node that did not answer", adopted)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-a")); o != "" {
t.Fatalf("a node that was down was settled %q", o)
}
mm.askDown = false
if adopted, _ := mm.m.Consider(ctx, []Holdings{r}); len(adopted) != 1 {
t.Fatalf("adopted %v on the next pass", adopted)
}
gone := mm.report("server", "rt-gone", t0.Add(time.Second), "acct-2")
_, _ = mm.m.Consider(ctx, []Holdings{gone})
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-gone")); o != Gone {
t.Fatalf("outcome %q", o)
}
}
// design 39 bus-and-switch §23, §46, B12, B22: a holdings-changed, decoded from a copy of the agent module's form, is
// kept as its node's latest report, and one carrying a fingerprint the manager does not hold is a candidate; the
// manager asks every node with holdings.asked, its time and who asks.
func TestAHoldingsChangedIsHeardAndMakesACandidate(t *testing.T) {
mm := newMesh(t)
raw, err := os.ReadFile("testdata/holdings-changed.json")
if err != nil {
t.Fatal(err)
}
heard := &Heard{}
if _, err := heard.Take(raw); err != nil {
t.Fatal(err)
}
all := heard.All()
if len(all) != 1 || heard.Of("laptop") != 4 {
t.Fatalf("heard %+v", all)
}
h := all[0]
if h.Node != "laptop" || h.Identity.AccountUUID != "u-1" || !h.Refresh.Present || h.Refresh.Fingerprint != "sha256:0123456789abcdef" {
t.Fatalf("%+v", h)
}
if _, err := time.Parse(time.RFC3339Nano, h.ChangedAt); err != nil {
t.Fatalf("the report's time does not parse: %v", err)
}
if c, err := mm.m.CandidatesIn(context.Background(), all); err != nil || len(c["u-1"]) != 1 {
t.Fatalf("candidates %v %v", c, err)
}
if _, err := heard.Take([]byte(`{"kind":"subscription"}`)); err == nil {
t.Fatal("a report naming no node was kept")
}
if err := mm.m.AskHoldings(Itself); err != nil || len(mm.events) != 1 ||
mm.events[0] != HoldingsAsked+` {"at":"`+t0.UTC().Format(time.RFC3339Nano)+`","by":"claude-licence-manager"}` {
t.Fatalf("said %v %v", mm.events, err)
}
}
// A node whose report arrives after its account was adopted — with an older login, so never a candidate —
// is still bound to that account's licence, once.
func TestANodeReportingAnAdoptedAccountLaterIsBoundToIt(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("novox", "rt-new", true, t0)})
late := mm.login("laptop", "rt-older", true, t0.Add(-24*time.Hour))
_, _ = mm.m.Consider(ctx, []Holdings{late})
if mm.bound("laptop").Licence != licence1 {
t.Fatalf("a node reporting the adopted account later was not bound: %v", mm.bindings.values)
}
if strings.Contains(strings.Join(mm.vendor.exchanged, ","), "rt-older") {
t.Fatal("the older login was exchanged")
}
g := mm.bound("laptop").Generation
_, _ = mm.m.Consider(ctx, []Holdings{late})
if mm.bound("laptop").Generation != g {
t.Fatal("a node already bound was bound again")
}
}
// A login to another account on one node adopts it and moves that node, and only that node (ADR 0209).
func TestALoginToAnotherAccountMovesItsNodeAndNoOther(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0), mm.report("server", "", t0, "acct-1")})
if mm.bound("laptop").Licence != licence1 || mm.bound("server").Licence != licence1 {
t.Fatalf("%v", mm.bindings.values)
}
mm.now = t0.Add(time.Hour)
mm.logins["laptop"] = FullGrant{AccessToken: "local", RefreshToken: "rt-b", ExpiresAt: 1}
mm.vendor.live["rt-b"] = true
second := mm.report("laptop", "rt-b", t0.Add(time.Hour), "acct-2")
adopted, _ := mm.m.Consider(ctx, []Holdings{second, mm.report("server", "", t0, "acct-1")})
if len(adopted) != 1 || adopted[0] != "acct-2@example.org" {
t.Fatalf("adopted %v", adopted)
}
if mm.bound("laptop").Licence != "acct-2@example.org" {
t.Fatalf("the node a login was made on stayed bound to %v", mm.bound("laptop"))
}
if mm.bound("server").Licence != licence1 {
t.Fatalf("another node moved: %v", mm.bound("server"))
}
ls, _ := mm.store.Licences(ctx)
if len(ls) != 2 {
t.Fatalf("%d licences", len(ls))
}
}
// A login that does not refresh moves nothing.
func TestALoginThatDoesNotRefreshMovesNothing(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
g := mm.bound("laptop")
mm.logins["laptop"] = FullGrant{AccessToken: "local", RefreshToken: "rt-dead", ExpiresAt: 1}
_, _ = mm.m.Consider(ctx, []Holdings{mm.report("laptop", "rt-dead", t0.Add(time.Hour), "acct-2")})
if mm.bound("laptop") != g {
t.Fatalf("a dead login moved its node: %v", mm.bound("laptop"))
}
}
// An API key sealed to the manager by a node is adopted, and opens only for the manager.
func TestAnAPIKeySealedByANodeIsAdopted(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
box, _ := Seal("sk-ant-api03-test-0123456789abcdefghij\n", mm.keys.PublicKey)
r, err := mm.m.AdoptSealedKey(ctx, "api", box, "laptop")
if err != nil || r["adopted"] != true {
t.Fatalf("%v %v", r, err)
}
for _, part := range []string{PartGrant, PartAccess} {
if plain, _ := mm.grants.Get(ctx, "api", part); plain != "sk-ant-api03-test-0123456789abcdefghij" {
t.Fatalf("the vault holds %q as the %s", plain, part)
}
}
other, _ := GenerateKeyPair()
wrong, _ := Seal("sk", other.PublicKey)
if _, err := mm.m.AdoptSealedKey(ctx, "api2", wrong, "laptop"); err == nil {
t.Fatal("a key sealed to another key was adopted")
}
for _, e := range mm.events {
if strings.Contains(e, "sk-ant") {
t.Fatalf("the key was published: %s", e)
}
}
}
// A store rebuilt after a loss counts generations from one again (novox/hq issue 243): the first look at the
// reports moves the count past every generation a node says it applied, so no binding repeats one.
func TestARebuiltStoreNeverGivesAGenerationANodeHasPassed(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
laptop := mm.login("laptop", "rt-a", true, t0)
laptop.Generation = 16
if _, err := mm.m.Consider(ctx, []Holdings{laptop}); err != nil {
t.Fatal(err)
}
if g := mm.bound("laptop").Generation; g <= 16 {
t.Fatalf("the laptop applied generation 16 and was given %d", g)
}
}
// What is not an API key is never adopted as one, and the refusal never repeats it (novox/hq ADR 0266).
func TestWhatIsNotAnAPIKeyIsRefused(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
for _, notAKey := range []string{"-----BEGIN OPENSSH PRIVATE KEY-----\nabc", "SUAOPERATORNKEYSEED", "sk-ant-short"} {
box, _ := Seal(notAKey, mm.keys.PublicKey)
_, err := mm.m.AdoptSealedKey(ctx, "api", box, "laptop")
if err == nil || strings.Contains(err.Error(), notAKey) {
t.Fatalf("%q: refused without repeating it, got %v", notAKey, err)
}
}
if l, _ := mm.store.Licence(ctx, "api"); l != nil {
t.Fatal("nothing was kept")
}
}
// changedSince is the binding.changed events said from mark on, decoded.
func (mm *miniMesh) changedSince(mark int) []map[string]any {
var out []map[string]any
for _, e := range mm.events[mark:] {
if rest, ok := strings.CutPrefix(e, BindingChanged+"."); ok {
// The consumer is the event's further part (A-134); one said under another consumer's name reads as none.
consumer, body, _ := strings.Cut(rest, " ")
var v map[string]any
_ = json.Unmarshal([]byte(body), &v)
if v["consumer"] != consumer {
v["consumer"] = "said as binding.changed." + consumer
}
out = append(out, v)
}
}
return out
}
// A change of what a consumer holds says what, why and who in the journal, the audit and an event (novox/hq
// issue 625): a login, a switch through the verb by a proven caller, a rotation and a release. design 39
// bus-and-switch §21/AC1, §36/AC1, B7, B16: each says one binding.changed per consumer moved, with its licence's key,
// after its key is written or deleted; a switch says one licence.switched with §27's fields.
func TestEveryMoveSaysOneBindingChangedPerConsumer(t *testing.T) {
mm := newMesh(t)
var lines []string
mm.m.Log = func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) }
ctx := context.Background()
once := func(mark int, consumer, licence, by string) map[string]any {
t.Helper()
got := mm.changedSince(mark)
if len(got) != 1 || got[0]["consumer"] != consumer || got[0]["licence"] != licence || got[0]["by"] != by {
t.Fatalf("binding.changed said %v, want one for %s to %q by %s", got, consumer, licence, by)
}
if licence != "" && got[0]["key"] != KeyFor(licence) {
t.Fatalf("binding.changed names the key %v", got[0]["key"])
}
if mm.bound(consumer).Licence != licence {
t.Fatalf("said before the key was written: %+v", mm.bound(consumer))
}
return got[0]
}
mark := len(mm.events)
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
if s := mm.bound("laptop"); !strings.Contains(s.Why, "a login to "+licence1+" on laptop") || s.By != Itself {
t.Fatalf("a login's binding said %+v", s)
}
once(mark, "laptop", licence1, Itself)
file := filepath.Join(t.TempDir(), "key")
_ = os.WriteFile(file, []byte("sk-ant-api03-key-0123456789abcdefghij\n"), 0o600)
if _, err := mm.m.AdoptKey(ctx, "api", file); err != nil {
t.Fatal(err)
}
mark = len(mm.events)
asked := WithCause(ctx, Cause{Why: "the switch verb", By: "g14.node-tools"})
if _, err := mm.m.Bind(asked, "laptop", "api"); err != nil {
t.Fatal(err)
}
if e := once(mark, "laptop", "api", "g14.node-tools"); e["from"] != licence1 || e["kind"] != "api-key" || e["at"] == nil {
t.Fatalf("the switch said %v", e)
}
if s := mm.bound("laptop"); s.Why != "the switch verb" || s.By != "g14.node-tools" {
t.Fatalf("the node is told %+v", s)
}
said := strings.Join(lines, "\n")
if !strings.Contains(said, "switched laptop from "+licence1+" to api (generation ") ||
!strings.Contains(said, "the switch verb; asked by g14.node-tools") {
t.Fatalf("the journal said:\n%s", said)
}
var switched []string
for _, e := range mm.events {
if strings.HasPrefix(e, "licence.switched ") {
switched = append(switched, e)
}
}
if len(switched) != 1 {
t.Fatalf("licence.switched said %d times: %v", len(switched), mm.events)
}
for _, want := range []string{`"by":"g14.node-tools"`, `"from":"` + licence1 + `"`, `"to":"api"`, `"consumers":["laptop"]`, `"why":"the switch verb"`} {
if !strings.Contains(switched[0], want) {
t.Fatalf("the event %q lacks %s", switched[0], want)
}
}
all := mm.audit.under("switched")
if raw, _ := json.Marshal(all[len(all)-1]); !strings.Contains(string(raw), `"by":"g14.node-tools"`) || !strings.Contains(string(raw), `"to":"api"`) {
t.Fatalf("the audit kept %s", raw)
}
_, _ = mm.m.Bind(ctx, "server", licence1)
mark = len(mm.events)
_, _ = mm.m.Rotate(WithCause(ctx, Cause{Why: "the refresh verb", By: "novox.node-tools"}), licence1, true)
once(mark, "server", licence1, "novox.node-tools")
mark = len(mm.events)
if _, err := mm.m.Release(WithCause(ctx, Cause{Why: "the release verb", By: "person.operator"}), "laptop"); err != nil {
t.Fatal(err)
}
if e := once(mark, "laptop", "", "person.operator"); e["from"] != "api" {
t.Fatalf("the release said %v", e)
}
if said := strings.Join(lines, "\n"); !strings.Contains(said, "released laptop from api: the release verb; asked by person.operator") {
t.Fatalf("the journal said:\n%s", said)
}
for _, e := range mm.events {
for _, gone := range []string{"licence.bound ", "licence.released ", "licence.rotated "} {
if strings.HasPrefix(e, gone) {
t.Fatalf("an event no longer said was said: %s", e)
}
}
if strings.Contains(e, "rt-a") || strings.Contains(e, "sk-ant-") || strings.Contains(e, "local-laptop") {
t.Fatalf("an event carries a secret: %s", e)
}
}
}
// A call with no cause is said as this module's own, never as nobody.
func TestNoCauseReadsAsTheModulesOwn(t *testing.T) {
if c := CauseOf(context.Background()); c.By != Itself || c.Why == "" {
t.Fatalf("no cause read as %+v", c)
}
}