Testing
The @auth-gate/testing package lets you write E2E tests with real authenticated sessions. It creates real users, gets real tokens, and injects real encrypted cookies — your app under test doesn't know it's a test.
Installation
npm install -D @auth-gate/testing
Core API
The core client wraps the AuthGate Testing API endpoints. Use it directly or through the framework-specific helpers.
import { AuthGateTest } from '@auth-gate/testing'
const testing = new AuthGateTest({
apiKey: process.env.AUTHGATE_API_KEY!,
baseUrl: process.env.AUTHGATE_URL!,
})
// Create a test user with an immediate session
const user = await testing.createUser({
email: 'alice@test.authgate.dev',
password: 'test-password-123',
name: 'Alice Test',
})
// Returns: { id, email, name, token, refreshToken, expiresAt }
// Create a new session for an existing test user
const session = await testing.createSession(user.id)
// Cleanup all test users in the project
await testing.cleanup()
// Delete a specific test user
await testing.deleteUser(user.id)
Test user emails must end with @test.authgate.dev. The API rejects any other domain. This prevents test endpoints from being used to create real users.
Test email convention
Any email ending with @test.authgate.dev gets special treatment across all AuthGate auth flows:
- No emails are sent — signup verification, magic links, and password resets skip delivery entirely
- Fixed OTP code — email verification codes are always
424242 - Immediate verification — test users created via the testing API are verified by default
This means you can also use test emails through the regular auth flows (signup, signin, magic link) without needing real email infrastructure in your test environment.
Playwright
Global setup
global.setup.ts
import { authgateSetup } from '@auth-gate/testing/playwright'
import { test as setup } from '@playwright/test'
setup('authgate setup', async ({}) => {
await authgateSetup({
apiKey: process.env.AUTHGATE_API_KEY!,
baseUrl: process.env.AUTHGATE_URL!,
cleanupOnStart: true, // delete leftover test users from previous runs
})
})
global.teardown.ts
import { authgateTeardown } from '@auth-gate/testing/playwright'
import { test as teardown } from '@playwright/test'
teardown('authgate teardown', async ({}) => {
await authgateTeardown() // cleanup + remove state file
})
Register both in your Playwright config:
playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
globalSetup: './global.setup.ts',
globalTeardown: './global.teardown.ts',
// ...
})
Writing tests
tests/dashboard.spec.ts
import { createTestUser, injectSession, cleanup } from '@auth-gate/testing/playwright'
import { test, expect } from '@playwright/test'
test('authenticated user sees dashboard', async ({ page, context }) => {
const user = await createTestUser({
email: 'bob@test.authgate.dev',
password: 'pass123',
})
// Injects real encrypted session cookies into the browser
await injectSession({ context, user })
await page.goto('/dashboard')
await expect(page.getByText('Welcome')).toBeVisible()
})
test.afterEach(async () => {
await cleanup()
})
injectSession encrypts the user into the same AES-256-GCM session cookie your SDK uses in production, then sets it on the browser context via context.addCookies(). No mocking involved.
Cypress
Plugin setup
cypress.config.ts
import { authgateSetup } from '@auth-gate/testing/cypress'
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
return authgateSetup({
on,
config,
apiKey: process.env.AUTHGATE_API_KEY!,
baseUrl: process.env.AUTHGATE_URL!,
cleanupOnStart: true,
})
},
},
})
Register commands
cypress/support/e2e.ts
import { registerAuthGateCommands } from '@auth-gate/testing/cypress'
registerAuthGateCommands()
TypeScript support
Add the type reference to your tsconfig.json:
tsconfig.json
{
"compilerOptions": {
"types": ["cypress", "@auth-gate/testing/cypress"]
}
}
Writing tests
cypress/e2e/dashboard.cy.ts
describe('Dashboard', () => {
it('shows dashboard for authenticated user', () => {
// Creates user + injects session cookies in one step
cy.authgateSignIn({
email: 'carol@test.authgate.dev',
password: 'pass123',
})
cy.visit('/dashboard')
cy.contains('Welcome')
})
afterEach(() => {
cy.authgateCleanup()
})
})
Available commands
| Command | Description |
|---|---|
cy.authgateCreateUser(options) | Create a test user (returns user object) |
cy.authgateSignIn(options) | Create user + inject session cookies |
cy.authgateCleanup() | Delete all test users in the project |
cy.authgateDeleteUser(userId) | Delete a specific test user |
Testing API endpoints
The testing package calls these endpoints on your AuthGate instance. They require a valid project API key.
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/testing/users | Create a test user with immediate session |
POST | /api/v1/testing/sessions | Create a new session for an existing test user |
DELETE | /api/v1/testing/users | Delete all @test.authgate.dev users in the project |
DELETE | /api/v1/testing/users/:id | Delete a specific test user |
Billing test utilities
The @auth-gate/testing/billing subpath provides an in-memory billing harness for unit-testing subscription logic, entitlement checks, and usage-based billing without hitting the AuthGate API.
npm install -D @auth-gate/testing @auth-gate/billing
Quick start
import { createTestBilling } from '@auth-gate/testing/billing'
import { billing } from './authgate.billing' // your defineBilling() config
const t = createTestBilling({ billing })
// Subscribe a user to a plan
t.subscribe('user_1', 'pro')
// Check entitlements
const check = t.checkEntitlement('user_1', 'api_calls')
// { type: "metered", allowed: true, limit: 100000, used: 0, remaining: 100000 }
// Report usage
t.reportUsage('user_1', 'api_calls', 500)
// Advance simulated time (triggers billing period rollover)
t.advanceTime({ months: 1 })
// Plan changes
t.changePlan('user_1', 'free')
// Cancel
t.cancel('user_1')
// Subscriber counts across plans
t.subscriberCounts() // { free: 1, pro: 0 }
Test clock
The test clock lets you simulate time progression with billing-period awareness:
t.clock.now // current simulated date
t.advanceTime({ days: 15 }) // advance by 15 days
t.advanceTime({ months: 1 }) // advance to next billing period boundary
Entitlement assertions
// Boolean feature
const check = t.checkEntitlement('user_1', 'analytics')
// { type: "boolean", allowed: true }
// Metered feature
const check = t.checkEntitlement('user_1', 'api_calls')
// { type: "metered", allowed: true, limit: 100000, used: 500, remaining: 99500 }
Snapshot testing with diffs
Re-exports computeDiff from @auth-gate/billing for diffing server state against local config:
import { computeDiff } from '@auth-gate/testing/billing'
const diff = computeDiff(serverState, localConfig)
expect(diff.planOps).toMatchSnapshot()
RBAC test utilities
The @auth-gate/testing/rbac subpath provides test builders, an in-memory permission checker, and assertion helpers for unit-testing RBAC configurations.
npm install -D @auth-gate/testing @auth-gate/rbac
Config builders
import { createTestRbacConfig, createTestResource, createTestRole } from '@auth-gate/testing/rbac'
// Create a minimal RBAC config with sensible defaults
const config = createTestRbacConfig()
// Create a custom resource
const docs = createTestResource('documents', ['read', 'write', 'delete'])
// Create a custom role with grants
const editor = createTestRole('editor', 'Editor', {
documents: { read: true, write: true },
})
In-memory permission checker
import { defineRbac } from '@auth-gate/rbac'
import { RbacChecker } from '@auth-gate/testing/rbac'
const rbac = defineRbac({ /* your config */ })
const checker = new RbacChecker(rbac)
checker.hasPermission('admin', 'documents:delete') // true
checker.getPermissions('viewer') // Set { "documents:read" }
checker.getRolesWithPermission('documents:delete') // ["admin"]
The checker resolves role inheritance automatically and guards against circular references.
Assertion helpers
import { expectPermission, expectNoPermission, expectRolePermissions } from '@auth-gate/testing/rbac'
// Assert a role has a specific permission
expectPermission(config, 'admin', 'documents:write')
// Assert a role does NOT have a permission
expectNoPermission(config, 'viewer', 'documents:delete')
// Assert a role has exactly these permissions (no more, no less)
expectRolePermissions(config, 'viewer', ['documents:read'])