Testing
WIPAn in-memory engine for tests, capturing what would be mailed, a redis double, and driving your routes
An engine for tests
import { createTest } from '@gentleduck/auth/test'
import { expect, it } from 'vitest'
it('signs in with the right password', async () => {
const auth = createTest()
const ada = await auth.identities.create({ profile: { username: 'ada', email: 'ada@example.com' } })
await auth.passwords.set(ada.id, 'correct horse battery staple', auth.cfg.stores.credentials)
const { sid, session } = await auth.flows.signIn({
providerId: 'password',
input: { email: 'ada@example.com', password: 'correct horse battery staple' },
})
expect(session?.identityId).toBe(ada.id)
const resolved = await auth.resolveSession({ headers: new Headers({ authorization: `Bearer ${sid}` }) }).orNull()
expect(resolved?.identity?.profile.email).toBe('ada@example.com')
})
createTest() is an engine over a MemoryAdapter, the bearer transport, a memory limiter allowing 1000 attempts a minute, and the password, MFA and API key providers. Each piece can be replaced:
import { MemoryLimiter } from '@gentleduck/auth/limiters/memory'
const auth = createTest({ limiter: new MemoryLimiter({ max: 2, windowMs: 60_000 }) })
// The third wrong password in a minute throws AUTH_RATE_LIMITED.
| Override | Replaces |
|---|---|
adapter, stores | The memory adapter, or the whole store bag. |
transport, limiter, events | Those engine pieces. |
providers | Added after the three built in. |
passwords, mfa, apiKeys | Those providers' options. |
noopLimiter() from @gentleduck/auth/limiters/mock never refuses, for a test that runs the same flow many times.
Capturing what would be sent
import { MemoryAdapter } from '@gentleduck/auth/adapters/memory'
import { createAuth } from '@gentleduck/auth/core'
import { authTestDeliver } from '@gentleduck/auth/test'
const { deliver, outbox } = authTestDeliver()
const auth = createAuth({ baseUrl: 'https://app.example.com', stores: new MemoryAdapter(), deliver, strict: false })
const ada = await auth.identities.create({ profile: { username: 'ada', email: 'ada@example.com' } })
await auth.flows.requestEmailVerification({ identityId: ada.id })
expect(outbox[0]).toMatchObject({ kind: 'email-verification', identityId: ada.id })
const token = new URL(String(outbox[0]?.vars.url)).searchParams.get('token')
outbox holds every message as deliver would have received it, with identityId and tenantId added.
Redis
fakeRedis() from @gentleduck/auth/test is an in-memory client for the adapters/redis stores:
import { session } from '@gentleduck/auth/adapters/redis'
import { fakeRedis } from '@gentleduck/auth/test'
const sessions = session({ redis: fakeRedis() })
Your routes
A route over a Fetch-native framework takes a Request, so a test calls it with no server. With a Hono app:
const res = await app.request('/auth/signin', {
body: JSON.stringify({ providerId: 'password', input: { email: 'a@x.com', password: 'correct-horse-battery' } }),
headers: { 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' },
method: 'POST',
})
expect(res.headers.get('set-cookie')).toContain('duck-sid=')
Send sec-fetch-site: cross-site to see the CSRF guard answer 403 AUTH_CSRF.