Testing

Local scenarios

Test direct Nimiq auth and cross-device flows without the real host app.

Use @onmax/better-auth-nimiq-pay-e2e local scenarios when you need deterministic Nimiq Pay Better Auth flow tests without a real mini-app host, injected provider, or network calls.

Do not use local scenarios as the only signal for host integration. They prove your app wiring and auth flow shape, not the real window.nimiq bridge.

Start with a profile

Use defineE2EProfile() to keep local, CI, and bridge runs on the same configuration surface.

test/e2e/profile.ts
import { defineE2EProfile } from '@onmax/better-auth-nimiq-pay-e2e'

export const profile = defineE2EProfile({
  env: process.env,
  playgroundUrl: 'http://127.0.0.1:3000',
  endpointPrefix: '/nimiq',
  qrEndpointPrefix: '/cross-device',
})

Default profile values:

profile
network: testnet
mode: local
endpointPrefix: /nimiq
qrEndpointPrefix: /mobile-qr
playgroundUrl: http://127.0.0.1:3000
nuxtAuthBaseUrl: http://127.0.0.1:3000/api/auth

Mainnet E2E runs are blocked unless NIMIQ_PAY_E2E_ALLOW_MAINNET=true, and mainnet runs are disabled in CI.

Test direct sign-in locally

Use runSignInScenario() for the full nonce -> sign -> verify loop.

test/e2e/local-auth.e2e.test.ts
import { describe, expect, it } from 'vitest'
import { runSignInScenario } from '@onmax/better-auth-nimiq-pay-e2e'
import { profile } from './profile'

describe('Nimiq Pay auth', () => {
  it('signs in with a deterministic local provider', async () => {
    const result = await runSignInScenario({
      profile,
      origin: 'https://example.test',
      requestAddressBeforeVerify: false,
    })

    expect(result).toMatchObject({
      ok: true,
      network: 'testnet',
      mode: 'local',
      token: 'local-token-1',
    })
  })
})

Expected behavior:

output
No remote fetch is used in local mode.
The token is local-token-1 for the first successful verification.

Test signature failures

Use a custom provider when you need to prove that invalid signatures return useful failure details.

test/e2e/invalid-signature.e2e.test.ts
import { expect, it } from 'vitest'
import { isNimiqProviderError } from '@onmax/better-auth-nimiq/provider'
import {
  createStubNimiqProvider,
  runSignInScenario,
} from '@onmax/better-auth-nimiq-pay-e2e'

it('reports invalid signatures', async () => {
  const stub = createStubNimiqProvider()
  const invalidProvider = {
    listAccounts: () => stub.listAccounts(),
    async sign(message: string) {
      const result = await stub.sign(message)
      if (isNimiqProviderError(result))
        return result

      const suffix = result.signature.endsWith('0') ? '1' : '0'
      return {
        publicKey: result.publicKey,
        signature: `${result.signature.slice(0, -1)}${suffix}`,
      }
    },
  }

  const result = await runSignInScenario({
    provider: invalidProvider as any,
    origin: 'https://example.test',
    requestAddressBeforeVerify: false,
  })

  expect(result.ok).toBe(false)
  expect(result.error).toContain('Invalid signature')
})

Test cross-device sign-in locally

Use runQrSignInScenario() when desktop and phone flows need to complete in one test process.

test/e2e/qr-local-auth.e2e.test.ts
import { describe, expect, it } from 'vitest'
import { runQrSignInScenario } from '@onmax/better-auth-nimiq-pay-e2e'

describe('QR sign-in', () => {
  it('starts, approves, and finalizes locally', async () => {
    const result = await runQrSignInScenario()

    expect(result).toMatchObject({
      ok: true,
      mode: 'local',
      token: 'local-finalized-order-1',
    })
  })
})

Expected behavior:

output
order-1 is created
the stub provider approves the challenge
desktop finalize returns local-finalized-order-1

Replace HTTP calls with local fetchers

Use local fetchers when you want to test lower-level client calls without the scenario runner.

test/e2e/local-fetcher.test.ts
import { expect, it } from 'vitest'
import { isNimiqProviderError } from '@onmax/better-auth-nimiq/provider'
import {
  createLocalAuthFetcher,
  createStubNimiqProvider,
} from '@onmax/better-auth-nimiq-pay-e2e'

it('issues and verifies a local nonce', async () => {
  const fetcher = createLocalAuthFetcher({
    appName: 'Test App',
    origin: 'https://example.test',
  })
  const provider = createStubNimiqProvider()

  const nonce = await fetcher<{
    nonceId: string
    message: string
  }>('/nimiq/nonce', { method: 'POST', body: {} })

  const signed = await provider.sign(nonce.message)
  if (isNimiqProviderError(signed))
    throw new Error(signed.error.message)

  const verified = await fetcher<{ ok: true, token: string }>('/nimiq/verify', {
    method: 'POST',
    body: {
      nonceId: nonce.nonceId,
      publicKeyHex: signed.publicKey,
      signatureHex: signed.signature,
    },
  })

  expect(verified).toEqual({
    ok: true,
    token: 'local-token-1',
  })
})

Use local scenarios in CI first. Add bridge mode only for the smaller set of tests that must prove injected-provider behavior.

Copyright © 2026