# Test your backend without a wallet

[`fake-sub3.mjs`](fake-sub3.mjs) is a fake sub3 API for automated tests. It serves what an integration reads
from sub3 (the session keys, `GET /v1/products`, `GET /v1/access` and `GET /v1/orders`) from state your test sets,
and mints session tokens exactly like sub3's `/login`: EdDSA JWTs with your issuer and audience. One file, no
dependencies, Node 22+. Copy it into your project's tests.

What it lets a test do, with no wallet, chain, test USDC or network:

- sign a user in: `await sub3.token(user)` is the token the browser would send as `Authorization: Bearer …`;
- make the user a member: `sub3.state.access[user] = { membership: sub3.membership('pro', 2), lifetime: [] }`;
- give them a lifetime item: `sub3.state.access[user] = { membership: null, lifetime: ['forever'] }`;
- record a payment: `sub3.state.orders.push(sub3.order({ user, payer: user, credits: '1000' }))`
  (a confirmed pack by default; `{ confirmed: false }` is still pending, `{ kind: 'tier', … }` a tier order);
- take sub3 down: `sub3.state.fail = true` answers 502 everywhere, which your app must treat as "unknown",
  never as "not paid";
- count reads: `sub3.state.hits.access` shows whether your cache works.

Pass your real product and items so the test sees what production will:
`startFakeSub3({ product: 'acme/app', items: [...] })`. The items have the shape of
`GET /v1/products/<handle>/<key>`; copy them from the sandbox API's answer. Any other product answers
`unknown_product`, like the real API.

## An end-to-end test of your backend

Start the fake, point your backend at it (its `SUB3_API` and `SUB3_SITE`, and `APP_ORIGIN` as the token
audience), then call your own routes as a signed-in user:

```js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { startFakeSub3 } from './fake-sub3.mjs';
import { startMyApp } from '../server.mjs'; // your app, started with the URLs below

test('a member gets in, a stranger does not, an outage is not "unpaid"', async (t) => {
	const sub3 = await startFakeSub3({ product: 'acme/app', audience: 'http://localhost:3317' });
	t.after(sub3.close);
	const app = await startMyApp({
		SUB3_API: sub3.api,
		SUB3_SITE: sub3.site,
		APP_ORIGIN: 'http://localhost:3317'
	});
	t.after(app.close);
	const alice = `0x${'aa'.repeat(20)}`;
	const get = async (path, user) =>
		fetch(app.url + path, { headers: { authorization: `Bearer ${await sub3.token(user)}` } });

	assert.equal((await get('/members', alice)).status, 402); // signed in, not paid

	sub3.state.access[alice] = { membership: sub3.membership('pro', 2), lifetime: [] };
	await fetch(app.url + '/api/refresh', {
		method: 'POST',
		headers: { authorization: `Bearer ${await sub3.token(alice)}` }
	}); // as after a checkout: read access fresh
	assert.equal((await get('/members', alice)).status, 200);

	const bad = await sub3.token(alice, { audience: 'https://elsewhere.example' });
	const wrong = await fetch(app.url + '/members', { headers: { authorization: `Bearer ${bad}` } });
	assert.equal(wrong.status, 401); // a token for another site is rejected

	sub3.state.fail = true;
	assert.notEqual((await get('/members', `0x${'bb'.repeat(20)}`)).status, 402); // unknown, not unpaid
});
```

The Node example's tests ([`../node/test/http.test.mjs`](../node/test/http.test.mjs)) use this fake for
sessions, the access cache, allowance cycles, upgrades, lifetime items and credit packs credited once.

The fake checks nothing on chain. Before going live, also try the real thing once on the sandbox
(https://sub3.dev/sandbox, free test USDC from https://faucet.circle.com).
