storetest

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package storetest is a conformance suite for the store interfaces.

authit's whole design rests on a host implementing store's ports against its own database, and nothing in the library can check that the implementation is right: an adapter that returns a nil pointer instead of store.ErrNotFound, or that filters revoked rows out of a Get, compiles perfectly and fails at runtime — sometimes as a security bug rather than an outage. This package is where those expectations are written down executably.

Point it at your adapter from your own test file:

func TestMyStores(t *testing.T) {
	storetest.RunAll(t, storetest.Stores{
		Users: func(t *testing.T) store.UserStore { return newUserStore(t) },
		// ...one factory per port you implement
	})
}

Each factory is called once per subtest and must return an empty store; use t.Cleanup for teardown. Leave a field nil to skip that port.

What it does not check

Concurrency, transactional behaviour, and performance are out of scope — it runs single-goroutine against one store at a time. Nor does it dictate column names, types, or how a []string is stored. It checks behaviour authit's service packages actually rely on, and nothing else.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InTx

func InTx(ctx context.Context) bool

InTx reports whether ctx is the one a TxProbe handed to its callback.

This is the same mechanism a real TxRunner uses — stash a handle in the context, have store methods look for it — with the handle replaced by a marker. A store double can call it to record which operations a service enrolled in the transaction, which is the only externally observable thing about the seam.

func RunAccountStore

func RunAccountStore(t *testing.T, newStore func(*testing.T) store.AccountStore, fx Fixtures)

RunAccountStore checks store.AccountStore.

func RunAll

func RunAll(t *testing.T, s Stores)

RunAll runs every suite for which a factory was supplied.

func RunBoundDeviceStore added in v0.7.0

func RunBoundDeviceStore(t *testing.T, newStore func(*testing.T) store.BoundDeviceStore, fx Fixtures)

RunBoundDeviceStore checks store.BoundDeviceStore.

func RunDeviceAuthorizationStore

func RunDeviceAuthorizationStore(t *testing.T, newStore func(*testing.T) store.DeviceAuthorizationStore, fx Fixtures)

RunDeviceAuthorizationStore checks store.DeviceAuthorizationStore.

func RunEmailLoginStore

func RunEmailLoginStore(t *testing.T, newStore func(*testing.T) store.EmailLoginStore, fx Fixtures)

RunEmailLoginStore checks store.EmailLoginStore.

func RunEmailVerificationStore

func RunEmailVerificationStore(t *testing.T, newStore func(*testing.T) store.EmailVerificationStore, fx Fixtures)

RunEmailVerificationStore checks store.EmailVerificationStore.

func RunInvitationStore

func RunInvitationStore(t *testing.T, newStore func(*testing.T) store.InvitationStore, fx Fixtures)

RunInvitationStore checks store.InvitationStore.

func RunLockoutStore

func RunLockoutStore(t *testing.T, newStore func(*testing.T) store.LockoutStore, fx Fixtures)

RunLockoutStore checks store.LockoutStore.

This is the port with the footgun: it needs two tables, and the second has no authit type at all, so an adapter that implements only the attempts half compiles cleanly and fails at runtime.

func RunMemberStore

func RunMemberStore(t *testing.T, newStore func(*testing.T) store.MemberStore, fx Fixtures)

RunMemberStore checks store.MemberStore.

func RunPINStore added in v0.7.0

func RunPINStore(t *testing.T, newStore func(*testing.T) store.PINStore, fx Fixtures)

RunPINStore checks store.PINStore.

func RunPasswordResetStore

func RunPasswordResetStore(t *testing.T, newStore func(*testing.T) store.PasswordResetStore, fx Fixtures)

RunPasswordResetStore checks store.PasswordResetStore.

func RunPendingTwoFactorStore

func RunPendingTwoFactorStore(t *testing.T, newStore func(*testing.T) store.PendingTwoFactorStore, fx Fixtures)

RunPendingTwoFactorStore checks store.PendingTwoFactorStore.

func RunPersonalAccessTokenStore

func RunPersonalAccessTokenStore(t *testing.T, newStore func(*testing.T) store.PersonalAccessTokenStore, fx Fixtures)

RunPersonalAccessTokenStore checks store.PersonalAccessTokenStore.

func RunRefreshTokenStore

func RunRefreshTokenStore(t *testing.T, newStore func(*testing.T) store.RefreshTokenStore, fx Fixtures)

RunRefreshTokenStore checks store.RefreshTokenStore.

func RunSuperuserRefreshTokenStore

func RunSuperuserRefreshTokenStore(t *testing.T, newStore func(*testing.T) store.SuperuserRefreshTokenStore, fx Fixtures)

RunSuperuserRefreshTokenStore checks store.SuperuserRefreshTokenStore.

func RunSuperuserStore

func RunSuperuserStore(t *testing.T, newStore func(*testing.T) store.SuperuserStore, fx Fixtures)

RunSuperuserStore checks store.SuperuserStore.

func RunSuperuserTwoFactorStore

func RunSuperuserTwoFactorStore(t *testing.T, newStore func(*testing.T) store.SuperuserTwoFactorStore, fx Fixtures)

RunSuperuserTwoFactorStore checks store.SuperuserTwoFactorStore.

func RunTOTPStore

func RunTOTPStore(t *testing.T, newStore func(*testing.T) store.TOTPStore, fx Fixtures)

RunTOTPStore checks store.TOTPStore.

func RunTeamStore

func RunTeamStore(t *testing.T, newStore func(*testing.T) store.TeamStore, fx Fixtures)

RunTeamStore checks store.TeamStore.

func RunUserStore

