Agora

Testing

Test auth flows in your host app without booting an IdP — mint real signed ID tokens with a local JWKS, fake ctx.auth, fake the account store, and build valid identities.

@adonis-agora/authkit-testing gives host apps the helpers they need to test authentication end-to-end without standing up a real Identity Provider. It can mint real RS256-signed ID tokens validated by an in-process JWKS, fake the ctx.auth authenticator for controller tests, fake the account store, and build valid Identity objects. It carries no runtime dependency on the server or client packages — the surfaces it fakes are reproduced structurally — so it stays a lightweight dev dependency.

Install

pnpm add -D @adonis-agora/authkit-testing

Everything is exported from the package root:

import {
  createTestIdentity,
  mintTestIdToken,
  generateTestKeyPair,
  serveJwks,
  jwksFromKey,
  testJwks,
  fakeAuthenticator,
  fakeAccountStore,
} from '@adonis-agora/authkit-testing'

createTestIdentity(overrides?)

Returns a fully-populated Identity with sane defaults (userId, email, empty globalRoles, a profile, a sessionId, and one-hour issuedAt/expiresAt timestamps). Override any field:

const admin = createTestIdentity({
  userId: 'user-42',
  email: 'jane@acme.dev',
  globalRoles: ['ADMIN'],
})

Use it anywhere you need a valid identity without going through a real token exchange — most often to feed fakeAuthenticator below.

fakeAuthenticator(options?)

Builds an object that satisfies the client's Authenticator surface — the whole of what ctx.auth exposes — so controller tests can inject a logged-in (or anonymous) user without resolving real tokens.

OptionTypeDefaultDescription
identityIdentity | nullcreateTestIdentity()The resolved identity. Pass null to simulate an anonymous request.
userunknownthe identityThe domain user returned by getUser().

Everything the fake does follows from one piece of state — whether an identity is present. That single switch decides all eight members of the surface, which is what makes an anonymous request a one-word change in a test. The returned object is typed as FakeAuthenticatorLike:

MemberBehaviour with an identityBehaviour when identity: null
getIdentity()resolves the Identityresolves null
identity (getter)the Identitynull
authenticate()resolves the Identitythrows
check()truefalse
hasGlobalRole(role)true when the identity carries the rolefalse
getUser()resolves the user option (the identity by default)resolves null
getUserOrFail()resolves the userthrows
toSharedProps(){ user, globalRoles }null

Two of those deserve their own note, because they are the fail-closed and the frontend-facing halves of the surface:

  • getUserOrFail() is getUser() without the null branch: it throws when there is no session and when there is a session but no user behind it (user: null or user: undefined). It is what routes behind an auth middleware call, where a null user means a misconfigured route rather than a visitor. Test both branches by flipping identity to null and by passing user: null with an identity present.
  • toSharedProps() builds the object you hand to the frontend — it matches the AuthSharedProps shape that @adonis-agora/authkit-react consumes, so an Inertia share helper can be tested against it directly. It returns null for an anonymous request, which is the signal the frontend reads as "no session"; with a session it returns { user, globalRoles }, where globalRoles comes from the identity and defaults to an empty array.
const anonymous = fakeAuthenticator({ identity: null })

await anonymous.check()          // false
await anonymous.toSharedProps()  // null
await anonymous.getUserOrFail()  // throws

A worked controller test:

import { test } from '@japa/runner'
import { fakeAuthenticator, createTestIdentity } from '@adonis-agora/authkit-testing'
import { ShowProfileController } from '#controllers/show_profile_controller'

test('returns the current user profile', async ({ assert }) => {
  const auth = fakeAuthenticator({
    identity: createTestIdentity({ email: 'jane@acme.dev', globalRoles: ['ADMIN'] }),
  })

  const ctx = { auth } as any // your HttpContext test double
  const result = await new ShowProfileController().handle(ctx)

  assert.equal(result.email, 'jane@acme.dev')
  assert.isTrue(auth.hasGlobalRole('ADMIN'))
})

AuthKit only authenticates — role-based authorization lives in @adonis-agora/authz. The fake exposes hasGlobalRole (roles carried on the token) but nothing more; test policy decisions with the authz package's own helpers.

fakeAccountStore(options?)

A capability-aware fake AccountStore. The core methods (findById, verifyCredentials, findByEmail, create, provider-identity linking, password-reset/email-verification tokens, listAccounts, setGlobalRoles) are always present. MFA, passkeys, and account-security capabilities are opt-in so you can exercise the server's supportsMfa / supportsPasskeys / supportsAccountSecurity type guards.

OptionTypeDefaultDescription
accountFakeAuthAccount{ id: 'u1', email: 'a@b.com', globalRoles: ['ADMIN'] }Fixed account returned by findById/findByEmail/verifyCredentials.
withMfabooleanfalseAdds getMfaState/enableMfa/disableMfa (supportsMfa → true).
withPasskeysbooleanfalseAdds listPasskeys/registerPasskey/deletePasskey (supportsPasskeys → true).
withAccountSecuritybooleanfalseAdds changePassword/requestEmailChange/confirmEmailChange.
overridesRecord<string, unknown>{}Replace any individual method.
import { fakeAccountStore } from '@adonis-agora/authkit-testing'

