Mock Reviews
Three small codebases, each with bugs from every level. Run them like the real round: timed, systematic, ranked.
How to run a mock
Each mock is a small multi-file feature (routes, a service, a repository, a test) with 9–11 planted bugs across L1–L6 and the tests. That's close to what a 45-minute interview round contains.
- Set the time box shown on the drill. The timer starts with your first flag.
- Use the passes from Reading in Passes: orient, search sweep, follow the money, two-at-once, attacker, production.
- Flag every line you'd raise, then write your top five findings, ranked, in the four-part format (what and where, how it breaks, fix, prevention) before you press Check.
- Check, then compare your ranking with the model write-up below each mock.
Scoring yourself:
| Result | Meaning |
|---|---|
| Found all the L5 and money bugs | You'd pass the round on substance |
| Found 70%+ overall, few false alarms | Strong |
| Ranked security and money first | That's what interviewers grade most heavily |
| Explained how each one breaks | The difference between "I spotted it" and "I understand it" |
Do each mock once now and again in two weeks. The second run should be faster and better ranked, not just higher scoring.
Mock 1: Team invites
A SaaS app lets workspace admins invite people by email. Workspaces have a seat limit.
Model write-up (open after your own)
Ranked by severity, likelihood and blast radius:
- Anyone can join any workspace (bugs 9, 1, 6, 4). The pending-invites endpoint has no membership check (1) and returns each invite's token (9), so any logged-in user can list another workspace's invites and accept one. Tokens also leak through the logs (6) and are guessable (4). Fix: membership check on the list route, never return or log tokens, cryptographic tokens stored hashed. Prevention: treat tokens like passwords; a test that a non-member gets 404.
- Privilege escalation (bug 2). Any admin can invite an owner. Fix: schema-validated body, roles capped at the inviter's.
- Invite reuse and expiry (bug 8). A double click or a forwarded link adds two memberships, and expired invites still work. Fix: one conditional
UPDATE … WHERE accepted_at IS NULL AND expires_at > now()in the same transaction as adding the member. - Seat limit bypass (bugs 10, 3). Pending invites aren't counted, and concurrent invites race the count. Fix: count members plus pending invites under a row lock, and recheck on acceptance.
- Email case mismatch (bug 5). Real users can't accept their own invites. Fix: normalise on write.
- Silent email failures (bug 7) and the test that can't fail (bug 11), which is why the seat bugs weren't caught.
Mock 2: Wallet top-ups
Users top up an in-app wallet through a payment provider's hosted payment link, and can withdraw to their bank. The wallet stores integer paise.
Model write-up (open after your own)
- Overdrafts and unrecorded payouts (bug 9). Two withdrawals can both pass the balance check, and a crash between
payouts.sendand the debit pays out money that's never deducted. This is direct financial loss. Fix: atomic conditional debit first, then the payout with an idempotency key, with a compensating credit on failure. - Webhook signature (bug 5). Verifying re-serialised JSON can reject real webhooks or accept tampered ones, and the comparison leaks timing. Fix: raw body +
timingSafeEqual. - Double credits and lost credits (bugs 8, 7). Redelivered events credit again; concurrent ones overwrite each other. Fix: processed-event table with a unique constraint plus an atomic increment, in one transaction.
- Wrong amounts (bugs 6, 1, 3). Credits are 100× too small; top-up amounts are never validated; all top-ups share one reference. Fix: integer paise validated at the boundary, a top-up record whose ID is the reference.
- Provider failures (bugs 4, 2): unchecked
res.okand no timeout. - The test (bug 10) passes with the 100× bug present, which is why it shipped.
Mock 3: Report exports
Tenants request a CSV export of their orders for a date range. A worker builds the file and emails a download link.
Model write-up (open after your own)
- Cross-tenant data leak (bug 3). Export IDs travel in emails and links; anyone logged in with an ID downloads another company's orders. Fix: scope the lookup to the caller's tenant. Prevention: never serve files by a client-supplied name; store the path on a tenant-scoped record.
- CSV formula injection (bug 7). Opening the export can run attacker-chosen formulas on an employee's machine, and unescaped commas corrupt the file. Fix: proper CSV quoting plus formula-prefix neutralising.
- Incomplete or wrong exports (bugs 1, 5). The last day is missing, and rows are skipped or duplicated while orders arrive. Finance will reconcile against this file. Fix: half-open date range and keyset paging.
- Reliability (bugs 2, 4, 6). Queued exports are lost on deploy, one failure crashes the worker, and large tenants run it out of memory. Fix: durable jobs, error handling per job, streaming.
- Ambiguous timestamps (bug 8).
- The test (bug 9) uses data too small and too tidy to catch any of the above.
What to take from the mocks
- Most serious bugs were at L3–L5, and most of them were visible only by following data across files: the token from the repo to the route, the amount from the webhook to the wallet, the export ID from the email to the download.
- Every mock had a test that couldn't catch its worst bug. Reading the tests is part of the review.
- Ranking matters as much as finding. Lead with the bug that loses money or leaks data, even if you found it last.
For runnable practice with failing tests to fix, use the repository's bug-gym/ folder: 01-orders-api (12 bugs across L1–L5) and 02-webhook-worker (9 bugs, mostly L4 and L6). Review each one first without running anything, then run npm run test:01 / test:02 from bug-gym/ and fix until green. The JS Gym has ten more, focused on async bugs.
Go deeper
- The Bug Catalogue: every kind of bug in these mocks, and more.
- Reading in Passes: the method to use on them.
- JS · Reviewing a Codebase: presenting findings and defending design changes.
- Next: Part 2, Eye Training: short plain-JavaScript snippets, grouped by surface, to train spotting bugs on sight.