Files
zeekayandHanzo Dev 8d7457cdf0 Lux ID is the only way in
Base v1.5.72 closed the door this depended on, and closed it deliberately:
"IAM IS THE ONLY AUTH. There is no second way in... _superusers is not an
exception: it reaches schema, settings, backups and logs, and one process
serves many orgs' Bases, so two independently-keyed doors to it would be
one more than can be reasoned about."

The sandbox login was the second door. It kept a bcrypt hash in its own
collection and minted a _superusers token, and until that release Base
honoured such a token as a fallback. After it, the route still answered
200 with a token and the very next request answered 401 — a login that
succeeds and authenticates nothing. Every authed route was refusing, and
the suite could not see it: the test harness never registers the org
plugin, so 141 tests passed green against a binary whose demo login was
broken.

It was also the custom auth this estate does not build. Gone: the route,
the credentials collection, the token minting, the demo password that was
a constant in a file every browser downloads, and the merged hook shape
that let one path stand in for the other. The SPA already had the PKCE
flow it needed.

What this costs: [email protected] must exist in Lux IAM under org lux, app
lux-bank. The demo cannot sign in until it does. That is an IAM seeding
task, not a code one, and it is the same identity convention every other
surface already follows.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-26 15:16:53 -07:00

140 lines
4.6 KiB
Go

package collections
import (
"testing"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/tests"
)
// every collection this package defines, with the name it must answer to.
var ensures = []struct {
name string
ensure func(core.App) error
coll string
}{
{"account", EnsureAccountCollection, AccountCollectionName},
{"balance", EnsureBalanceCollection, BalanceCollectionName},
{"beneficiary", EnsureBeneficiaryCollection, BeneficiaryCollectionName},
{"transaction", EnsureTransactionCollection, TransactionCollectionName},
{"fee", EnsureFeeCollection, FeeCollectionName},
{"card", EnsureCardCollection, CardCollectionName},
{"wallet", EnsureWalletCollection, WalletCollectionName},
{"conversion", EnsureConversionCollection, ConversionCollectionName},
{"audit", EnsureAuditCollection, AuditCollectionName},
{"session", EnsureSessionCollection, SessionCollectionName},
{"document", EnsureDocumentCollection, DocumentCollectionName},
{"position", EnsurePositionCollection, PositionCollectionName},
}
func newApp(t *testing.T) *tests.TestApp {
t.Helper()
app, err := tests.NewTestApp()
if err != nil {
t.Fatalf("new test app: %v", err)
}
t.Cleanup(app.Cleanup)
return app
}
// Every Ensure runs on EVERY boot, against a database that may already hold the
// collection from a previous version, so each has to do two jobs: create it when
// absent, and change nothing when it is already there. A second call that errors
// takes the daemon down on restart; one that duplicates a field or an index
// corrupts the schema it exists to migrate.
//
// They are run in ORDER and in one app because that is how the daemon runs them
// and because they depend on each other: nearly every collection holds a
// relation to accounts, and creating it first is not optional — out of order,
// the relation field has no collection to point at and the rule referencing
// account.owner will not compile.
func TestEnsureCreatesThenIsIdempotent(t *testing.T) {
app := newApp(t)
for _, e := range ensures {
if _, err := app.FindCollectionByNameOrId(e.coll); err == nil {
t.Fatalf("%s exists before its Ensure ran", e.coll)
}
if err := e.ensure(app); err != nil {
t.Fatalf("%s: first Ensure: %v", e.name, err)
}
}
type shape struct{ fields, indexes int }
was := map[string]shape{}
for _, e := range ensures {
c, err := app.FindCollectionByNameOrId(e.coll)
if err != nil {
t.Fatalf("%s absent after Ensure: %v", e.coll, err)
}
if len(c.Fields) == 0 {
t.Fatalf("%s was created with no fields", e.coll)
}
was[e.coll] = shape{len(c.Fields), len(c.Indexes)}
}
// Two more boots against the schema that is already there.
for boot := 2; boot <= 3; boot++ {
for _, e := range ensures {
if err := e.ensure(app); err != nil {
t.Fatalf("%s: boot %d: %v", e.name, boot, err)
}
}
}
for _, e := range ensures {
c, err := app.FindCollectionByNameOrId(e.coll)
if err != nil {
t.Fatalf("%s vanished across restarts: %v", e.coll, err)
}
if got, want := len(c.Fields), was[e.coll].fields; got != want {
t.Errorf("%s field count moved %d -> %d across restarts", e.coll, want, got)
}
if got, want := len(c.Indexes), was[e.coll].indexes; got != want {
t.Errorf("%s index count moved %d -> %d across restarts", e.coll, want, got)
}
}
}
// The account collection carries the one field a customer's money hangs off:
// chainIndex is the derivation path their address and signing key come from, and
// the partial unique index is what stops two accounts reaching one key.
func TestAccountCollectionGuardsTheDerivationIndex(t *testing.T) {
app := newApp(t)
if err := EnsureAccountCollection(app); err != nil {
t.Fatalf("EnsureAccountCollection: %v", err)
}
c, err := app.FindCollectionByNameOrId(AccountCollectionName)
if err != nil {
t.Fatal(err)
}
f := c.Fields.GetByName("chainIndex")
if f == nil {
t.Fatal("accounts has no chainIndex field")
}
if !hasIndex(c.Indexes, chainIndexUnique()) {
t.Fatalf("accounts is missing the partial unique index on chainIndex: %v", c.Indexes)
}
// Partial on purpose: unassigned accounts all hold 0, and a plain unique
// index would let only one of them exist.
if got := chainIndexUnique(); got == "" {
t.Fatal("chainIndexUnique is empty")
}
}
func TestHasIndex(t *testing.T) {
idx := []string{"CREATE INDEX a ON t (x)", "CREATE UNIQUE INDEX b ON t (y)"}
if !hasIndex(idx, "CREATE UNIQUE INDEX b ON t (y)") {
t.Error("hasIndex missed an exact match")
}
if hasIndex(idx, "CREATE INDEX b ON t (y)") {
t.Error("hasIndex matched a different statement")
}
if hasIndex(nil, "anything") {
t.Error("hasIndex found something in nil")
}
if hasIndex(idx, "") {
t.Error("hasIndex matched the empty string")
}
}