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 ¶
- func InTx(ctx context.Context) bool
- func RunAccountStore(t *testing.T, newStore func(*testing.T) store.AccountStore, fx Fixtures)
- func RunAll(t *testing.T, s Stores)
- func RunBoundDeviceStore(t *testing.T, newStore func(*testing.T) store.BoundDeviceStore, fx Fixtures)
- func RunDeviceAuthorizationStore(t *testing.T, newStore func(*testing.T) store.DeviceAuthorizationStore, ...)
- func RunEmailLoginStore(t *testing.T, newStore func(*testing.T) store.EmailLoginStore, fx Fixtures)
- func RunEmailVerificationStore(t *testing.T, newStore func(*testing.T) store.EmailVerificationStore, ...)
- func RunInvitationStore(t *testing.T, newStore func(*testing.T) store.InvitationStore, fx Fixtures)
- func RunLockoutStore(t *testing.T, newStore func(*testing.T) store.LockoutStore, fx Fixtures)
- func RunMemberStore(t *testing.T, newStore func(*testing.T) store.MemberStore, fx Fixtures)
- func RunPINStore(t *testing.T, newStore func(*testing.T) store.PINStore, fx Fixtures)
- func RunPasswordResetStore(t *testing.T, newStore func(*testing.T) store.PasswordResetStore, fx Fixtures)
- func RunPendingTwoFactorStore(t *testing.T, newStore func(*testing.T) store.PendingTwoFactorStore, ...)
- func RunPersonalAccessTokenStore(t *testing.T, newStore func(*testing.T) store.PersonalAccessTokenStore, ...)
- func RunRefreshTokenStore(t *testing.T, newStore func(*testing.T) store.RefreshTokenStore, fx Fixtures)
- func RunSuperuserRefreshTokenStore(t *testing.T, newStore func(*testing.T) store.SuperuserRefreshTokenStore, ...)
- func RunSuperuserStore(t *testing.T, newStore func(*testing.T) store.SuperuserStore, fx Fixtures)
- func RunSuperuserTwoFactorStore(t *testing.T, newStore func(*testing.T) store.SuperuserTwoFactorStore, ...)
- func RunTOTPStore(t *testing.T, newStore func(*testing.T) store.TOTPStore, fx Fixtures)
- func RunTeamStore(t *testing.T, newStore func(*testing.T) store.TeamStore, fx Fixtures)
- func RunUserStore(t *testing.T, newStore func(*testing.T) store.UserStore, fx Fixtures)
- func RunWebAuthnChallengeStore(t *testing.T, newStore func(*testing.T) store.WebAuthnChallengeStore, ...)
- func RunWebAuthnCredentialStore(t *testing.T, newStore func(*testing.T) store.WebAuthnCredentialStore, ...)
- type Fixtures
- type Stores
- type TxProbe
- type TxWitness
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func InTx ¶
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 ¶
RunAccountStore checks store.AccountStore.
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 ¶
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 ¶
RunInvitationStore checks store.InvitationStore.
func RunLockoutStore ¶
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 ¶
RunMemberStore checks store.MemberStore.
func RunPINStore ¶ added in v0.7.0
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 ¶
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 ¶
RunTOTPStore checks store.TOTPStore.
func RunTeamStore ¶
RunTeamStore checks store.TeamStore.
func RunUserStore ¶
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.
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 (*TxWitness) AssertInTx ¶
AssertInTx fails unless op was called at least once and every call was inside a transaction.
func (*TxWitness) AssertOutsideTx ¶
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) 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.