Documentation
¶
Overview ¶
Package rigtest brings up rig's foundation for a test, and reads its tables back.
Every project on rig writes the same first two hundred lines before it can assert anything: a pool that refuses rather than skips when there is no database, the migration sets in the right order, a tenant of this test's own, and hand-written SQL against rig_account, rig_auth_log and rig_account_token. None of that is about the application. It is a schema rig owns and can change, so it is rig's to answer.
What this package deliberately does not do is start a container or build an object graph. The first is the runner's — `rig db up`, a compose file, a CI service — and a harness that shelled out to Docker would be a second opinion about where the database is. The second is the application's: which resources exist, what they are called, and every assertion stay yours. A harness that owned those would be a framework for writing tests rather than a way to start one.
rt := rigtest.New(t, rigtest.Config{
DatabaseURL: os.Getenv("DATABASE_URL"),
Migrations: api.MigrationSources(ownMigrations),
})
tenant := rt.Tenant(t)
accounts := rt.Accounts(t, tenant.ID)
It imports testing, the way a package of helpers has to. Import it from a test and never from a path a deployment takes.
Index ¶
- Constants
- type Account
- type Config
- type Identity
- type Link
- type LogEntry
- type LogQuery
- type Rig
- func (r *Rig) Account(tb testing.TB, tenantID, identityID uuid.UUID) *Account
- func (r *Rig) Accounts(tb testing.TB, tenantID uuid.UUID) []Account
- func (r *Rig) AgeSessions(tb testing.TB, identityID uuid.UUID, by time.Duration)
- func (r *Rig) AuthLog(tb testing.TB, q LogQuery) []LogEntry
- func (r *Rig) Events(tb testing.TB, q LogQuery) int
- func (r *Rig) ForgetAuthLog(tb testing.TB, event, emailAddress string)
- func (r *Rig) Identities(tb testing.TB) int
- func (r *Rig) Identity(tb testing.TB, emailAddress string) *Identity
- func (r *Rig) LastTenant(tb testing.TB, identityID uuid.UUID) uuid.UUID
- func (r *Rig) Links(tb testing.TB, identityID uuid.UUID) []Link
- func (r *Rig) Tenant(tb testing.TB, opts ...TenantOption) Tenant
- type Tenant
- type TenantOption
Constants ¶
const ( Succeeded = "Succeeded" Failed = "Failed" )
Outcome is how an event went.
The two spellings are named here because getting one wrong is not a failed assertion: rig_auth_outcome is a Postgres enum, so a query comparing against "Success" is an error at runtime from a database that will not say which of your queries it meant.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Account ¶
type Account struct {
ID uuid.UUID
TenantID uuid.UUID
// IdentityID is the person this account belongs to, and nil for a service
// account, which is nobody. A pointer because the column is nullable and the
// zero UUID is not the answer: rig_account's own CHECK is
// `(kind = 'Person') = (identity_id IS NOT NULL)`, so a test that could not
// tell the two apart could not assert on that constraint at all.
IdentityID *uuid.UUID
EmailAddress string
DisplayName string
Kind string
Role string
IsActive bool
CreatedAt time.Time
}
Account is one person's membership of one tenant.
type Config ¶
type Config struct {
// DatabaseURL is the database to run against. There is no default and no
// fallback: a hardcoded port is wrong the moment two checkouts run at once,
// and a harness that guessed would send one branch's migrations into
// another's schema.
//
// Leave it empty and set Pool instead when the suite already has one.
DatabaseURL string
// Pool is an existing pool to use rather than opening one, for a suite
// adopting this package a read at a time: the application's own fixtures go
// on using the pool they already had, and nothing has two pools against one
// database competing for its connections.
//
// It is not closed here, because it was not opened here.
Pool *pgxpool.Pool
// Migrations are every set to apply, in order — which is what the generated
// api.MigrationSources returns, rig's sets first and the project's last.
//
// They are the caller's rather than this package's on purpose. rig's
// foundation DDL travels with the module that owns it, an application has
// tables of its own, and a harness that carried a copy of either would be a
// second place for the schema to be.
Migrations []migrate.Source
// Isolate skips applying them, for a suite handed a database something else
// has already migrated. The schema is still checked: a database behind the
// binary refuses here rather than failing later as a missing column.
Isolate bool
}
Config is what a suite has to say.
type Identity ¶
type Identity struct {
ID uuid.UUID
EmailAddress string
DisplayName string
IsActive bool
EmailVerifiedAt *time.Time
CreatedAt time.Time
}
Identity is a person, across every tenant.
type Link ¶
type Link struct {
IdentityID uuid.UUID
Provider string
Subject string
EmailAddress string
CreatedAt time.Time
}
Link is a provider account somebody signs in with.
type LogEntry ¶
type LogEntry struct {
ID uuid.UUID
TenantID *uuid.UUID
CreatedAt time.Time
Event string
Outcome string
AccountID *uuid.UUID
EmailAddress string
Detail map[string]any
}
LogEntry is one row of rig_auth_log — both the audit trail and what rig's rate limits count, deliberately the same table.
That second job is worth knowing before a suite is built on it. A limit counts these rows over a rolling window, so a test database that outlives the run carries the previous run's budget: a suite signing in repeatedly as one address spends that address's hourly allowance and the next run is a wall of 429s saying nothing about the code under test. Delete the rows your own fixtures wrote, and only those.
func (LogEntry) Reason ¶
Reason is what the entry's detail says went wrong, when it says anything.
rig records the distinguishing fact here rather than in the response, because several refusals deliberately answer identically from outside — every way a code can be wrong is one error, and an address nobody has gets the same 204 as one somebody does. So this is where a test asks which of them actually happened.
type Rig ¶
type Rig struct {
// Pool is the connection pool, closed when the test ends. It is exported
// because an application's own fixtures need one and making a second would
// be two pools against one database.
Pool *pgxpool.Pool
}
Rig is a database with rig's foundation on it.
func New ¶
New connects, applies the migrations and answers with the result.
It fails rather than skips when there is no database, and that is the whole point of it. A suite that skips silently reports the same green as one that ran, so the day the database stops being started is the day the suite stops being a test and nothing says so.
func (*Rig) Account ¶
Account is one person's membership of one tenant, or nil when they have none.
Nil is the state an invitation leaves somebody in: rig writes the identity and a pending membership, and accepting is what creates the account. So this answering nil while Rig.Identity answers a row is "invited and not yet arrived" rather than a mistake.
func (*Rig) AgeSessions ¶
AgeSessions moves every one of somebody's sessions back in time.
It is here because the alternative is sleeping. "Where somebody was last" is decided by which session root is newest, so a test that signs in twice to prove the order needs the first sign-in to be older than the second — and two sign-ins in the same millisecond are not reliably ordered by a timestamp. Ageing the first is deterministic; a sleep is a slower way to be flaky.
func (*Rig) ForgetAuthLog ¶
ForgetAuthLog deletes one address's entries for one event, and nothing else.
This is the escape from the budget LogEntry describes, and it is narrow on purpose. Everything else in that table is somebody's assertion or somebody else's limit, so a suite clears the rows its own repeated sign-ins wrote — by address and by event — rather than truncating and wondering later why an unrelated test went green.
func (*Rig) Identities ¶
Identities counts every person this installation has, which is the assertion an identity gate is actually about: a refused stranger leaves no row.
func (*Rig) Identity ¶
Identity resolves an address to the person who has it, or nil.
The address is global rather than per tenant, which is the fact worth knowing before asserting on anything else here: one address is one person, and the same person holds an account in each tenant they belong to.
func (*Rig) LastTenant ¶
LastTenant is where somebody would land if they signed in now, or uuid.Nil.
There is no column for this and it is the single read most likely to be written wrongly by hand: it is the tenant of the newest session *root* — `root_token_id = id` — because a root is one per sign-in and every refresh after it shares the root's family. Counting refreshes instead would make "where somebody was last" mean "where they were busiest".
func (*Rig) Links ¶
Links lists the providers one person has connected.
A link is not scoped to a tenant, because a provider account belongs to a person. Two links on one identity is somebody who signed in with Google and later with GitHub, which is the case the address lookup exists to make work.
func (*Rig) Tenant ¶
func (r *Rig) Tenant(tb testing.TB, opts ...TenantOption) Tenant
Tenant makes one, and it is the isolation model to build a suite on.
rig scopes every generated query by tenant, so a test that leaks into another one's rows has found a bug rather than tripped over its own fixture. That only holds if each test has a tenant nobody else is in — which is why this exists rather than a shared fixture seeded once.
Two things about the row are easy to get wrong and neither fails in a way that names itself. rig_tenant.slug is NOT NULL with a unique index on lower(slug), so a fixture that leaves it out is refused. And a slug built from the first characters of a UUIDv7 collides: those are the high bits of a millisecond timestamp, so every v7 minted inside about a minute shares them, and two parallel tests are a unique-constraint violation rather than a flake. The suffix here is from crypto/rand for that reason.
Options are for the fields a test is actually about; the zero value is a tenant with no domain restrictions, which is the right default for a fixture that is not about domains.
type TenantOption ¶
type TenantOption func(*Tenant, *tenantSettings)
TenantOption changes something about the tenant a test is given.
func AllowedEmailDomains ¶
func AllowedEmailDomains(domains ...string) TenantOption
AllowedEmailDomains restricts who may hold an account in this tenant, which is the column rig checks when somebody is provisioned, invited, or joins a tenant a provider sign-in named.
It is a different question from whether somebody may become an identity at all — that one is the auth.AllowIdentity gate, and it is not per tenant.
func Named ¶
func Named(name string) TenantOption
Named gives the tenant a name and a slug derived from it.
The slug keeps a random suffix even so: a name is what a test is asserting about, and two tests naming their tenant the same thing is ordinary rather than a mistake to refuse.