mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Alex Yang <himself65@outlook.com>
344 lines
8.1 KiB
Plaintext
344 lines
8.1 KiB
Plaintext
---
|
|
title: Test Utils
|
|
description: Testing utilities for integration and E2E testing
|
|
---
|
|
|
|
The Test Utils plugin provides helpers for writing integration and E2E tests against Better Auth. It includes factories, database helpers, authentication helpers, and OTP capture functionality.
|
|
|
|
<Callout type="warn">
|
|
This plugin is designed for test environments only. Do not use it in production.
|
|
</Callout>
|
|
|
|
## Installation
|
|
|
|
<Steps>
|
|
<Step>
|
|
### Add the plugin to your auth config
|
|
|
|
```ts title="auth.ts"
|
|
import { betterAuth } from "better-auth"
|
|
import { testUtils } from "better-auth/plugins" // [!code highlight]
|
|
|
|
export const auth = betterAuth({
|
|
// ... other config options
|
|
plugins: [
|
|
testUtils() // [!code highlight]
|
|
]
|
|
})
|
|
```
|
|
</Step>
|
|
<Step>
|
|
### Access test helpers via context
|
|
|
|
```ts title="test-setup.ts"
|
|
const ctx = await auth.$context
|
|
const test = ctx.test
|
|
```
|
|
</Step>
|
|
</Steps>
|
|
|
|
## Usage
|
|
|
|
### Factories
|
|
|
|
Factories create objects without writing to the database. Use them to generate test data with sensible defaults.
|
|
|
|
#### createUser
|
|
|
|
Creates a user object with default values that can be overridden.
|
|
|
|
```ts
|
|
// Create user with defaults
|
|
const user = test.createUser()
|
|
// { id: "...", email: "user-xxx@example.com", name: "Test User", emailVerified: true, ... }
|
|
|
|
// Create user with overrides
|
|
const user = test.createUser({
|
|
email: "alice@example.com",
|
|
name: "Alice",
|
|
emailVerified: false
|
|
})
|
|
```
|
|
|
|
#### createOrganization
|
|
|
|
Creates an organization object. Only available when the organization plugin is installed.
|
|
|
|
```ts
|
|
const org = test.createOrganization({
|
|
name: "Acme Corp",
|
|
slug: "acme-corp"
|
|
})
|
|
```
|
|
|
|
### Database Helpers
|
|
|
|
Database helpers persist and remove test data from the database.
|
|
|
|
#### saveUser
|
|
|
|
Saves a user to the database.
|
|
|
|
```ts
|
|
const user = test.createUser({ email: "test@example.com" })
|
|
const savedUser = await test.saveUser(user)
|
|
```
|
|
|
|
#### deleteUser
|
|
|
|
Deletes a user from the database.
|
|
|
|
```ts
|
|
await test.deleteUser(user.id)
|
|
```
|
|
|
|
#### saveOrganization
|
|
|
|
Saves an organization to the database. Only available with the organization plugin.
|
|
|
|
```ts
|
|
const org = test.createOrganization({ name: "Test Org" })
|
|
const savedOrg = await test.saveOrganization(org)
|
|
```
|
|
|
|
#### deleteOrganization
|
|
|
|
Deletes an organization from the database. Only available with the organization plugin.
|
|
|
|
```ts
|
|
await test.deleteOrganization(org.id)
|
|
```
|
|
|
|
#### addMember
|
|
|
|
Adds a user as a member of an organization. Only available with the organization plugin.
|
|
|
|
```ts
|
|
const member = await test.addMember({
|
|
userId: user.id,
|
|
organizationId: org.id,
|
|
role: "admin"
|
|
})
|
|
```
|
|
|
|
### Auth Helpers
|
|
|
|
Auth helpers create authenticated sessions for testing protected routes.
|
|
|
|
#### login
|
|
|
|
Creates a session for a user and returns session details, headers, cookies, and token.
|
|
|
|
```ts
|
|
const { session, user, headers, cookies, token } = await test.login({
|
|
userId: user.id
|
|
})
|
|
|
|
// session - The session object with userId, token, etc.
|
|
// user - The user object
|
|
// headers - Headers object with session cookie (for fetch/Request)
|
|
// cookies - Cookie array (for Playwright/Puppeteer)
|
|
// token - The session token string
|
|
```
|
|
|
|
#### getAuthHeaders
|
|
|
|
Returns a `Headers` object with the session cookie set. Useful for making authenticated requests.
|
|
|
|
```ts
|
|
const headers = await test.getAuthHeaders({ userId: user.id })
|
|
|
|
// Use with auth API
|
|
const session = await auth.api.getSession({ headers })
|
|
|
|
// Use with fetch
|
|
const response = await fetch("/api/protected", { headers })
|
|
```
|
|
|
|
#### getCookies
|
|
|
|
Returns an array of cookie objects compatible with browser testing tools like Playwright and Puppeteer.
|
|
|
|
```ts
|
|
const cookies = await test.getCookies({
|
|
userId: user.id,
|
|
domain: "localhost" // optional, defaults to baseURL domain
|
|
})
|
|
|
|
// Playwright example
|
|
await context.addCookies(cookies)
|
|
|
|
// Puppeteer example
|
|
for (const cookie of cookies) {
|
|
await page.setCookie(cookie)
|
|
}
|
|
```
|
|
|
|
Each cookie object contains:
|
|
- `name` - Cookie name (e.g., "better-auth.session_token")
|
|
- `value` - Cookie value
|
|
- `domain` - Cookie domain
|
|
- `path` - Cookie path (defaults to "/")
|
|
- `httpOnly` - Whether cookie is HTTP-only
|
|
- `secure` - Whether cookie requires HTTPS
|
|
- `sameSite` - SameSite attribute ("Lax", "Strict", or "None")
|
|
|
|
### OTP Capture
|
|
|
|
When `captureOTP: true` is set, the plugin passively captures OTPs as they are created. This allows you to retrieve OTPs in tests without needing to mock email or SMS sending.
|
|
|
|
<Callout>
|
|
OTP capture is passive - it does not prevent OTPs from being sent via your configured `sendVerificationOTP` function. It simply stores a copy for test retrieval.
|
|
</Callout>
|
|
|
|
```ts title="auth.ts"
|
|
import { betterAuth } from "better-auth"
|
|
import { testUtils, emailOTP } from "better-auth/plugins"
|
|
|
|
export const auth = betterAuth({
|
|
plugins: [
|
|
testUtils({ captureOTP: true }), // [!code highlight]
|
|
emailOTP({
|
|
async sendVerificationOTP({ email, otp }) {
|
|
// Your email sending logic
|
|
}
|
|
})
|
|
]
|
|
})
|
|
```
|
|
|
|
#### getOTP
|
|
|
|
Retrieves a captured OTP by identifier (email or phone number).
|
|
|
|
```ts
|
|
// Send OTP
|
|
await auth.api.sendVerificationOTP({
|
|
body: { email: "user@example.com", type: "sign-in" }
|
|
})
|
|
|
|
// Retrieve captured OTP
|
|
const otp = test.getOTP("user@example.com")
|
|
// "123456"
|
|
```
|
|
|
|
## Options
|
|
|
|
| Option | Type | Default | Description |
|
|
|--------|------|---------|-------------|
|
|
| `captureOTP` | `boolean` | `false` | Enable OTP capture for testing verification flows |
|
|
|
|
## Examples
|
|
|
|
### Integration Test (Vitest)
|
|
|
|
```ts
|
|
import { describe, it, expect, beforeAll } from "vitest"
|
|
import { auth } from "./auth"
|
|
import type { TestHelpers } from "better-auth/plugins"
|
|
|
|
describe("protected route", () => {
|
|
let test: TestHelpers
|
|
|
|
beforeAll(async () => {
|
|
const ctx = await auth.$context
|
|
test = ctx.test
|
|
})
|
|
|
|
it("should return user data for authenticated request", async () => {
|
|
// Setup
|
|
const user = test.createUser({ email: "test@example.com" })
|
|
await test.saveUser(user)
|
|
|
|
// Get authenticated headers
|
|
const headers = await test.getAuthHeaders({ userId: user.id })
|
|
|
|
// Test authenticated request
|
|
const session = await auth.api.getSession({ headers })
|
|
expect(session?.user.id).toBe(user.id)
|
|
|
|
// Cleanup
|
|
await test.deleteUser(user.id)
|
|
})
|
|
})
|
|
```
|
|
|
|
### E2E Test (Playwright)
|
|
|
|
```ts
|
|
import { test, expect } from "@playwright/test"
|
|
import { auth } from "./auth"
|
|
|
|
test("dashboard shows user name", async ({ context, page }) => {
|
|
const ctx = await auth.$context
|
|
const testUtils = ctx.test
|
|
|
|
// Create and save user
|
|
const user = testUtils.createUser({
|
|
email: "e2e@example.com",
|
|
name: "E2E User"
|
|
})
|
|
await testUtils.saveUser(user)
|
|
|
|
// Get cookies and inject into browser
|
|
const cookies = await testUtils.getCookies({
|
|
userId: user.id,
|
|
domain: "localhost"
|
|
})
|
|
await context.addCookies(cookies)
|
|
|
|
// Navigate to protected page
|
|
await page.goto("/dashboard")
|
|
|
|
// Assert user name is visible
|
|
await expect(page.getByText("E2E User")).toBeVisible()
|
|
|
|
// Cleanup
|
|
await testUtils.deleteUser(user.id)
|
|
})
|
|
```
|
|
|
|
### OTP Verification Test
|
|
|
|
```ts
|
|
import { describe, it, expect, beforeAll, beforeEach } from "vitest"
|
|
import { auth } from "./auth"
|
|
import type { TestHelpers } from "better-auth/plugins"
|
|
|
|
describe("OTP verification", () => {
|
|
let test: TestHelpers
|
|
|
|
beforeAll(async () => {
|
|
const ctx = await auth.$context
|
|
test = ctx.test
|
|
})
|
|
|
|
beforeEach(() => {
|
|
test.clearOTPs()
|
|
})
|
|
|
|
it("should verify email with captured OTP", async () => {
|
|
const email = "otp-test@example.com"
|
|
const user = test.createUser({ email, emailVerified: false })
|
|
await test.saveUser(user)
|
|
|
|
// Request OTP
|
|
await auth.api.sendVerificationOTP({
|
|
body: { email, type: "email-verification" }
|
|
})
|
|
|
|
// Get captured OTP
|
|
const otp = test.getOTP(email)
|
|
expect(otp).toBeDefined()
|
|
|
|
// Verify email
|
|
await auth.api.verifyEmail({
|
|
body: { email, otp }
|
|
})
|
|
|
|
// Cleanup
|
|
await test.deleteUser(user.id)
|
|
})
|
|
})
|
|
```
|