Skip to content

Commit b7316cd

Browse files
kantordpeppescg
andauthored
test: set up automated mocking based on API schema (#48)
* test: add msw * test: add auto mocker * test: test actual mock api * test: remove implicity failures in auto-mocker * test: fix auto mocker * chore: fix type errors * test: reduce crazy auto mock size * fixes * fix heuristics * fixes * . * . * add standalone mock server for using msw with nextjs * allow running local server against mocks * fixes * . * . * . * . * . * chore: store provider accessToken into secure cookie * test: auth test cases * test: move test files * test: refactor import * . * fix: delete unmatched cookie * refactor: caching the derived key * refactor: suggestions from copilot * fix: derived key for missing better-auth secret * refactor: replace crypto with jose and clean cookie on signout * refactor: move auth into a dedicated lib/auth folder * leftover * refactor: add more error handling * . * . * cleanup * . * fixes * . * . * cleanup * . * config fixes * more global mocks * do not change generated files * move data fetching to server action * simplify base url logic * . * comment cleanup * . * . * . * load .env and .env.local equally * remove unused logic * patches from Giueseppe * remove OIDC_PROVIDER_ID env var --------- Co-authored-by: Giuseppe Scuglia <[email protected]>
1 parent ea13150 commit b7316cd

File tree

30 files changed

+2065
-105
lines changed

30 files changed

+2065
-105
lines changed

AGENTS.md

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This document provides essential context for AI coding assistants (Claude, GitHu
99
1. This file (AGENTS.md) - Project overview and key patterns
1010
2. `CLAUDE.md` - Detailed guidelines for AI assistants
1111
3. `README.md` - Setup and deployment instructions
12+
4. `docs/mocks.md` - MSW auto-mocker, fixtures, and dev server
1213

1314
## Project Summary
1415

@@ -36,10 +37,11 @@ This document provides essential context for AI coding assistants (Claude, GitHu
3637
## Core Principles
3738

3839
1. **Server Components First** - Use `'use client'` only when necessary
39-
2. **Generated API Client** - Never write manual fetch logic, use hey-api hooks
40-
3. **Async/Await Only** - No `.then()` promise chains
41-
4. **🚫 NEVER USE `any`** - STRICTLY FORBIDDEN. Use `unknown` with type guards or proper types
42-
5. **Stateless Auth** - JWT tokens, no server-side sessions
40+
2. **Generated API Client** - Never write manual fetch logic, use hey-api functions in server actions
41+
3. **Server Actions for API Calls** - Client components fetch data via server actions, not direct API calls
42+
4. **Async/Await Only** - No `.then()` promise chains
43+
5. **🚫 NEVER USE `any`** - STRICTLY FORBIDDEN. Use `unknown` with type guards or proper types
44+
6. **Stateless Auth** - JWT tokens, no server-side sessions
4345

4446
## ⚠️ Before Suggesting Code
4547

@@ -68,6 +70,8 @@ src/
6870
├── generated/ # hey-api output (DO NOT EDIT)
6971
└── hooks/ # Custom React hooks
7072
73+
src/mocks/ # MSW auto-mocker, handlers, fixtures, and dev server
74+
7175
dev-auth/ # Development OIDC mock
7276
helm/ # Kubernetes deployment
7377
scripts/ # Build scripts
@@ -78,6 +82,7 @@ scripts/ # Build scripts
7882
```bash
7983
# Development
8084
pnpm dev # Start dev server + OIDC mock
85+
pnpm mock:server # Start standalone MSW mock server (default: http://localhost:9090)
8186
pnpm dev:next # Start only Next.js
8287
pnpm oidc # Start only OIDC mock
8388

@@ -91,6 +96,14 @@ pnpm test # Run tests
9196
pnpm generate-client # Regenerate from backend API
9297
```
9398

99+
## Mocking & Fixtures
100+
101+
- Schema-based mocks are generated automatically. To create a new mock for an endpoint, run a Vitest test (or the app in dev) that calls that endpoint. The first call writes a fixture under `src/mocks/fixtures/<sanitized-path>/<method>.ts`.
102+
- To adjust the payload, edit the generated fixture file. Prefer this over adding a non-schema mock when you only need more realistic sample data.
103+
- Non-schema mocks live in `src/mocks/customHandlers` and take precedence over schema-based mocks. Use these for behavior overrides or endpoints without schema.
104+
105+
- Global test setup: Add common mocks to `vitest.setup.ts` (e.g., `next/headers`, `next/navigation`, `next/image`, `sonner`, auth client). Before copying a mock into a test file, check if it can be centralized globally. Reset all mocks globally between tests.
106+
94107
## Next.js App Router Key Concepts
95108

96109
### File-System Routing
@@ -134,8 +147,8 @@ app/
134147

135148
```typescript
136149
async function ServerList() {
137-
const response = await fetch("http://api/servers", {
138-
next: { revalidate: 3600 }, // Cache for 1 hour
150+
const response = await fetch("/registry/v0.1/servers", {
151+
next: { revalidate: 3600 }, // In dev, Next rewrites proxy to mock server
139152
});
140153
const data = await response.json();
141154
return <ServerList servers={data} />;
@@ -242,7 +255,7 @@ export async function createServer(formData: FormData) {
242255
### ❌ NEVER DO
243256

244257
- **Use `any` type** - STRICTLY FORBIDDEN. Use `unknown` + type guards or proper types
245-
- Edit files in `src/generated/*` (auto-generated)
258+
- **Edit files in `src/generated/*`** - Auto-generated, will be overwritten on regeneration
246259
- Use `'use client'` on every component
247260
- Create manual fetch logic in components
248261
- Use `.then()` promise chains
@@ -262,21 +275,42 @@ export async function createServer(formData: FormData) {
262275

263276
### Using Generated API Client
264277

265-
**Queries (GET)**:
278+
**⚠️ IMPORTANT:**
279+
- Never edit files in `src/generated/*`** - they are auto-generated and will be overwritten
280+
- **Always use server actions** - Client components should not call the API directly
281+
- The API client is server-side only (no `NEXT_PUBLIC_` env vars needed)
282+
283+
**Example: Server Action**:
266284

267285
```typescript
268-
import { useGetApiV0Servers } from "@/generated/client/@tanstack/react-query.gen";
286+
// src/app/catalog/actions.ts
287+
"use server";
288+
289+
import { getRegistryV01Servers } from "@/generated/sdk.gen";
269290

270-
const { data, isLoading, error } = useGetApiV0Servers();
291+
export async function getServersSummary() {
292+
try {
293+
const resp = await getRegistryV01Servers();
294+
const data = resp.data;
295+
// Process data...
296+
return { count: data?.servers?.length ?? 0, ... };
297+
} catch (error) {
298+
console.error("Failed to fetch servers:", error);
299+
return { count: 0, ... };
300+
}
301+
}
271302
```
272303

273-
**Mutations (POST/PUT/DELETE)**:
304+
**Example: Server Component**:
274305

275306
```typescript
276-
import { usePostApiV0Servers } from "@/generated/client/@tanstack/react-query.gen";
307+
// src/app/catalog/page.tsx
308+
import { getServersSummary } from "./actions";
277309

278-
const mutation = usePostApiV0Servers();
279-
await mutation.mutateAsync({ body: data });
310+
export default async function CatalogPage() {
311+
const summary = await getServersSummary();
312+
return <div>{summary.count} servers</div>;
313+
}
280314
```
281315

282316
**When Backend Changes**:

CLAUDE.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@ This document provides context and guidelines for Claude (and other AI assistant
6767

6868
### 3. Generated API Client
6969

70-
- **Never write manual fetch logic** - Always use hey-api generated hooks
70+
- **Never write manual fetch logic** - Always use hey-api generated functions
71+
- **Never edit generated files** - They are regenerated from OpenAPI spec and changes will be lost
72+
- **Use server actions for all API calls** - Client components should not call the API directly
7173
- API client regenerated from OpenAPI spec via custom script
72-
- Type-safe API calls with automatic loading/error states
74+
- Type-safe API calls with proper error handling
7375

74-
**Why**: Generated client ensures type safety, eliminates manual state management, and stays in sync with backend API.
76+
**Why**: Generated client ensures type safety and stays in sync with backend API. Server-only API access keeps the backend URL secure and reduces client bundle size.
7577

7678
### 4. Async/Await Over Promises
7779

@@ -137,7 +139,9 @@ pnpm generate-client:nofetch # Regenerate without fetching
137139
### DON'T ❌
138140

139141
1. **🚫 Don't EVER use `any` type** - STRICTLY FORBIDDEN. Use `unknown` + type guards or proper types
140-
2. **Don't edit generated files** - `src/generated/*` is auto-generated
142+
2. **🚫 Don't EVER edit generated files** - `src/generated/*` is auto-generated and will be overwritten
143+
- Generated files are overwritten on every `pnpm generate-client` run
144+
- If you need to configure or extend the client, do it in your own files
141145
3. **Don't use `'use client'` everywhere** - Only when necessary
142146
4. **Don't create custom fetch logic** - Use hey-api hooks
143147
5. **Don't use `.then()`** - Use async/await
@@ -198,11 +202,19 @@ pnpm generate-client:nofetch # Regenerate without fetching
198202
- Loading states and skeleton screens
199203
- Accessibility (keyboard navigation, screen readers)
200204

201-
### Testing Tools
205+
### Mocking & Testing
206+
207+
- **MSW Auto-Mocker**
208+
- Auto-generates handlers from `swagger.json` and creates fixtures under `src/mocks/fixtures` on first run.
209+
- Strict validation with Ajv + ajv-formats; fixtures are type-checked against `@api/types.gen` by default.
210+
- Hand-written, non-schema mocks live in `src/mocks/customHandlers` and take precedence over schema-based mocks.
211+
- Dev: `pnpm mock:server` starts a standalone HTTP mock on `http://localhost:9090` (configurable via `API_BASE_URL`). In dev, Next rewrites proxy `/registry/*` there; always use relative URLs like `/registry/v0.1/servers`.
212+
- Regenerate by deleting specific fixture files.
213+
- Create new fixtures by calling the desired endpoint in a Vitest test (or via the app in dev). The first call generates a TypeScript fixture file; customize the payload by editing that file instead of writing a new custom handler when you only need different sample data.
214+
- Prefer global test setup for common mocks: add shared mocks to `vitest.setup.ts` (e.g., `next/headers`, `next/navigation`, `next/image`, `sonner`, auth client). Before adding a mock in a specific test file, check if it belongs in the global setup.
202215

203216
- **Vitest** - Test runner (faster than Jest)
204217
- **Testing Library** - Component testing
205-
- **MSW** - API mocking
206218
- **jsdom** - DOM simulation
207219

208220
### Example Test

dev-auth/oidc-provider.mjs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import { config } from "dotenv";
12
import Provider from "oidc-provider";
23

3-
const ISSUER = "http://localhost:4000";
4-
const PORT = 4000;
4+
config();
5+
config({ path: ".env.local" });
6+
7+
const ISSUER = process.env.OIDC_ISSUER_URL || "http://localhost:4000";
8+
const PORT = new URL(ISSUER).port || 4000;
9+
const CLIENT_ID = process.env.OIDC_CLIENT_ID || "better-auth-dev";
10+
const CLIENT_SECRET =
11+
process.env.OIDC_CLIENT_SECRET || "dev-secret-change-in-production";
512

613
// Simple in-memory account storage
714
const accounts = {
@@ -17,8 +24,8 @@ const accounts = {
1724
const configuration = {
1825
clients: [
1926
{
20-
client_id: "better-auth-dev",
21-
client_secret: "dev-secret-change-in-production",
27+
client_id: CLIENT_ID,
28+
client_secret: CLIENT_SECRET,
2229
redirect_uris: [
2330
// Better Auth genericOAuth uses /oauth2/callback/:providerId
2431
"http://localhost:3000/api/auth/oauth2/callback/oidc",
@@ -126,10 +133,10 @@ oidc.use(async (ctx, next) => {
126133

127134
oidc.listen(PORT, () => {
128135
console.log(`🔐 OIDC Provider running at ${ISSUER}`);
129-
console.log(`📝 Client ID: better-auth-dev`);
130-
console.log(`🔑 Client Secret: dev-secret-change-in-production`);
136+
console.log(`📝 Client ID: ${CLIENT_ID}`);
137+
console.log(`🔑 Client Secret: ${CLIENT_SECRET}`);
131138
console.log(`👤 Test user: [email protected]`);
132139
console.log(
133-
`\n⚙️ Update your .env.local with:\nOIDC_CLIENT_ID=better-auth-dev\nOIDC_CLIENT_SECRET=dev-secret-change-in-production\nOIDC_ISSUER=${ISSUER}`,
140+
`\n⚙️ Update your .env.local with:\nOIDC_CLIENT_ID=${CLIENT_ID}\nOIDC_CLIENT_SECRET=${CLIENT_SECRET}\nOIDC_ISSUER_URL=${ISSUER}`,
134141
);
135142
});

docs/mocks.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
MSW Auto-Mocker
2+
3+
- Handlers: `src/mocks/handlers.ts` combines non-schema mocks and auto-generated mocks.
4+
- Non-schema mocks: add hand-written handlers in `src/mocks/customHandlers/index.ts`. These take precedence over schema-based mocks.
5+
- Auto-generated: `src/mocks/mocker.ts` reads `swagger.json` and creates fixtures under `src/mocks/fixtures` on first run.
6+
- Validation: Loaded fixtures are validated with Ajv; errors log to console.
7+
8+
Usage
9+
- Vitest: tests initialize MSW in `src/mocks/test.setup.ts`. Run `pnpm test`.
10+
- Browser (optional): call `startWorker()` from `src/mocks/browser.ts` in your development entry point to mock requests in the browser.
11+
- Standalone server (dev): `pnpm mock:server` starts an HTTP mock server at `http://localhost:9090` (configurable via `API_BASE_URL`). In dev, Next.js rewrites proxy `/registry/*` to this origin; use relative URLs like `/registry/v0.1/servers` from both client and server code.
12+
13+
Generating fixtures
14+
- To create a new fixture for an endpoint, simply run a Vitest test (or the app in dev) that calls that endpoint. The auto‑mocker will generate `src/mocks/fixtures/<sanitized-path>/<method>.ts` on first use using schema‑based fake data.
15+
- To customize the response, edit the generated TypeScript file. This is preferred over writing a non‑schema mock for simple data tweaks (e.g., replacing lorem ipsum with realistic text). Non‑schema mocks are intended for behavior overrides or endpoints without schema.
16+
17+
Regeneration
18+
- Delete a fixture file to re-generate it on next request.
19+
20+
Failure behavior (always strict)
21+
- If a schema is missing or faker fails, the handler responds 500 and does not write a placeholder.
22+
- Invalid fixtures (including empty `{}` when the schema defines properties) respond 500.
23+
24+
Types
25+
- Fixtures default to strict types. Generated modules import response types from `@api/types.gen` and use a `satisfies` clause to ensure compatibility.
26+
- Make sure `tsconfig.json` includes: `"paths": { "@api/*": ["./src/generated/*"] }`.

next.config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import type { NextConfig } from "next";
22

3+
const isDev = process.env.NODE_ENV !== "production";
4+
35
const nextConfig: NextConfig = {
46
/* config options here */
57
reactCompiler: true,
68
output: "standalone",
9+
async rewrites() {
10+
if (!isDev) return [];
11+
12+
const apiBaseUrl = process.env.API_BASE_URL || "";
13+
14+
return [
15+
// Proxy registry API in development (to mock server or real backend)
16+
{
17+
source: "/registry/:path*",
18+
destination: `${apiBaseUrl}/registry/:path*`,
19+
},
20+
];
21+
},
722
};
823

924
export default nextConfig;

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"packageManager": "[email protected]",
66
"scripts": {
7-
"dev": "concurrently -n \"OIDC,Next\" -c \"blue,green\" \"pnpm oidc\" \"pnpm dev:next\"",
7+
"dev": "concurrently -n \"OIDC,Mock,Next\" -c \"blue,magenta,green\" \"pnpm oidc\" \"pnpm mock:server\" \"pnpm dev:next\"",
88
"dev:next": "next dev",
99
"build": "next build",
1010
"start": "next start",
@@ -16,13 +16,18 @@
1616
"oidc": "node dev-auth/oidc-provider.mjs",
1717
"generate-swagger": "tsx scripts/generate-swagger.ts",
1818
"generate-client": "pnpm run generate-swagger && openapi-ts",
19-
"generate-client:nofetch": "openapi-ts"
19+
"generate-client:nofetch": "openapi-ts",
20+
"mock:server": "tsx src/mocks/server.ts"
2021
},
2122
"dependencies": {
23+
"ajv": "^8.17.1",
24+
"ajv-formats": "^3.0.1",
2225
"better-auth": "1.4.0-beta.25",
2326
"class-variance-authority": "0.7.1",
2427
"clsx": "2.1.1",
2528
"jose": "^6.1.2",
29+
"json-schema-faker": "^0.5.6",
30+
"msw": "^2.12.2",
2631
"next": "16.0.3",
2732
"react": "19.2.0",
2833
"react-dom": "19.2.0",
@@ -33,6 +38,7 @@
3338
"@biomejs/biome": "2.3.6",
3439
"@hey-api/client-next": "0.5.1",
3540
"@hey-api/openapi-ts": "0.87.5",
41+
"@mswjs/http-middleware": "^0.10.2",
3642
"@tailwindcss/postcss": "^4",
3743
"@testing-library/dom": "^10.4.1",
3844
"@testing-library/react": "^16.3.0",
@@ -43,6 +49,7 @@
4349
"@vitejs/plugin-react": "^5.1.1",
4450
"babel-plugin-react-compiler": "1.0.0",
4551
"concurrently": "^9.2.1",
52+
"dotenv": "^17.2.3",
4653
"husky": "^9.1.7",
4754
"jsdom": "^27.2.0",
4855
"lint-staged": "^16.0.0",

0 commit comments

Comments
 (0)