Documentation
¶
Overview ¶
Package authtest is the test harness for applications that embed the library: an Auth on a fresh database per test, a clock the test moves, a mailbox and an SMS box that record what the plugins send, a signed-in session as a cookie, a bearer header or a Go context, and a browser with a cookie jar over the app's handler.
It exists because every app test otherwise writes the same eighty lines: open SQLite with the right pragmas, migrate, record mail, wait for deliveries, mark the Secure cookie for a plain test server, pull the token out of a link. Each of those was written in this repository's own examples several times before it was made public here.
The package is part of the core module and imports no database driver: the test imports its own (`_ "modernc.org/sqlite"`) and New opens the database by driver name, so the module's dependency rule holds.
Index ¶
- Constants
- Variables
- func DSNConnector(d driver.Driver, dsn string) driver.Connector
- func SweepFaults[E any](t SweepT, fresh func() (E, *Faults), ...)
- type Browser
- func (b *Browser) Client() *http.Client
- func (b *Browser) Cookie(name string) *http.Cookie
- func (b *Browser) Do(req *http.Request) *Response
- func (b *Browser) Get(path string) *Response
- func (b *Browser) Post(path string, body any) *Response
- func (b *Browser) SetSession(s *Session)
- func (b *Browser) URL() string
- type Clock
- type Env
- func (e *Env) Anonymous(ctx context.Context) context.Context
- func (e *Env) Browser(h http.Handler) *Browser
- func (e *Env) CreateUser(name string, ids ...Identity) auth.User
- func (e *Env) DeleteUser(id string)
- func (e *Env) NewUser(name string, ids ...Identity) UserFixture
- func (e *Env) SaveUser(f UserFixture) auth.User
- func (e *Env) SignIn(userID string) *Session
- type Faults
- type Identity
- type Mailbox
- func (m *Mailbox) Last(addr string) (email.Mail, bool)
- func (m *Mailbox) LastCode(addr string) string
- func (m *Mailbox) LastLink(addr string) *url.URL
- func (m *Mailbox) LastToken(addr string) auth.Token
- func (m *Mailbox) Reset()
- func (m *Mailbox) Send(_ context.Context, mail email.Mail) error
- func (m *Mailbox) Sent() []email.Mail
- func (m *Mailbox) To(addr string) []email.Mail
- type Option
- type Response
- type SMSBox
- type Session
- func (s *Session) Apply(req *http.Request)
- func (s *Session) ApplyBearer(req *http.Request)
- func (s *Session) Authorization() string
- func (s *Session) BearerContext(ctx context.Context) context.Context
- func (s *Session) Context(ctx context.Context) context.Context
- func (s *Session) Cookie() *http.Cookie
- type SweepT
- type UserFixture
Constants ¶
const BaseURL = "https://app.example.test"
BaseURL is the public URL every Env runs under unless WithBaseURL says otherwise: an https origin, so the __Host- cookie rules hold, on a name that resolves nowhere.
const DefaultUserName = "Test User"
DefaultUserName is the name NewUser gives a fixture that names none, as better-auth's test utils default theirs.
const MaxFaultSteps = 200
MaxFaultSteps bounds a SweepFaults run. No operation in the library makes this many database calls; reaching it means the sweep never saw the operation finish without a fault, which is a loop in the test, not a slow operation.
const SQLiteDriver = "sqlite"
SQLiteDriver is the database/sql driver name New opens by default: the one modernc.org/sqlite registers. The test must import that driver, or New fails with sql's "unknown driver".
Variables ¶
var ErrFault = errors.New("authtest: injected database fault")
ErrFault is the error an injected database failure returns. A test compares against it to prove the failure reached the caller rather than being eaten.
Functions ¶
func DSNConnector ¶
DSNConnector is a driver.Connector that opens dsn with d, for a driver that offers only Open (modernc.org/sqlite, for one). The driver of an open *sql.DB is db.Driver(), so a test needs no import beyond the one that registers the driver.
func SweepFaults ¶
func SweepFaults[E any](t SweepT, fresh func() (E, *Faults), setup func(e E) func(ctx context.Context) error, swallowOK ...int)
SweepFaults proves that an operation surfaces every database failure it can meet. It runs the operation once per database call it makes, each time on a fresh state from fresh with a different call failing, until a run finishes without reaching the armed call.
fresh builds a new database and returns it with its Faults (for an Env: New with WithFaults); setup prepares the state the operation needs, with faults off, and returns the operation. Every faulted run must return an error: a nil error after a fault means a failure was swallowed, and the operation reported success for work that did not happen. swallowOK names the steps where swallowing is the documented behaviour (a best-effort "last used" update, say), so an intentional one is written down at the test rather than silently allowed.
Types ¶
type Browser ¶
type Browser struct {
// Origin is what the browser sends as the Origin header on a mutation;
// change it to play a cross-site page.
Origin string
// contains filtered or unexported fields
}
Browser drives an http.Handler the way a browser on the app's page does: over TLS, because the session cookie is __Host- and Secure and a cookie jar keeps such a cookie only for an https URL; with a cookie jar, so a sign-in's cookie rides on the next request; and sending the app's Origin on every mutation, so the origin check passes. Redirects are not followed, so a test sees the 302 an OAuth start answers.
func (*Browser) Client ¶
Client returns the underlying client, with the jar, for a request the helpers do not shape.
func (*Browser) Post ¶
Post sends body as JSON to path with the browser's Origin. body is encoded with encoding/json; pass a map for a request that carries a password, since password.Secret (an auth.Token) encodes as "[redacted]" on purpose. A request type whose token is an auth.WireToken (a link or poll token) encodes it, so it can be passed as it is. A nil body sends "{}".
func (*Browser) SetSession ¶
SetSession puts a session in the jar, as if the browser had signed in through the handler; the next request carries its cookie.
type Clock ¶
type Clock struct {
// contains filtered or unexported fields
}
Clock is a settable clock for auth.WithClock. Instants are rounded to the microsecond because Postgres and MySQL store no finer, and a test that compares a stored time with Now would otherwise differ in the nanoseconds.
func NewClock ¶
func NewClock() *Clock
NewClock returns a clock stopped at the current time, in UTC.
type Env ¶
type Env struct {
T testing.TB
// Auth is wired, migrated and closed when the test ends.
Auth *auth.Auth
// DB and Dialect are what Auth runs on.
DB *sql.DB
Dialect sqlb.Dialect
// Clock is the clock Auth measures every expiry by; Advance it to expire
// sessions, codes and links without sleeping.
Clock *Clock
// Mail records every mail the email plugin sends when the plugin is built
// with email.WithMailer(env.Mail); SMS does the same for the phone plugin
// with phone.WithSMS(env.SMS).
Mail *Mailbox
SMS *SMSBox
// BaseURL is the app's public URL as Auth knows it.
BaseURL string
// Faults fails a chosen database call when the Env was built with
// WithFaults, and is nil otherwise.
Faults *Faults
}
Env is one test's wired library and the fakes around it.
func New ¶
New builds the Env: it opens the database (SQLite in memory unless WithDB or WithSQLite), wires the plugins and options over a frozen clock and a memory rate store on that clock, migrates, and registers Close with the test's Cleanup. Any failure fails the test at once; a harness that returned errors would make every test start with four checks.
func (*Env) Anonymous ¶
Anonymous returns ctx as a signed-out browser request from the app's own origin: what a public operation (sign-in, forgot password) sees.
func (*Env) Browser ¶
Browser serves h over TLS for the rest of the test and returns a Browser on it. One Env can have several, for two users signed in at once.
func (*Env) CreateUser ¶
CreateUser creates a user with the given verified, primary identities, through the real core API, so every plugin hook runs. It is the fixture most tests start with; a test of sign-up itself goes through the plugin's operation instead.
func (*Env) DeleteUser ¶
DeleteUser deletes user id through the core (a.DeleteUser), so its identities, sessions and every plugin's rows go with it, as better-auth's deleteUser does. An unknown id fails the test.
func (*Env) NewUser ¶
func (e *Env) NewUser(name string, ids ...Identity) UserFixture
NewUser builds a UserFixture without touching the database: an id from the Auth's generator (auth.WithIDs), DefaultUserName unless the test passes a name, the Env's clock as the creation time, and the given verified, primary identities. Nothing is checked until SaveUser, which is where every rule (uniqueness, hooks) runs.
func (*Env) SaveUser ¶
func (e *Env) SaveUser(f UserFixture) auth.User
SaveUser writes f through the real core API (CreateUser, then each identity), so every plugin hook runs, and returns the stored user. A taken id or identity fails the test.
func (*Env) SignIn ¶
SignIn creates a session for userID through the core, the way a sign-in plugin does after it checked a credential, so a user's second factor still applies: a user who must pass one gets a pending session. For a test that needs the whole sign-in flow, call the plugin's operation.
type Faults ¶
type Faults struct {
// contains filtered or unexported fields
}
Faults counts database calls and fails the one numbered at: armed, the at-th call of any kind (exec, query, prepare, begin, commit) returns ErrFault; disarmed, every call passes. It is the switch behind the connector Connector returns.
Why a driver and not a mock: the library talks to *sql.DB through sqlb, and the only honest way to reach a "the database failed here" branch is to make a real database fail at that exact call. Counting calls instead of matching SQL text keeps a test independent of the queries' wording.
type Identity ¶
type Identity struct {
Kind auth.IdentifierKindName
Value string
}
Identity is one identifier a test user is created with: a verified, primary identity of Kind holding Value, such as {email.KindEmail, "ada@example.test"}.
type Mailbox ¶
type Mailbox struct {
// contains filtered or unexported fields
}
Mailbox records mail instead of sending it. It is an email.Mailer; build the email plugin with email.WithMailer(env.Mail). Every reader waits for the deliveries in flight first, because a message goes out after the transaction commits, in the background, and a test that read the box right after the request would race it.
func (*Mailbox) LastCode ¶
LastCode returns the one-time code in the newest mail to addr, for the email plugin's codes. It fails the test when no mail carries one.
func (*Mailbox) LastLink ¶
LastLink returns the first URL in the newest mail to addr that carries a "token" query parameter: the verify, reset, sign-in or confirmation link. It fails the test when there is none.
func (*Mailbox) Reset ¶
func (m *Mailbox) Reset()
Reset forgets every recorded mail, so a test can assert on what one step sends.
type Option ¶
type Option func(*settings)
Option configures New.
func WithDB ¶
WithDB runs the Env on an open database instead of SQLite in memory: a Postgres or MySQL the test suite reset itself. The Env does not close it.
func WithFaults ¶
WithFaults runs the Env's SQLite through f (Faults.Connector): every database call the Env makes, migrations included, passes f's counter, which stays disarmed until the test arms it. Env.Faults is f. It applies to the SQLite New opens (WithSQLite included), not to WithDB.
func WithOptions ¶
WithOptions adds options for auth.New after the harness's own (database, base URL, clock, rate store), so a test can add a session cache, a policy or a hook. An option that repeats one of the harness's replaces it.
func WithPlugins ¶
WithPlugins sets the plugin list. It is a function of the Env because the plugins take the recorders (env.Mail, env.SMS) and the Env exists first. New fails without it: a library with no sign-in method cannot start.
func WithSQLite ¶
WithSQLite opens SQLite through another driver or DSN, for a test that uses mattn/go-sqlite3 (whose pragma syntax differs) or wants a file in t.TempDir(). The dialect is SQLite's.
type Response ¶
type Response struct {
Method string
Path string
Status int
Header http.Header
Body []byte
// contains filtered or unexported fields
}
Response is one answer, read in full.
func (*Response) Decode ¶
Decode unmarshals the JSON body into v, failing the test when it does not parse. v is any because the answer's type is the test's to choose; this is the boundary where encoded bytes meet typed Go.
type SMSBox ¶
type SMSBox struct {
// contains filtered or unexported fields
}
SMSBox records text messages instead of sending them. It is a phone.SMSSender; build the phone plugin with phone.WithSMS(env.SMS).
func (*SMSBox) LastCode ¶
LastCode returns the one-time code in the newest text to number. It fails the test when there is none.
type Session ¶
type Session struct {
// Session is the row as the core created it.
Session auth.Session
// Token is the raw session token.
Token auth.Token
// contains filtered or unexported fields
}
Session is a signed-in session for a test, in every form a request can carry it: a cookie, a bearer header, or a Go context. It comes from Env.SignIn; the raw token is in Token for a test that builds its own request.
func (*Session) Apply ¶
Apply adds the session cookie to req, and the Origin a page of the app sends, so a mutation passes the origin check.
func (*Session) ApplyBearer ¶
ApplyBearer adds the session as an Authorization header, the way an app without cookies sends it; no Origin is needed for a bearer credential.
func (*Session) Authorization ¶
Authorization returns the header value a bearer client sends.
func (*Session) BearerContext ¶
BearerContext is Context with the token presented as a bearer credential, which the origin check leaves alone and the session policy treats as an app.
type SweepT ¶
type SweepT interface {
Helper()
Errorf(format string, args ...any)
Fatalf(format string, args ...any)
Logf(format string, args ...any)
}
SweepT is what SweepFaults needs of a test: *testing.T satisfies it, and so does a recorder in the harness's own tests.
type UserFixture ¶
type UserFixture struct {
ID string
Name string
Image string
CreatedAt time.Time
Identities []Identity
}
UserFixture is a user built in memory and not yet saved, as better-auth's ctx.test.createUser returns one: the test reads or edits its fields, then SaveUser writes it through the real core API. The ID is already final, so a test can refer to the user (in another fixture, an expected value) before it exists.