# Write Tests

> Write tests following project conventions. Use when adding new tests or modifying existing ones. Use when this capability is needed.

- Skill: `tomevault-io/write-tests` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/write-tests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/write-tests/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/write-tests

---


# Write Tests Skill

Write tests using Vitest. Tests are **colocated** next to source files.

## Structure

```
src/lib/utils.ts
src/lib/utils.test.ts        # colocated
src/app/api/stats/route.ts
src/app/api/stats/route.test.ts
src/test-utils/
├── setup.ts        # global setup (PGlite, MSW, auth mock)
├── msw-handlers.ts # external API mocks
└── auth.ts         # auth test helpers
```

## Auth Testing

Auth is **globally mocked**. Use helpers to control auth state:

```typescript
import { mockAuthenticated, mockUnauthenticated } from '@/test-utils/auth';

it('returns 401 when unauthenticated', async () => {
  await mockUnauthenticated();
  const response = await GET(new Request('http://localhost/api/foo'));
  expect(response.status).toBe(401);
});

it('returns data when authenticated', async () => {
  await mockAuthenticated(); // uses default test user
  // or with overrides:
  await mockAuthenticated({ email: 'custom@example.com' });

  const response = await GET(new Request('http://localhost/api/foo'));
  expect(response.status).toBe(200);
});
```

Default test user: `test@example.com` / `Test User`

## Database Testing

Uses **PGlite** (in-memory PostgreSQL). No Docker required.

- Schema pushed automatically on boot
- Each test runs in a transaction that rolls back
- Use `insertUsageRecord` from `@/lib/queries` to seed data

```typescript
import { insertUsageRecord, getOverallStats } from '@/lib/queries';

beforeEach(async () => {
  await insertUsageRecord({
    date: '2025-01-01',
    email: 'user@example.com',
    tool: 'claude_code',
    model: 'sonnet-4',
    rawModel: 'claude-sonnet-4-20250514',
    inputTokens: 1000,
    outputTokens: 500,
    cacheWriteTokens: 0,
    cacheReadTokens: 0,
    cost: 0.01,
  });
});
```

## Running Tests

```bash
pnpm test        # run all
pnpm test:watch  # watch mode
```

## Key Rules

1. Colocate tests next to source (`foo.ts` → `foo.test.ts`)
2. Use `mockAuthenticated()`/`mockUnauthenticated()` for auth
3. Use `insertUsageRecord` for seeding database
4. External APIs mocked via MSW in `src/test-utils/msw-handlers.ts`

## CRITICAL: Auth Tests Required

**Every protected route MUST have an auth test.** This is non-negotiable.

### Session-Authenticated Routes

Routes using `getSession()` must verify 401 on unauthenticated requests:

```typescript
it('returns 401 for unauthenticated requests', async () => {
  await mockUnauthenticated();
  const response = await GET(new Request('http://localhost/api/your-route'));
  expect(response.status).toBe(401);
});
```

### Cron Routes

Routes using `CRON_SECRET` must verify auth:

```typescript
beforeEach(() => {
  vi.stubEnv('CRON_SECRET', 'test-secret');
});

it('returns 401 without authorization header', async () => {
  const response = await GET(new Request('http://localhost/api/cron/your-route'));
  expect(response.status).toBe(401);
});

it('returns 401 with invalid authorization', async () => {
  const response = await GET(
    new Request('http://localhost/api/cron/your-route', {
      headers: { Authorization: 'Bearer wrong-secret' },
    })
  );
  expect(response.status).toBe(401);
});
```

### Webhook Routes

Routes with signature verification must test invalid signatures:

```typescript
it('returns 401 without signature', async () => {
  const response = await POST(
    new Request('http://localhost/api/webhooks/github', {
      method: 'POST',
      body: '{}',
    })
  );
  expect(response.status).toBe(401);
});

it('returns 401 with invalid signature', async () => {
  const response = await POST(
    new Request('http://localhost/api/webhooks/github', {
      method: 'POST',
      body: '{}',
      headers: { 'x-hub-signature-256': 'sha256=invalid' },
    })
  );
  expect(response.status).toBe(401);
});
```

**When adding a new route, always ask: "What auth does this route require?" and add the corresponding auth test.**

---
> Source: [getsentry/abacus](https://github.com/getsentry/abacus) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-07-04 -->

