Skip to content

Commit ccafaec

Browse files
catomeanclaude
andcommitted
fix: resolve test infrastructure issues
- Add next/server mock for Jest to handle NextRequest/NextResponse imports - Fix api-responses tests to match actual function signatures: - jsonSuccess wraps data in response.data property - jsonValidationError expects ValidationError[] not object - formatZodErrors returns array not object with field properties Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4907fc4 commit ccafaec

4 files changed

Lines changed: 140 additions & 13 deletions

File tree

.eslintcache

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

__mocks__/next/server.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* Mock for next/server in Jest tests
3+
*
4+
* This allows tests to import modules that use NextResponse/NextRequest
5+
* without loading the actual Next.js server runtime.
6+
*/
7+
8+
// Use global Request/Response if available (jsdom), otherwise create simple mocks
9+
const BaseRequest =
10+
globalThis.Request ||
11+
class MockRequest {
12+
url: string;
13+
method: string;
14+
headers: Map<string, string>;
15+
body: unknown;
16+
17+
constructor(input: string | URL, init?: RequestInit) {
18+
this.url = input.toString();
19+
this.method = init?.method || 'GET';
20+
this.headers = new Map(Object.entries(init?.headers || {}));
21+
this.body = init?.body;
22+
}
23+
24+
json() {
25+
return Promise.resolve(typeof this.body === 'string' ? JSON.parse(this.body) : this.body);
26+
}
27+
};
28+
29+
const BaseResponse =
30+
globalThis.Response ||
31+
class MockResponse {
32+
body: string | null;
33+
status: number;
34+
headers: Map<string, string>;
35+
36+
constructor(body?: string | null, init?: ResponseInit) {
37+
this.body = body || null;
38+
this.status = init?.status || 200;
39+
this.headers = new Map(Object.entries(init?.headers || {}));
40+
}
41+
42+
json() {
43+
return Promise.resolve(this.body ? JSON.parse(this.body) : null);
44+
}
45+
};
46+
47+
export class NextRequest extends BaseRequest {
48+
nextUrl: URL;
49+
50+
constructor(input: string | URL, init?: RequestInit) {
51+
super(input, init);
52+
this.nextUrl = new URL(typeof input === 'string' ? input : input.toString());
53+
}
54+
55+
get cookies() {
56+
return {
57+
get: jest.fn(),
58+
getAll: jest.fn(() => []),
59+
set: jest.fn(),
60+
delete: jest.fn(),
61+
has: jest.fn(() => false),
62+
};
63+
}
64+
65+
get geo() {
66+
return {};
67+
}
68+
69+
get ip() {
70+
return '127.0.0.1';
71+
}
72+
}
73+
74+
export class NextResponse extends BaseResponse {
75+
static json(body: unknown, init?: ResponseInit) {
76+
const response = new NextResponse(JSON.stringify(body), {
77+
...init,
78+
headers: {
79+
...init?.headers,
80+
'content-type': 'application/json',
81+
},
82+
});
83+
return response;
84+
}
85+
86+
static redirect(url: string | URL, status?: number) {
87+
return new NextResponse(null, {
88+
status: status || 307,
89+
headers: { Location: url.toString() },
90+
});
91+
}
92+
93+
static rewrite(destination: string | URL) {
94+
return new NextResponse(null, {
95+
headers: { 'x-middleware-rewrite': destination.toString() },
96+
});
97+
}
98+
99+
static next() {
100+
return new NextResponse(null);
101+
}
102+
103+
get cookies() {
104+
return {
105+
get: jest.fn(),
106+
getAll: jest.fn(() => []),
107+
set: jest.fn(),
108+
delete: jest.fn(),
109+
};
110+
}
111+
}
112+
113+
export const userAgent = jest.fn(() => ({
114+
isBot: false,
115+
browser: { name: 'Chrome', version: '100' },
116+
device: { type: undefined, vendor: undefined, model: undefined },
117+
engine: { name: 'Blink', version: '100' },
118+
os: { name: 'Mac OS', version: '12' },
119+
cpu: { architecture: undefined },
120+
}));
121+
122+
export const userAgentFromString = jest.fn(() => userAgent());

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const customJestConfig = {
1111
testEnvironment: 'jest-environment-jsdom',
1212
moduleNameMapper: {
1313
'^@/(.*)$': '<rootDir>/$1',
14+
'^next/server$': '<rootDir>/__mocks__/next/server.ts',
1415
},
1516
testMatch: [
1617
'<rootDir>/tests/**/*.test.ts',

tests/__tests__/lib/api-responses.test.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,20 @@ describe('API Response Helpers', () => {
2828
describe('jsonSuccess', () => {
2929
it('returns success response with data', async () => {
3030
const response = jsonSuccess({ user: 'test' });
31-
const data = await response.json();
31+
const result = await response.json();
3232

3333
expect(response.status).toBe(200);
34-
expect(data.success).toBe(true);
35-
expect(data.user).toBe('test');
34+
expect(result.success).toBe(true);
35+
expect(result.data.user).toBe('test');
3636
});
3737

3838
it('returns success response with message', async () => {
3939
const response = jsonSuccess({ id: 1 }, 'Created successfully');
40-
const data = await response.json();
40+
const result = await response.json();
4141

42-
expect(data.success).toBe(true);
43-
expect(data.message).toBe('Created successfully');
44-
expect(data.id).toBe(1);
42+
expect(result.success).toBe(true);
43+
expect(result.message).toBe('Created successfully');
44+
expect(result.data.id).toBe(1);
4545
});
4646

4747
it('returns success response with custom status', async () => {
@@ -101,14 +101,15 @@ describe('API Response Helpers', () => {
101101

102102
describe('jsonValidationError', () => {
103103
it('returns 400 validation error response', async () => {
104-
const response = jsonValidationError('Invalid input', { field: 'email' });
104+
const details = [{ field: 'email', message: 'Invalid email format' }];
105+
const response = jsonValidationError('Invalid input', details);
105106
const data = await response.json();
106107

107108
expect(response.status).toBe(400);
108109
expect(data.success).toBe(false);
109110
expect(data.error).toBe('Invalid input');
110111
expect(data.code).toBe('VALIDATION_ERROR');
111-
expect(data.details).toEqual({ field: 'email' });
112+
expect(data.details).toEqual(details);
112113
});
113114
});
114115

@@ -127,8 +128,10 @@ describe('API Response Helpers', () => {
127128
if (!result.success) {
128129
const formatted = formatZodErrors(result.error);
129130

130-
expect(formatted).toHaveProperty('email');
131-
expect(formatted).toHaveProperty('password');
131+
// formatZodErrors returns an array of { field, message } objects
132+
expect(Array.isArray(formatted)).toBe(true);
133+
expect(formatted.some((e) => e.field === 'email')).toBe(true);
134+
expect(formatted.some((e) => e.field === 'password')).toBe(true);
132135
}
133136
});
134137

@@ -145,7 +148,8 @@ describe('API Response Helpers', () => {
145148

146149
if (!result.success) {
147150
const formatted = formatZodErrors(result.error);
148-
expect(formatted).toBeDefined();
151+
expect(Array.isArray(formatted)).toBe(true);
152+
expect(formatted.some((e) => e.field === 'user.email')).toBe(true);
149153
}
150154
});
151155
});

0 commit comments

Comments
 (0)