const store = fakeAccountStore({
  account: { id: 'u1', email: 'jane@acme.dev', globalRoles: ['ADMIN'] },
  withMfa: true,
  overrides: {
    // e.g. force the "not found" branch for one test
    findById: async () => null,
  },
})

The capability flags exist to flip the supports* guards, which probe a single representative method each — they are not full implementations of the capability interfaces. If the code under test calls further methods of a capability (startTotpEnrollment, verifyPasskeyRegistration, …), supply them through overrides.

Minting real ID tokens

For resolver-level tests you want a real signed JWT and a JWKS that validates it — not a mock. mintTestIdToken produces both.

mintTestIdToken(options)

Mints an RS256-signed JWT and returns { token, key, jwks }.

OptionTypeDefaultDescription
issuerstringiss claim — must match the resolver's issuer.
clientIdstringaud claim — must match the resolver's clientId/audience.
claimsRecord<string, unknown>{ sub, email }Extra claims / overrides (e.g. sub, roles).
keyTestKeyPairfreshly generatedReuse a keypair across tokens.
expiresInSecondsnumber3600Token lifetime.

serveJwks(jwks)

The JwtResolver uses createRemoteJWKSet, so it needs a URL. serveJwks boots a throwaway in-process HTTP server that serves your JWKS at GET /.well-known/jwks.json and returns { jwksUri, server, close } — no external network, no IdP.

import { test } from '@japa/runner'
import { mintTestIdToken, serveJwks } from '@adonis-agora/authkit-testing'
import { resolvers } from '@adonis-agora/authkit-client'

test.group('jwt resolver', (group) => {
  test('resolves a valid token into an identity', async ({ assert, cleanup }) => {
    // 1. Mint a real signed token + the public JWKS that validates it.
    const { token, jwks } = await mintTestIdToken({
      issuer: 'https://idp.test',
      clientId: 'my-app',
      claims: { sub: 'user-42', email: 'jane@test.dev', roles: ['ADMIN'] },
    })

    // 2. Serve the JWKS locally so the resolver can fetch it.
    const served = await serveJwks(jwks)
    cleanup(() => served.close())

    // 3. Build the resolver against that jwks_uri. Read the token from the
    //    Authorization header so we can drive it with a plain ctx double.
    const factory = resolvers.jwt({ jwksUri: served.jwksUri, tokenSource: 'bearer' })
    const resolver = await factory.resolver({
      issuer: 'https://idp.test',
      clientId: 'my-app',
      sessionKey: 'authkit',
      globalRolesClaim: 'roles',
    })

    // 4. Resolve through an HttpContext double carrying the bearer token.
    const ctx = {
      request: { header: () => `Bearer ${token}` },
    } as any
    const identity = await resolver.resolve(ctx)

    assert.equal(identity?.userId, 'user-42')
    assert.deepEqual(identity?.globalRoles, ['ADMIN'])
  })
})

resolvers.jwt(...).resolver(ctx) returns a SessionResolver whose resolve(httpCtx) pulls the raw token from the request (session idToken by default, or the Authorization: Bearer header with tokenSource: 'bearer'), verifies it against the served JWKS, and builds the Identity.

Always close() the served JWKS in your test teardown (cleanup(...) / group.teardown(...)). Each serveJwks call binds a fresh ephemeral port; leaving them open leaks sockets across the suite.

Reusing a keypair

Generate one keypair and sign many tokens with it, or pull the raw public JWKS without minting a token:

import { generateTestKeyPair, mintTestIdToken, jwksFromKey } from '@adonis-agora/authkit-testing'

const key = await generateTestKeyPair() // stable kid across tokens

const alice = await mintTestIdToken({ issuer, clientId, key, claims: { sub: 'alice' } })
const bob = await mintTestIdToken({ issuer, clientId, key, claims: { sub: 'bob' } })

const jwks = await jwksFromKey(key) // public JWKS that validates both

jwksFromKey(key) is an alias of testJwks(key); both resolve to { keys: JWK[] }, ready to feed serveJwks or a local createLocalJWKSet. generateTestKeyPair() also takes an optional kid — pass one when a test asserts on the key id, otherwise a random one is generated for you.

API summary

ExportKindPurpose
createTestIdentityfunctionBuild a valid Identity with overrides.
fakeAuthenticatorfunctionFake ctx.auth for controller tests.
fakeAccountStorefunctionCapability-aware fake AccountStore.
mintTestIdTokenfunctionReal RS256 JWT + validating JWKS.
generateTestKeyPairfunctionExtractable RS256 keypair with a stable kid.
serveJwksfunctionIn-process JWKS HTTP endpoint (jwksUri, close).
jwksFromKey / testJwksfunctionPublic JWKS object from a keypair.

The package also exports the types behind those helpers, so a test file can annotate its own fixtures without redeclaring them: FakeAuthenticatorLike and FakeAuthenticatorOptions, FakeAuthAccount and FakeAccountStoreOptions, TestKeyPair, MintTestIdTokenOptions, MintedToken, and ServedJwks.

On this page