# Testing without a wallet

> Unit-test wallet-authenticated routes with throwaway keys. No wallet app, no browser, no network, just node:test.

> Agents: search these docs with the `search_docs` tool on the MCP server at https://createbsvapp.vercel.app/mcp, or read everything at https://createbsvapp.vercel.app/llms-full.txt.

You can't click "Approve" in a wallet from CI. You don't have to. A wallet's job in a proof is to *sign*, and `ProtoWallet` from `@bsv/sdk` signs with a key held in memory. Give each test its own random key and you have as many users as you like.

## A complete test file

Five tests covering the guarantees you actually rely on. Copy it into `server/test/auth.test.ts`. It uses only what a scaffold already installs.

```ts [server/test/auth.test.ts] twoslash
// @filename: test/auth.test.ts
// ---cut---
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { PrivateKey, ProtoWallet } from '@bsv/sdk'
import { createAuthProof, type RequestBody } from '../src/bsv/auth.js'
import { verifySignedRequest } from '../src/bsv/verifySignedRequest.js'

// Two throwaway identities: no wallet app, no network, no browser.
const server = new ProtoWallet(PrivateKey.fromRandom())
const alice = new ProtoWallet(PrivateKey.fromRandom())
const { publicKey: serverKey } = await server.getPublicKey({ identityKey: true })
const { publicKey: aliceKey } = await alice.getPublicKey({ identityKey: true })

// A fresh in-memory nonce store per test keeps tests independent.
const nonceStore = () => {
  const seen = new Set<string>()
  return (nonce: string) => (seen.has(nonce) ? false : (seen.add(nonce), true))
}
const sign = (body: RequestBody) =>
  createAuthProof(alice, { counterparty: serverKey, action: 'create-note', body })

test('a signed request proves who sent it', async () => {
  const body = { text: 'gm' }
  const r = await verifySignedRequest(server, await sign(body), { action: 'create-note', body }, nonceStore())
  assert.equal(r.valid, true)
  assert.equal(r.identityKey, aliceKey)
})

test('a changed body is rejected', async () => {
  const proof = await sign({ text: 'gm' })
  const r = await verifySignedRequest(server, proof, { action: 'create-note', body: { text: 'gn' } }, nonceStore())
  assert.equal(r.valid, false)
})

test('a different action is rejected', async () => {
  const body = { text: 'gm' }
  const r = await verifySignedRequest(server, await sign(body), { action: 'delete-note', body }, nonceStore())
  assert.equal(r.valid, false)
})

test('a replayed proof is rejected', async () => {
  const body = { text: 'gm' }
  const proof = await sign(body)
  const consume = nonceStore()
  assert.equal((await verifySignedRequest(server, proof, { action: 'create-note', body }, consume)).valid, true)
  assert.equal((await verifySignedRequest(server, proof, { action: 'create-note', body }, consume)).valid, false)
})

test('a proof made for another server is rejected', async () => {
  const other = new ProtoWallet(PrivateKey.fromRandom())
  const body = { text: 'gm' }
  const r = await verifySignedRequest(other, await sign(body), { action: 'create-note', body }, nonceStore())
  assert.equal(r.valid, false)
})
```

Run it from `server/`:

```console
$ npx tsx --test test/auth.test.ts
✔ a signed request proves who sent it
✔ a changed body is rejected
✔ a different action is rejected
✔ a replayed proof is rejected
✔ a proof made for another server is rejected
ℹ tests 5
ℹ pass 5
ℹ fail 0
```

Add `"test": "tsx --test test/*.test.ts"` to `server/package.json` and it's `npm test` from then on.

::: tip Why the server is a `ProtoWallet` too
That's exactly what the generated server does: `new ProtoWallet(PrivateKey.fromString(SERVER_PRIVATE_KEY))`. A `ProtoWallet` does the key work (deriving keys, signing, verifying) with no coin storage behind it, which is all a verifier needs.
:::

## Testing over HTTP

To exercise your real routes, middleware included, start the server and post proofs at it. The [tutorial's attack script](https://createbsvapp.vercel.app/docs/tutorial#5-break-it-on-purpose) does exactly that. The only extra step is fetching the server's identity first, because it's the proof's counterparty:

```ts
const { identityKey: server } = await (await fetch('http://localhost:3000/api/identity')).json()
const proof = await createAuthProof(wallet, { counterparty: server, action: 'login' })
await fetch('http://localhost:3000/api/login', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(proof),
})
```

::: warning Set `SERVER_PRIVATE_KEY` in long test runs
Without it, the server picks a new identity each time it restarts. In watch mode that means mid-run, and any proof signed before the restart fails. Put a fixed test key in `server/.env.test` and start the server with `tsx watch --env-file=.env.test src/index.ts`.
:::

## Testing the React side

The client hooks call a real wallet through `WalletClient('auto')`. For component tests, mock `useWallet()` rather than the wallet. With [Vitest](https://vitest.dev) (not installed by the scaffold):

```tsx
vi.mock('./bsv/WalletContext', () => ({
  useWallet: () => ({ connected: true, identityKey: '02ab…', status: 'connected', wallet: null }),
}))
```

For a true end-to-end run (Playwright clicking **Connect wallet**), you need a wallet that approves automatically. That's out of scope for the scaffold. Most teams cover the crypto in server tests like the ones above, and the UI with mocks.
