package main // Wiring tests for issue 0006c: --store kv selects the replicated JetStream KV // control plane, the authenticator serves from it through the storeHolder, and a // new node sees state created by another (the divergence that per-node SQLite // caused — audit 0008 N5 — is gone). Branch-by-abstraction is verified elsewhere // (the SQLite default path is the unchanged baseline covered by the existing // suite). import ( "encoding/hex" "testing" "time" cs "fn-registry/functions/cybersecurity" "github.com/enmanuel/unibus/pkg/busauth" "github.com/enmanuel/unibus/pkg/embeddednats" "github.com/enmanuel/unibus/pkg/membership" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" ) // TestKVStoreBootstrapUnderEnforce drives the exact decentralized boot the binary // performs: build the authenticator over an empty holder, start NATS, open the // privileged internal connection, open the KV store, publish it into the holder, // then a real bus user (seeded into the KV store) authenticates over nkey. This // proves the bootstrap cycle is broken correctly — the KV-backed control plane // authorizes live clients under enforce. func TestKVStoreBootstrapUnderEnforce(t *testing.T) { internalID, err := cs.GenerateIdentity() if err != nil { t.Fatalf("internal identity: %v", err) } holder := &storeHolder{} auth := busauth.NewNkeyAuthenticatorACLInternal( holder.IsAuthorized, busauth.PermissionsFromSubjects(holder.subjectACL), hex.EncodeToString(internalID.SignPub), ) ns, err := embeddednats.StartServer(embeddednats.ServerConfig{ StoreDir: t.TempDir(), Host: "127.0.0.1", Port: freePort(t), Auth: auth, }) if err != nil { t.Fatalf("nats: %v", err) } t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() }) // Privileged internal connection opens the KV store while the holder still // denies every normal client. intNC, js, err := connectInternalJS(ns, internalID, true) if err != nil { t.Fatalf("connectInternalJS: %v", err) } t.Cleanup(intNC.Close) kvStore, err := membership.OpenJetStream(js, membership.JetStreamConfig{Replicas: 1, OpTimeout: 3 * time.Second}) if err != nil { t.Fatalf("open kv store: %v", err) } holder.set(kvStore) // Seed a bus user into the KV control plane. alice, err := cs.GenerateIdentity() if err != nil { t.Fatalf("alice: %v", err) } if err := kvStore.AddUser(hex.EncodeToString(alice.SignPub), "alice", membership.RoleMember); err != nil { t.Fatalf("seed alice: %v", err) } // alice authenticates over nkey — authorized via the KV store through the holder. pub, sign, err := busauth.ClientNkey(alice.SignPriv) if err != nil { t.Fatalf("alice nkey: %v", err) } aliceNC, err := nats.Connect(ns.ClientURL(), nats.Nkey(pub, sign), nats.MaxReconnects(0), nats.Timeout(2*time.Second)) if err != nil { t.Fatalf("alice (KV-authorized) must connect under enforce: %v", err) } aliceNC.Close() // An outsider not in the KV store is denied (fail closed). outsider, err := cs.GenerateIdentity() if err != nil { t.Fatalf("outsider: %v", err) } opub, osign, err := busauth.ClientNkey(outsider.SignPriv) if err != nil { t.Fatalf("outsider nkey: %v", err) } if oc, err := nats.Connect(ns.ClientURL(), nats.Nkey(opub, osign), nats.MaxReconnects(0), nats.Timeout(2*time.Second)); err == nil { oc.Close() t.Fatalf("an outsider absent from the KV store must be rejected") } } // TestKVStoreDecentralizedConsistency: a room/user created via one node's KV store // is immediately visible to another node's KV store over the same JetStream — the // shared, replicated control plane that ends the per-node SQLite divergence. func TestKVStoreDecentralizedConsistency(t *testing.T) { ns, err := embeddednats.StartServer(embeddednats.ServerConfig{ StoreDir: t.TempDir(), Host: "127.0.0.1", Port: freePort(t), }) if err != nil { t.Fatalf("nats: %v", err) } t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() }) open := func() membership.Store { nc, err := nats.Connect(ns.ClientURL()) if err != nil { t.Fatalf("connect: %v", err) } t.Cleanup(nc.Close) js, err := jetstream.New(nc) if err != nil { t.Fatalf("jetstream: %v", err) } st, err := membership.OpenJetStream(js, membership.JetStreamConfig{Replicas: 1, OpTimeout: 3 * time.Second}) if err != nil { t.Fatalf("open kv: %v", err) } return st } nodeA := open() nodeB := open() owner, err := cs.GenerateIdentity() if err != nil { t.Fatalf("owner: %v", err) } ownerPub := hex.EncodeToString(owner.SignPub) if err := nodeA.AddUser(ownerPub, "owner", membership.RoleAdmin); err != nil { t.Fatalf("nodeA add user: %v", err) } if err := nodeA.CreateRoom( membership.RoomInfo{RoomID: "ROOMX", Subject: "room.shared.x", OwnerEndpoint: "owner-ep"}, owner.SignPub, owner.KexPub, nil, ); err != nil { t.Fatalf("nodeA create room: %v", err) } // nodeB (a different connection, same buckets) sees both immediately. if !nodeB.IsAuthorized(ownerPub) { t.Fatalf("nodeB must see the user created on nodeA (decentralized state divergence)") } got, err := nodeB.GetRoom("ROOMX") if err != nil { t.Fatalf("nodeB must see the room created on nodeA: %v", err) } if got.Subject != "room.shared.x" { t.Fatalf("nodeB read wrong room subject: %q", got.Subject) } }