Automating the API tests
With the manual test cases designed, automate them at the layer where they belong most: the API. This phase turns the test-case table from the manual phase into a fast, exhaustive automated suite — the thick base of your pyramid. It is the highest-value automation you will write for AgentPay, and much of it is a direct translation of thinking you have already done. This lesson walks the API-automation phase of the capstone.
Set up and get one test running
Work in the AgentPay repo (or your own copy). The API already uses Vitest and can be tested in-process with
Fastify's inject (the api-automation module). Your first goal is one green test end to end:
import { describe, it, expect, beforeEach } from 'vitest';
import { buildApp } from './app.js';
import { reset } from './db.js';
let app;
beforeEach(() => { reset(); app = buildApp(); }); // known data every test
it('logs in and returns a token', async () => {
const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { email: 'admin@agentpay.test', password: 'admin123' } });
expect(res.statusCode).toBe(200);
expect(res.json().token).toBeTypeOf('string');
});
Add the tokenFor helper (the auth-and-state lesson) so no test repeats the login, and confirm the suite
runs (npm run test:api). Now you have the loop; the rest is filling it in from your test cases.
Automate the rules and validation
Translate your create-transaction cases into tests, asserting the right status and the body:
const good = { agentId: 1, amount: 500, phone: '9876543210', type: 'payment' };
it('creates a valid transaction (201)', async () => {
const res = await post('/transactions', good);
expect(res.statusCode).toBe(201);
expect(res.json()).toMatchObject({ agentId: 1, amount: 500, status: 'success' });
});
it('rejects amount 0 (422)', async () => expect((await post('/transactions', { ...good, amount: 0 })).statusCode).toBe(422));
it('rejects amount over limit (422)', async () => expect((await post('/transactions', { ...good, amount: 100001 })).statusCode).toBe(422));
it('rejects a bad phone (422)', async () => expect((await post('/transactions', { ...good, phone: '12345' })).statusCode).toBe(422));
it('rejects a suspended agent (409)', async () => expect((await post('/transactions', { ...good, agentId: 4 })).statusCode).toBe(409));
Each is one line of your manual table, now automated. This is where API automation pays off: the twenty boundary and negative cases that are tedious by hand run in milliseconds and on every change.
Automate auth, permissions and idempotency
The high-risk cases from your plan — automate them, because these are the serious bugs:
// auth
it('rejects no token (401)', async () => expect((await app.inject({ method: 'GET', url: '/transactions' })).statusCode).toBe(401));
// permission — the id-swap access test
it("forbids an operator reading another agent's transaction (403)", async () => {
const other = /* an admin-fetched transaction with agentId !== 1 */;
const res = await getAs(operatorToken, `/transactions/${other.id}`);
expect(res.statusCode).toBe(403);
});
// idempotency — no double charge
it('same Idempotency-Key never duplicates', async () => {
const headers = { authorization: `Bearer ${token}`, 'idempotency-key': 'k1' };
const a = await app.inject({ method: 'POST', url: '/transactions', headers, payload: good });
const b = await app.inject({ method: 'POST', url: '/transactions', headers, payload: good });
expect(a.json().id).toBe(b.json().id); // same record
});
These three — auth, the id-swap permission test, and idempotency — are the most valuable tests in the suite, because they guard access control and money, exactly the highest-risk behaviour you identified. Prioritise them.
Add schema validation
Finally, protect the contract (the contracts and assertions lessons). Define the expected transaction shape once and assert responses against it, so a quiet contract break (renamed field, type change) fails loudly:
import { z } from 'zod';
const TxnSchema = z.object({
id: z.number(), agentId: z.number(), amount: z.number(),
status: z.enum(['success', 'pending', 'failed']), createdAt: z.string(),
});
it('the transaction response matches its schema', async () => {
const res = await post('/transactions', good);
expect(() => TxnSchema.parse(res.json())).not.toThrow();
});
Add assertions for the collections too — filtering returns only-and-all matches, pagination drops/duplicates nothing, sorting is numeric — asserting the property, not hard-coded rows (the assertions lesson).
Compare with the reference, and reflect
Once your suite runs green, compare it with AgentPay's own apps/api/src/app.test.ts (21 tests). Did you
cover the same cases? Did the reference test something you missed (a case worth adding)? Did you test
something it did not (even better)? This comparison is a real learning check — not to copy, but to see where
your test design was strong and where it had gaps. A good outcome is your suite covering the rules, auth,
permissions, idempotency and schema, running in seconds, reliably — the thick, fast base your web and mobile
layers will sit on.
Check your work
Set up: in the AgentPay repo, one green test via Fastify inject + Vitest, beforeEach(reset) for known
data, and a tokenFor helper. Run with npm run test:api.
Rules/validation: translate your manual create-transaction table into tests asserting the right status and body — valid → 201, amount 0/over-limit/bad-phone → 422, suspended agent → 409. The tedious boundary/ negative cases now run in milliseconds every change.
High-risk cases: auth (no/invalid token → 401), the id-swap permission test (operator reads another agent's transaction → 403), and idempotency (same key → same id, no duplicate). The most valuable tests — they guard access control and money.
Schema: validate responses against a defined shape (Zod/JSON Schema) so contract breaks fail loudly; and assert collection properties (filter only-and-all, pagination no drop/dupe, numeric sort), not hard-coded rows.
Compare with the reference app.test.ts (21 tests) to check your design for gaps — not to copy. Aim for
a fast, reliable base covering rules/auth/permissions/idempotency/schema.
Practice
- Get one API test green in the AgentPay repo, with
beforeEach(reset)and atokenForhelper. - Automate your create-transaction validation table (valid + each invalid case) with correct statuses.
- Automate the three high-risk cases: an auth case, the id-swap permission test, and idempotency.
- Add Zod (or JSON Schema) validation of the transaction response, and make it fail by changing a field type.
- Automate the collection properties: filter returns only-and-all matches, pagination has no drops/duplicates.
- Compare your suite with the reference
app.test.ts; list one case it has that you missed and one you have that it lacks.
Official documentation
- AgentPay reference repository — Its
apps/api/src/app.test.tsis the reference API suite. - Vitest — Getting started — The test runner.
- Zod — Basic usage — Validating response schemas in tests.
Next: automating the web UI with Playwright.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship