package client_test import ( "encoding/hex" "testing" "github.com/enmanuel/unibus/pkg/client" "github.com/enmanuel/unibus/pkg/membership" ) // TestClientInvitesAdminAPI drives the wallet-model account flow through the real // pkg/client methods against an in-process membershipd under enforce: an admin // mints an invite, a brand-new identity redeems it via the UNSIGNED Register call // (it is not yet in the allowlist), the admin then sees the user, and finally the // admin hard-deletes it and it vanishes. This is the exact path the admin panel + // the /join client page depend on, so it locks the client/server contract. func TestClientInvitesAdminAPI(t *testing.T) { h := newHarnessMode(t, membership.AuthEnforce) waitHealth(t, h.ctrlURL) admin, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t)) if err != nil { t.Fatalf("connect admin: %v", err) } defer admin.Close() registerClient(t, h, admin, "admin", membership.RoleAdmin) // Admin mints a single-use invite fixing handle + role. inv, err := admin.CreateInvite("dora", membership.RoleMember, 0) if err != nil { t.Fatalf("admin CreateInvite: %v", err) } if len(inv.Token) != 64 || inv.ExpiresAt == "" { t.Fatalf("invite malformed: %+v", inv) } if inv.Handle != "dora" || inv.Role != membership.RoleMember { t.Fatalf("invite echo wrong: %+v", inv) } // It appears among the pending invites. pend, err := admin.ListInvites() if err != nil { t.Fatalf("admin ListInvites: %v", err) } if !containsToken(pend, inv.Token) { t.Fatalf("minted invite not pending: %+v", pend) } // A brand-new identity (NOT in the allowlist) redeems the invite via the // UNSIGNED Register. We model its locally-generated keypair with a fresh // identity and present its two public keys. Redeeming through this joiner // client — which never registered and never seeded an admin — proves Register // needs no admin signature; the bearer token is the sole authorization. newID := mustIdentity(t) signPub := hex.EncodeToString(newID.SignPub) kexPub := hex.EncodeToString(newID.KexPub) joiner, err := client.New(h.natsURL, h.ctrlURL, newID) if err != nil { t.Fatalf("connect joiner: %v", err) } defer joiner.Close() if err := joiner.Register(inv.Token, signPub, kexPub); err != nil { t.Fatalf("joiner Register: %v", err) } // Admin now sees dora in the allowlist with the invite's handle/role. users, err := admin.ListUsers() if err != nil { t.Fatalf("admin ListUsers: %v", err) } row, ok := findUserInfo(users, signPub) if !ok { t.Fatalf("registered dora missing from allowlist: %+v", users) } if row.Handle != "dora" || row.Role != membership.RoleMember || row.Status != membership.StatusActive { t.Fatalf("dora row wrong: %+v", row) } // Single-use: redeeming again is an error. if err := joiner.Register(inv.Token, signPub, kexPub); err == nil { t.Fatalf("second Register should error (used token)") } // Admin hard-deletes dora; she vanishes from the allowlist entirely. if err := admin.DeleteUser(signPub); err != nil { t.Fatalf("admin DeleteUser: %v", err) } users, err = admin.ListUsers() if err != nil { t.Fatalf("admin ListUsers after delete: %v", err) } if _, ok := findUserInfo(users, signPub); ok { t.Fatalf("hard-deleted dora must NOT appear: %+v", users) } } func containsToken(invites []client.InviteInfo, token string) bool { for _, i := range invites { if i.Token == token { return true } } return false }