func RunUserStore(t *testing.T, newStore func(*testing.T) store.UserStore, fx Fixtures)

RunUserStore checks store.UserStore.

func RunWebAuthnChallengeStore

func RunWebAuthnChallengeStore(t *testing.T, newStore func(*testing.T) store.WebAuthnChallengeStore, fx Fixtures)

RunWebAuthnChallengeStore checks store.WebAuthnChallengeStore.

The suite is short because the port is, and one case carries most of the weight: consuming a challenge must be atomic. Everything else here would pass against a map with a Get and a Delete bolted together, and that implementation is exactly the one that lets a captured assertion be replayed.

func RunWebAuthnCredentialStore

func RunWebAuthnCredentialStore(t *testing.T, newStore func(*testing.T) store.WebAuthnCredentialStore, fx Fixtures)

RunWebAuthnCredentialStore checks store.WebAuthnCredentialStore.

Types

type Fixtures

type Fixtures struct {
	EnsureUser      func(t *testing.T, userID string)
	EnsureTeam      func(t *testing.T, teamID string)
	EnsureSuperuser func(t *testing.T, superuserID string)
	// EnsureMember creates member memberID in team teamID, for a schema
	// whose invitations reference the inviting member.
	EnsureMember func(t *testing.T, memberID, teamID string)
}

Fixtures lets an adapter satisfy constraints its own schema imposes but the ports say nothing about.

Each suite exercises one port in isolation, so it creates rows that refer to users, teams and operators that do not exist. Against memstore that is fine. Against a real schema with foreign keys -- authit's own schema.sql included -- those inserts are rejected, and the suite would be reporting a constraint the interfaces never promised.

Supply the hooks you need; each is called before dependent rows are created, and may be called more than once with the same id, so make them idempotent. Leave them nil when nothing is required.

type Stores

type Stores struct {
	// Fixtures is consulted before rows with foreign keys are created.
	Fixtures Fixtures

	Users              func(*testing.T) store.UserStore
	RefreshTokens      func(*testing.T) store.RefreshTokenStore
	PasswordResets     func(*testing.T) store.PasswordResetStore
	EmailVerifications func(*testing.T) store.EmailVerificationStore
	TOTP               func(*testing.T) store.TOTPStore
	PendingTwoFactor   func(*testing.T) store.PendingTwoFactorStore
	Lockouts           func(*testing.T) store.LockoutStore
	Teams              func(*testing.T) store.TeamStore
	Members            func(*testing.T) store.MemberStore
	Invitations        func(*testing.T) store.InvitationStore
	PATs               func(*testing.T) store.PersonalAccessTokenStore
	Devices            func(*testing.T) store.DeviceAuthorizationStore
	Superusers         func(*testing.T) store.SuperuserStore
	SuperuserTokens    func(*testing.T) store.SuperuserRefreshTokenStore
	SuperuserTwoFactor func(*testing.T) store.SuperuserTwoFactorStore
	Accounts           func(*testing.T) store.AccountStore
	WebAuthn           func(*testing.T) store.WebAuthnCredentialStore
	EmailLogins        func(*testing.T) store.EmailLoginStore
	WebAuthnChallenges func(*testing.T) store.WebAuthnChallengeStore
	PINs               func(*testing.T) store.PINStore
	BoundDevices       func(*testing.T) store.BoundDeviceStore
}

Stores collects one factory per port. Nil fields are skipped.

type TxProbe

type TxProbe struct {

	// Calls counts how many times RunInTx was entered.
	Calls int
	// Fail, when non-nil, is returned instead of running fn — standing in
	// for a transaction that could not begin.
	Fail error
	// contains filtered or unexported fields
}

TxProbe is a store.TxRunner for tests. It records how a service used the seam and can be made to fail.

It provides no atomicity whatsoever: nothing is rolled back, because there is nothing to roll back an in-memory map with. It is not a substitute for testing a real implementation against a real database. It answers a different and still useful question — did the service put the right writes inside the transaction, and keep the wrong ones out.

func (*TxProbe) CallCount

func (p *TxProbe) CallCount() int

CallCount returns Calls under the probe's lock.

func (*TxProbe) RunInTx

func (p *TxProbe) RunInTx(ctx context.Context, fn func(context.Context) error) error

type TxWitness

type TxWitness struct {
	// contains filtered or unexported fields
}

TxWitness records, per operation name, whether it was called inside a transaction. Store doubles report into it; a test then asserts which operations were enrolled.

func NewTxWitness

func NewTxWitness() *TxWitness

NewTxWitness returns an empty witness.

func (*TxWitness) AssertInTx

func (w *TxWitness) AssertInTx(t *testing.T, op string)

AssertInTx fails unless op was called at least once and every call was inside a transaction.

func (*TxWitness) AssertOutsideTx

func (w *TxWitness) AssertOutsideTx(t *testing.T, op string)

AssertOutsideTx fails unless op was called at least once and every call was outside a transaction.

This is the direction that catches the subtler mistake. A write that must survive the surrounding call returning an error — recording a failed login, revoking a token family after detecting reuse — is undone by a rollback if it is swept into the transaction with everything else.

func (*TxWitness) Record

func (w *TxWitness) Record(ctx context.Context, op string)

Record notes one call of op, and whether it saw a transaction context.

func (*TxWitness) Reset

func (w *TxWitness) Reset()

Reset forgets every recorded call.

Most store operations are used both inside and outside a transaction -- CreateRefreshToken is part of an atomic rotation in Refresh and a lone write in Authenticate, and both are correct. Call Reset immediately before the operation under test so the assertions describe that operation rather than everything the fixture did to reach it.

Jump to

Keyboard shortcuts

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