| name | api-test |
| description | Generate an integration test for an API route with all required mocks and boilerplate. Use when creating tests for API endpoints. |
| argument-hint | <route-import-path> |
Integration Test Generator
Generate an integration test for the API route at $ARGUMENTS.
Required Boilerplate (copy exactly)
File header — imports and standard mocks:
import { describe, it, expect, beforeEach, afterAll, vi } from "vitest";
import { cleanDatabase, disconnectTestDb } from "../../setup/test-db";
import { setMockSession, clearMockSession } from "../../setup/mock-session";
import {
callRoute,
callRouteWithParams,
expectJson,
createTestUser,
} from "../../setup/test-helpers";
vi.mock("@/lib/db", async () => {
const { getTestPrisma } = await import("../../setup/test-db");
return { prisma: getTestPrisma() };
});
vi.mock("@/lib/logger", () => ({
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
apiLogger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
dbLogger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));
External service mocks (if needed):
const mockTestConnection = vi.fn();
vi.mock("@/lib/arr/sonarr-client", () => ({
SonarrClient: vi.fn().mockImplementation(function () {
return { testConnection: mockTestConnection };
}),
}));
vi.hoisted() pattern — use when mock variables are referenced inside vi.mock() factories:
const { mockFn } = vi.hoisted(() => ({
mockFn: vi.fn(),
}));
vi.mock("@/lib/some-module", () => ({
someFunction: mockFn,
}));
Route imports — MUST come AFTER all vi.mock() calls:
import { GET, POST } from "@/app/api/path/to/route";
Lifecycle hooks:
describe("API endpoint description", () => {
beforeEach(async () => {
await cleanDatabase();
clearMockSession();
vi.clearAllMocks();
});
afterAll(async () => {
await disconnectTestDb();
});
});
Standard Test Cases
For every route — 401 unauthorized:
it("returns 401 when not authenticated", async () => {
const response = await callRoute(GET);
await expectJson(response, 401);
});
GET list endpoints:
it("returns empty array when user has no data", async () => {
const user = await createTestUser();
setMockSession({ userId: user.id, plexToken: "tok", isLoggedIn: true });
const response = await callRoute(GET);
const body = await expectJson<{ items: unknown[] }>(response, 200);
expect(body.items).toEqual([]);
});
it("returns data belonging to authenticated user", async () => {
const user = await createTestUser();
setMockSession({ userId: user.id, plexToken: "tok", isLoggedIn: true });
const response = await callRoute(GET);
const body = await expectJson<{ items: unknown[] }>(response, 200);
expect(body.items).toHaveLength(1);
});
it("does not return data belonging to another user", async () => {
const user1 = await createTestUser();
const user2 = await createTestUser({ data: { plexId: "other", plexUsername: "other" } });
setMockSession({ userId: user1.id, plexToken: "tok", isLoggedIn: true });
const response = await callRoute(GET);
const body = await expectJson<{ items: unknown[] }>(response, 200);
expect(body.items).toHaveLength(0);
});
POST endpoints:
it("returns 400 with invalid body", async () => {
const user = await createTestUser();
setMockSession({ userId: user.id, plexToken: "tok", isLoggedIn: true });
const response = await callRoute(POST, { body: {} });
await expectJson(response, 400);
});
it("creates resource with valid data", async () => {
const user = await createTestUser();
setMockSession({ userId: user.id, plexToken: "tok", isLoggedIn: true });
const response = await callRoute(POST, {
body: { name: "Test", },
});
const body = await expectJson<{ item: { id: string } }>(response, 201);
expect(body.item.id).toBeDefined();
});
PUT/DELETE with [id] — ownership check:
it("returns 404 when resource belongs to another user", async () => {
const user1 = await createTestUser();
const user2 = await createTestUser({ data: { plexId: "other", plexUsername: "other" } });
setMockSession({ userId: user1.id, plexToken: "tok", isLoggedIn: true });
const response = await callRouteWithParams(DELETE, { id: resource.id });
await expectJson(response, 404);
});
Route Handler Invocation
- Simple routes:
callRoute(GET) or callRoute(POST, { body: { ... } })
- With URL:
callRoute(GET, { url: "/api/path?param=value" })
- Dynamic params:
callRouteWithParams(PUT, { id: "abc" }, { body: { ... } })
- Assert response:
const body = await expectJson<{ key: Type }>(response, 200);
Steps
- Read the route file to identify which HTTP methods it exports
- Check imports for external services that need mocking (Plex, Sonarr, Radarr, etc.)
- Identify which data factories are needed based on Prisma models used
- Create test file at
tests/integration/<category>/<name>.test.ts
- Include all standard test cases plus route-specific edge cases
- Run the test to verify:
pnpm exec vitest run tests/integration/<category>/<name>.test.ts