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 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

CommandDescription
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.

MethodEndpointDescription
POST/api/v1/testing/usersCreate a test user with immediate session
POST/api/v1/testing/sessionsCreate a new session for an existing test user
DELETE/api/v1/testing/usersDelete all @test.authgate.dev users in the project
DELETE/api/v1/testing/users/:idDelete 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'])

Was this page helpful?