Help us shape the future of Go! Take the 2026 Go Developer Survey

rigtest

package module
v0.6.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
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.

func (Identity) Verified

func (i Identity) Verified() bool

Verified reports whether the address has been confirmed, which is what auth.Config.RequireVerifiedEmail gates a sign-in on.

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

func (e LogEntry) Reason() string

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 LogQuery

type LogQuery struct {
	Event        string
	Outcome      string
	EmailAddress string
	TenantID     *uuid.UUID
}

LogQuery narrows the trail. A zero value is everything.

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

func New(tb testing.TB, cfg Config) *Rig

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

func (r *Rig) Account(tb testing.TB, tenantID, identityID uuid.UUID) *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) Accounts

func (r *Rig) Accounts(tb testing.TB, tenantID uuid.UUID) []Account

Accounts lists a tenant's people, newest last.

func (*Rig) AgeSessions

func (r *Rig) AgeSessions(tb testing.TB, identityID uuid.UUID, by time.Duration)

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) AuthLog

func (r *Rig) AuthLog(tb testing.TB, q LogQuery) []LogEntry

AuthLog reads the trail, oldest first.

func (*Rig) Events

func (r *Rig) Events(tb testing.TB, q LogQuery) int

Events counts matching entries, for the assertion that is only about how many.

func (*Rig) ForgetAuthLog

func (r *Rig) ForgetAuthLog(tb testing.TB, event, emailAddress string)

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

func (r *Rig) Identities(tb testing.TB) int

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

func (r *Rig) Identity(tb testing.TB, emailAddress string) *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

func (r *Rig) LastTenant(tb testing.TB, identityID uuid.UUID) uuid.UUID

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 (r *Rig) Links(tb testing.TB, identityID uuid.UUID) []Link

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 Tenant

type Tenant struct {
	ID   uuid.UUID
	Name string
	Slug string
}

Tenant is a workspace made for one test.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL