> ## Documentation Index
> Fetch the complete documentation index at: https://ribaunt-e66481b6-mintlify-4e7c3afc.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Learn how to test your Ribaunt CAPTCHA integration using solveChallenge for end-to-end flows and bypassing the widget in unit tests.

Ribaunt provides `solveChallenge()` — a synchronous, server-side solver — that makes it easy to write automated tests for your challenge and verify endpoints without a browser. Rather than standing up a headless browser or mocking internal state, you can drive the full challenge → solve → verify cycle directly from your test suite.

## Using solveChallenge in tests

`solveChallenge` runs the same proof-of-work algorithm the browser executes, but synchronously and in Node.js. You can generate a challenge on your server, solve it as a client would, and then feed the result into `verifySolution` — all within a single test.

```ts theme={null}
import { createChallenge, solveChallenge, verifySolution } from 'ribaunt';

// Generate a challenge (as your server would)
const tokens = await createChallenge({ difficulty: 3, amount: 2, ttlSeconds: 30 });

// Solve it (as the browser would)
const solutions = solveChallenge(tokens);

// Verify the solution (as your verify endpoint would)
const result = await verifySolution(tokens, solutions);
console.assert(result.valid === true);
```

<Tip>
  Use low difficulty (3–4) in tests to keep them fast. Higher difficulty makes CPU-bound tests slow. Difficulty 5 is fine for production but will noticeably slow down test suites.
</Tip>

## Testing with timeout guardrails

When you need a safety net against a runaway solver in CI, pass guardrail options to `solveChallenge`. You can cap the wall-clock time, the number of hash attempts, or both.

```ts theme={null}
const solution = solveChallenge(token, {
  maxDurationMs: 2000,    // Give up after 2 seconds
  maxIterations: 500_000, // Or after 500k attempts
});
```

If either guardrail is reached, `solveChallenge` returns `undefined` for that token. Your test should assert the return value is defined before passing it to `verifySolution`.

## Testing Argon2id challenges

`solveChallenge` supports SHA-256 tokens only and returns `undefined` for tokens created with `algorithm: 'argon2id'`. Use the asynchronous [`solveChallengeAsync`](/api/solve-challenge#solvechallengeasync) in tests that cover Argon2id. It accepts the same guardrail options and solves both algorithms.

```ts theme={null}
import { createChallenge, solveChallengeAsync, verifySolution } from 'ribaunt';

const tokens = await createChallenge({
  algorithm: 'argon2id',
  difficulty: 1,
  amount: 2,
  ttlSeconds: 30,
});

const solutions = await solveChallengeAsync(tokens);
const result = await verifySolution(tokens, solutions);
console.assert(result.valid === true);
```

<Tip>
  Each Argon2id hash takes milliseconds instead of microseconds. Keep `difficulty` at `1` and amounts small so Argon2id tests stay fast.
</Tip>

## Testing replay protection

Ribaunt's default `local` replay mode blocks any token from being verified more than once within the same process. You can confirm this behavior directly in your tests by submitting the same solution twice.

```ts theme={null}
const tokens = await createChallenge({ difficulty: 3, amount: 2, ttlSeconds: 30 });
const solutions = solveChallenge(tokens);

const first = await verifySolution(tokens, solutions);
console.assert(first.valid === true);  // Passes

const second = await verifySolution(tokens, solutions);
console.assert(second.valid === false); // Rejected — replay detected
```

The second call returns `false` because the challenge JTIs are consumed on first use.

## Testing with disabled replay protection

Some unit tests need to reuse the same tokens across multiple assertions — for example, testing that a valid solution always passes your business logic regardless of replay state. You can disable replay protection for a single `verifySolution` call.

```ts theme={null}
const result = await verifySolution(tokens, solutions, {
  replayPrevention: 'disabled',
});
```

<Note>
  Only use `replayPrevention: 'disabled'` in tests. Never disable replay protection in your production verify endpoint, as it allows attackers to reuse intercepted solutions.
</Note>

## Capturing verification warnings

When you want to assert *why* a verification failed — not just that it returned `false` — use the `onWarning` callback. This is especially useful for testing edge cases like expired tokens or invalid solutions.

```ts theme={null}
const warnings: string[] = [];

const result = await verifySolution(tokens, badSolutions, {
  onWarning: (w) => warnings.push(w.reason),
});

// warnings will contain 'invalid-solution' etc.
```

The `reason` field on the warning object can be one of the following values:

| Reason                     | When it fires                                                                  |
| -------------------------- | ------------------------------------------------------------------------------ |
| `invalid-token`            | The JWT is malformed, tampered with, or uses an unknown secret                 |
| `expired-token`            | The challenge's TTL has elapsed before the solution was submitted              |
| `invalid-solution`         | The submitted nonce does not produce a hash with the required leading zeros    |
| `context-mismatch`         | The `context` option does not match the context the challenge was created with |
| `replay-detected`          | The same token JTI has already been consumed by the replay store               |
| `replay-store-unavailable` | The replay store threw during consumption (for example, Redis is unreachable)  |
| `configuration-error`      | A required option (e.g. `replayStore`) is missing or misconfigured             |

<Warning>
  `solveChallenge` is synchronous and CPU-intensive. Don't use it in production request handlers — it's designed for tests and tooling only.
</Warning>
