next-api-mock 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/__tests__/configValidator.test.ts +3 -3
- package/package.json +1 -1
- package/src/core.ts +380 -0
- package/src/index.ts +35 -198
- package/src/middleware.ts +25 -7
- package/src/monitoring.ts +18 -8
- package/src/serverMock.ts +15 -21
- package/src/types.ts +33 -23
- package/src/utils/versionCheck.ts +20 -0
- package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,5 @@
|
|
1
1
|
import { validateConfig } from '../src/configValidator'
|
2
|
-
import { MockConfig, MockFunction } from '../src/types'
|
2
|
+
import { MockConfig, MockFunction, MockResponse } from '../src/types'
|
3
3
|
import { NextApiRequest } from 'next'
|
4
4
|
import { NextRequest } from 'next/server'
|
5
5
|
|
@@ -10,7 +10,7 @@ describe('Config Validator', () => {
|
|
10
10
|
status: 200,
|
11
11
|
data: { message: 'Test successful' },
|
12
12
|
},
|
13
|
-
'/api/function': ((req: NextApiRequest | NextRequest) => ({
|
13
|
+
'/api/function': (async (req: NextApiRequest | NextRequest) => ({
|
14
14
|
status: 200,
|
15
15
|
data: { method: req.method },
|
16
16
|
})) as MockFunction,
|
@@ -58,7 +58,7 @@ describe('Config Validator', () => {
|
|
58
58
|
|
59
59
|
it('should pass for valid function configurations', () => {
|
60
60
|
const validConfig: MockConfig = {
|
61
|
-
'/api/function': ((req: NextApiRequest | NextRequest) => ({
|
61
|
+
'/api/function': (async (req: NextApiRequest | NextRequest): Promise<MockResponse> => ({
|
62
62
|
status: 200,
|
63
63
|
data: { method: req.method },
|
64
64
|
})) as MockFunction,
|
package/package.json
CHANGED
package/src/core.ts
ADDED
@@ -0,0 +1,380 @@
|
|
1
|
+
import { EventEmitter } from 'events'
|
2
|
+
import * as winston from 'winston'
|
3
|
+
import config from 'config'
|
4
|
+
import { parse } from 'url'
|
5
|
+
import { match } from 'path-to-regexp'
|
6
|
+
import { isAppRouter, isNextJs12, isNextJs13, isNextJs14, isNextJs15, getNextVersion } from './utils/versionCheck'
|
7
|
+
import {
|
8
|
+
MockResponse,
|
9
|
+
MockConfig,
|
10
|
+
RequestLogger,
|
11
|
+
MockInterceptor,
|
12
|
+
MockOptions,
|
13
|
+
MiddlewareFunction,
|
14
|
+
MockFunction,
|
15
|
+
AnyRequest,
|
16
|
+
AnyResponse,
|
17
|
+
isNextApiRequest as typeGuardIsNextApiRequest,
|
18
|
+
isNextRequest
|
19
|
+
} from './types'
|
20
|
+
import { validateConfig } from './configValidator'
|
21
|
+
import { applyMiddleware } from './middleware'
|
22
|
+
import { cacheResponse, getCachedResponse, clearCache } from './cache'
|
23
|
+
import { configureServerMocks, resetServerMocks, mockServerAction, createServerComponentMock } from './serverMock'
|
24
|
+
import { initializePlugins, registerPlugin } from './plugins'
|
25
|
+
import { setSecureHeaders as localSetSecureHeaders } from './security'
|
26
|
+
import { collectMetrics, reportMetrics } from './monitoring'
|
27
|
+
import { createRequestValidator } from './validation'
|
28
|
+
import { createRateLimiter } from './rateLimit'
|
29
|
+
import { createMockDatabase } from './mockDatabase'
|
30
|
+
import { createGraphQLMockHandler } from './graphqlMock'
|
31
|
+
|
32
|
+
import type { NextApiRequest, NextApiResponse } from 'next'
|
33
|
+
import { NextRequest, NextResponse } from 'next/server'
|
34
|
+
|
35
|
+
const logger = winston.createLogger({
|
36
|
+
level: config.get<string>('logLevel') || 'info',
|
37
|
+
format: winston.format.json(),
|
38
|
+
defaultMeta: { service: 'mock-library' },
|
39
|
+
transports: [
|
40
|
+
new winston.transports.Console(),
|
41
|
+
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
42
|
+
new winston.transports.File({ filename: 'combined.log' }),
|
43
|
+
],
|
44
|
+
})
|
45
|
+
|
46
|
+
const defaultConfig: MockConfig = {}
|
47
|
+
const defaultOptions: MockOptions = {
|
48
|
+
enableLogging: false,
|
49
|
+
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
50
|
+
defaultDelay: 0,
|
51
|
+
errorRate: 0,
|
52
|
+
}
|
53
|
+
|
54
|
+
let mockConfig: MockConfig = { ...defaultConfig }
|
55
|
+
let mockOptions: MockOptions = { ...defaultOptions }
|
56
|
+
let globalDelay = 0
|
57
|
+
let requestLogger: RequestLogger | null = null
|
58
|
+
let mockInterceptor: MockInterceptor | null = null
|
59
|
+
const eventEmitter = new EventEmitter()
|
60
|
+
const middlewarePipeline: MiddlewareFunction[] = []
|
61
|
+
|
62
|
+
export async function configureMocksAsync(configPromise: Promise<MockConfig>, options?: Partial<MockOptions>): Promise<void> {
|
63
|
+
try {
|
64
|
+
const loadedConfig = await configPromise
|
65
|
+
configureMocks(loadedConfig, options)
|
66
|
+
} catch (error) {
|
67
|
+
logger.error('Error loading configuration:', error)
|
68
|
+
throw new Error('Failed to load mock configuration')
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
export function configureMocks(loadedConfig: MockConfig, options?: Partial<MockOptions>): void {
|
73
|
+
try {
|
74
|
+
validateConfig(loadedConfig)
|
75
|
+
mockConfig = { ...defaultConfig, ...loadedConfig }
|
76
|
+
mockOptions = { ...defaultOptions, ...options }
|
77
|
+
if (mockOptions.enableLogging) {
|
78
|
+
setRequestLogger(defaultLogger)
|
79
|
+
}
|
80
|
+
if (mockOptions.defaultDelay) {
|
81
|
+
setGlobalDelay(mockOptions.defaultDelay)
|
82
|
+
}
|
83
|
+
initializePlugins(mockConfig)
|
84
|
+
} catch (error) {
|
85
|
+
logger.error('Error configuring mocks:', error)
|
86
|
+
throw new Error('Failed to configure mocks')
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
export function resetMocks(): void {
|
91
|
+
mockConfig = { ...defaultConfig }
|
92
|
+
mockOptions = { ...defaultOptions }
|
93
|
+
globalDelay = 0
|
94
|
+
requestLogger = null
|
95
|
+
mockInterceptor = null
|
96
|
+
clearCache()
|
97
|
+
resetServerMocks()
|
98
|
+
}
|
99
|
+
|
100
|
+
export function setGlobalDelay(delay: number): void {
|
101
|
+
globalDelay = delay
|
102
|
+
}
|
103
|
+
|
104
|
+
export function setRequestLogger(logger: RequestLogger): void {
|
105
|
+
requestLogger = logger
|
106
|
+
}
|
107
|
+
|
108
|
+
export function setMockInterceptor(interceptor: MockInterceptor): void {
|
109
|
+
mockInterceptor = interceptor
|
110
|
+
}
|
111
|
+
|
112
|
+
export function onMockResponse(listener: (path: string, response: MockResponse<unknown>) => void): () => void {
|
113
|
+
eventEmitter.on('mockResponse', listener)
|
114
|
+
return () => eventEmitter.off('mockResponse', listener)
|
115
|
+
}
|
116
|
+
|
117
|
+
export function addMiddleware(middleware: MiddlewareFunction): void {
|
118
|
+
middlewarePipeline.push(middleware)
|
119
|
+
}
|
120
|
+
|
121
|
+
function defaultLogger(req: AnyRequest, res: AnyResponse): void {
|
122
|
+
let status: number
|
123
|
+
if (isNextApiResponse(res)) {
|
124
|
+
status = res.statusCode
|
125
|
+
} else if ('status' in res) {
|
126
|
+
status = res.status
|
127
|
+
} else {
|
128
|
+
status = 200 // Default status if not found
|
129
|
+
}
|
130
|
+
logger.info(`${req.method} ${req.url} - Status: ${status}`)
|
131
|
+
}
|
132
|
+
|
133
|
+
function shouldSimulateError(): boolean {
|
134
|
+
return Math.random() < mockOptions.errorRate
|
135
|
+
}
|
136
|
+
|
137
|
+
const limiter = createRateLimiter({
|
138
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
139
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
140
|
+
})
|
141
|
+
|
142
|
+
export function createMockHandler(path: string) {
|
143
|
+
if (isAppRouter) {
|
144
|
+
return createAppRouterHandler(path)
|
145
|
+
} else {
|
146
|
+
return createPagesApiHandler(path)
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
function createAppRouterHandler(path: string) {
|
151
|
+
return async (req: NextRequest, context: { params: any }): Promise<NextResponse> => {
|
152
|
+
try {
|
153
|
+
if (isNextRequest(req)) {
|
154
|
+
// Handle NextRequest (App Router)
|
155
|
+
const headers = new Headers(req.headers)
|
156
|
+
headers.set('X-Secure-Header', 'true')
|
157
|
+
const secureReq = new NextRequest(req.url, {
|
158
|
+
headers,
|
159
|
+
method: req.method,
|
160
|
+
body: req.body,
|
161
|
+
cache: req.cache,
|
162
|
+
credentials: req.credentials,
|
163
|
+
integrity: req.integrity,
|
164
|
+
keepalive: req.keepalive,
|
165
|
+
mode: req.mode,
|
166
|
+
redirect: req.redirect,
|
167
|
+
referrer: req.referrer,
|
168
|
+
referrerPolicy: req.referrerPolicy,
|
169
|
+
signal: req.signal,
|
170
|
+
})
|
171
|
+
|
172
|
+
// Process the request with the new headers
|
173
|
+
return handleRequest(secureReq, path)
|
174
|
+
} else {
|
175
|
+
throw new Error('Unexpected request type in App Router handler')
|
176
|
+
}
|
177
|
+
} catch (error) {
|
178
|
+
logger.error('Error in App Router handler:', error)
|
179
|
+
return NextResponse.json({ error: 'Internal server error', details: (error as Error).message }, { status: 500 })
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
function createPagesApiHandler(path: string) {
|
185
|
+
return limiter(async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
186
|
+
try {
|
187
|
+
if (typeGuardIsNextApiRequest(req)) {
|
188
|
+
localSetSecureHeaders(res)
|
189
|
+
await handleApiRequest(req, res, path)
|
190
|
+
} else {
|
191
|
+
throw new Error('Unexpected request type in Pages API handler')
|
192
|
+
}
|
193
|
+
} catch (error) {
|
194
|
+
logger.error('Error in Pages API handler:', error)
|
195
|
+
res.status(500).json({ error: 'Internal server error', details: (error as Error).message })
|
196
|
+
}
|
197
|
+
})
|
198
|
+
}
|
199
|
+
|
200
|
+
async function handleRequest(req: NextRequest, path: string): Promise<NextResponse> {
|
201
|
+
if (shouldSimulateError()) {
|
202
|
+
throw new Error('Simulated server error')
|
203
|
+
}
|
204
|
+
|
205
|
+
for (const middleware of middlewarePipeline) {
|
206
|
+
await middleware(req as unknown as AnyRequest, NextResponse.next() as unknown as AnyResponse)
|
207
|
+
}
|
208
|
+
|
209
|
+
const cacheKey = `${req.method}:${path}:${JSON.stringify(Object.fromEntries(req.nextUrl.searchParams))}:${await req.text()}`
|
210
|
+
const cachedResponse = getCachedResponse(cacheKey)
|
211
|
+
|
212
|
+
if (cachedResponse) {
|
213
|
+
return NextResponse.json(cachedResponse.data, { status: cachedResponse.status || 200 })
|
214
|
+
}
|
215
|
+
|
216
|
+
const { pathname } = new URL(req.url)
|
217
|
+
const matchFn = match(path, { decode: decodeURIComponent })
|
218
|
+
|
219
|
+
if (pathname === null) {
|
220
|
+
return NextResponse.json({ error: 'Invalid URL' }, { status: 400 })
|
221
|
+
}
|
222
|
+
|
223
|
+
const matched = matchFn(pathname)
|
224
|
+
|
225
|
+
if (!matched) {
|
226
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
227
|
+
}
|
228
|
+
|
229
|
+
let mockResponse: MockResponse<unknown>
|
230
|
+
const mockConfigItem = mockConfig[path]
|
231
|
+
|
232
|
+
if (typeof mockConfigItem === 'function') {
|
233
|
+
mockResponse = await (mockConfigItem as MockFunction)(req as unknown as AnyRequest)
|
234
|
+
} else {
|
235
|
+
mockResponse = mockConfigItem as MockResponse<unknown>
|
236
|
+
}
|
237
|
+
|
238
|
+
if (!mockResponse) {
|
239
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
240
|
+
}
|
241
|
+
|
242
|
+
mockResponse = await applyMiddleware(req as unknown as AnyRequest, mockResponse, mockInterceptor)
|
243
|
+
|
244
|
+
if (requestLogger) {
|
245
|
+
requestLogger(req as unknown as AnyRequest, NextResponse.json({}, { status: mockResponse.status || 200 }) as unknown as AnyResponse)
|
246
|
+
}
|
247
|
+
|
248
|
+
const delay = mockResponse.delay || globalDelay
|
249
|
+
if (delay) {
|
250
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
251
|
+
}
|
252
|
+
|
253
|
+
eventEmitter.emit('mockResponse', path, mockResponse)
|
254
|
+
collectMetrics(req as unknown as AnyRequest, NextResponse.json({}, { status: mockResponse.status || 200 }) as unknown as AnyResponse, mockResponse)
|
255
|
+
|
256
|
+
cacheResponse(cacheKey, mockResponse, mockOptions.cacheTimeout)
|
257
|
+
|
258
|
+
return NextResponse.json(mockResponse.data, {
|
259
|
+
status: mockResponse.status || 200,
|
260
|
+
headers: mockResponse.headers,
|
261
|
+
})
|
262
|
+
}
|
263
|
+
|
264
|
+
async function handleApiRequest(req: NextApiRequest, res: NextApiResponse, path: string): Promise<void> {
|
265
|
+
if (shouldSimulateError()) {
|
266
|
+
throw new Error('Simulated server error')
|
267
|
+
}
|
268
|
+
|
269
|
+
for (const middleware of middlewarePipeline) {
|
270
|
+
await middleware(req, res)
|
271
|
+
}
|
272
|
+
|
273
|
+
const cacheKey = `${req.method}:${path}:${JSON.stringify(req.query)}:${JSON.stringify(req.body)}`
|
274
|
+
const cachedResponse = getCachedResponse(cacheKey)
|
275
|
+
|
276
|
+
if (cachedResponse) {
|
277
|
+
res.status(cachedResponse.status || 200).json(cachedResponse.data)
|
278
|
+
return
|
279
|
+
}
|
280
|
+
|
281
|
+
const { pathname } = parse(req.url || '', true)
|
282
|
+
const matchFn = match(path, { decode: decodeURIComponent })
|
283
|
+
|
284
|
+
if (pathname === null) {
|
285
|
+
res.status(400).json({ error: 'Invalid URL' })
|
286
|
+
return
|
287
|
+
}
|
288
|
+
|
289
|
+
const matched = matchFn(pathname)
|
290
|
+
|
291
|
+
if (!matched) {
|
292
|
+
res.status(404).json({ error: 'Not found' })
|
293
|
+
return
|
294
|
+
}
|
295
|
+
|
296
|
+
let mockResponse: MockResponse<unknown>
|
297
|
+
const mockConfigItem = mockConfig[path]
|
298
|
+
|
299
|
+
if (typeof mockConfigItem === 'function') {
|
300
|
+
mockResponse = await (mockConfigItem as MockFunction)(req)
|
301
|
+
} else {
|
302
|
+
mockResponse = mockConfigItem as MockResponse<unknown>
|
303
|
+
}
|
304
|
+
|
305
|
+
if (!mockResponse) {
|
306
|
+
res.status(404).json({ error: 'Not found' })
|
307
|
+
return
|
308
|
+
}
|
309
|
+
|
310
|
+
mockResponse = await applyMiddleware(req, mockResponse, mockInterceptor)
|
311
|
+
|
312
|
+
if (requestLogger) {
|
313
|
+
requestLogger(req, res)
|
314
|
+
}
|
315
|
+
|
316
|
+
const delay = mockResponse.delay || globalDelay
|
317
|
+
if (delay) {
|
318
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
319
|
+
}
|
320
|
+
|
321
|
+
if (mockResponse.headers) {
|
322
|
+
Object.entries(mockResponse.headers).forEach(([key, value]) => {
|
323
|
+
res.setHeader(key, value as string)
|
324
|
+
})
|
325
|
+
}
|
326
|
+
|
327
|
+
eventEmitter.emit('mockResponse', path, mockResponse)
|
328
|
+
collectMetrics(req, res, mockResponse)
|
329
|
+
|
330
|
+
cacheResponse(cacheKey, mockResponse, mockOptions.cacheTimeout)
|
331
|
+
|
332
|
+
res.status(mockResponse.status || 200).json(mockResponse.data)
|
333
|
+
}
|
334
|
+
|
335
|
+
function isNextApiRequest(req: AnyRequest): req is NextApiRequest {
|
336
|
+
return typeGuardIsNextApiRequest(req)
|
337
|
+
}
|
338
|
+
|
339
|
+
function isNextApiResponse(res: AnyResponse): res is NextApiResponse {
|
340
|
+
return 'status' in res && typeof res.status === 'function'
|
341
|
+
}
|
342
|
+
|
343
|
+
function setSecureHeaders(reqOrRes: NextRequest | NextApiResponse): NextRequest | void {
|
344
|
+
if (reqOrRes instanceof NextRequest) {
|
345
|
+
// For NextRequest, we can't set headers directly, but we can create a new request with added headers
|
346
|
+
const headers = new Headers(reqOrRes.headers)
|
347
|
+
headers.set('X-Secure-Header', 'true')
|
348
|
+
return new NextRequest(reqOrRes.url, {
|
349
|
+
headers,
|
350
|
+
method: reqOrRes.method,
|
351
|
+
body: reqOrRes.body,
|
352
|
+
cache: reqOrRes.cache,
|
353
|
+
credentials: reqOrRes.credentials,
|
354
|
+
integrity: reqOrRes.integrity,
|
355
|
+
keepalive: reqOrRes.keepalive,
|
356
|
+
mode: reqOrRes.mode,
|
357
|
+
redirect: reqOrRes.redirect,
|
358
|
+
referrer: reqOrRes.referrer,
|
359
|
+
referrerPolicy: reqOrRes.referrerPolicy,
|
360
|
+
signal: reqOrRes.signal,
|
361
|
+
})
|
362
|
+
} else {
|
363
|
+
// Handle NextApiResponse (Pages Router)
|
364
|
+
localSetSecureHeaders(reqOrRes)
|
365
|
+
}
|
366
|
+
}
|
367
|
+
|
368
|
+
export {
|
369
|
+
createMockDatabase,
|
370
|
+
createGraphQLMockHandler,
|
371
|
+
configureServerMocks,
|
372
|
+
resetServerMocks,
|
373
|
+
mockServerAction,
|
374
|
+
createServerComponentMock,
|
375
|
+
registerPlugin,
|
376
|
+
reportMetrics,
|
377
|
+
createRequestValidator,
|
378
|
+
setSecureHeaders
|
379
|
+
}
|
380
|
+
|
package/src/index.ts
CHANGED
@@ -1,210 +1,47 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
import { match } from 'path-to-regexp'
|
4
|
-
import { EventEmitter } from 'events'
|
5
|
-
import * as winston from 'winston'
|
6
|
-
import config from 'config'
|
7
|
-
import { MockResponse, MockConfig, RequestLogger, MockInterceptor, MockOptions, MiddlewareFunction, MockFunction } from './types'
|
8
|
-
import { createMockDatabase } from './mockDatabase'
|
9
|
-
import { createGraphQLMockHandler } from './graphqlMock'
|
10
|
-
import { validateConfig } from './configValidator'
|
11
|
-
import { applyMiddleware } from './middleware'
|
12
|
-
import { cacheResponse, getCachedResponse, clearCache } from './cache'
|
13
|
-
import { configureServerMocks, resetServerMocks, mockServerAction, createServerComponentMock } from './serverMock'
|
14
|
-
import { initializePlugins, registerPlugin } from './plugins'
|
15
|
-
import { setSecureHeaders } from './security'
|
16
|
-
import { collectMetrics, reportMetrics } from './monitoring'
|
17
|
-
import { createRequestValidator } from './validation'
|
18
|
-
import { createRateLimiter } from './rateLimit'
|
1
|
+
// Core functionality
|
2
|
+
export { configureMocks, configureMocksAsync, resetMocks, setGlobalDelay, setRequestLogger, setMockInterceptor, onMockResponse, addMiddleware, createMockHandler } from './core'
|
19
3
|
|
20
|
-
|
21
|
-
|
22
|
-
format: winston.format.json(),
|
23
|
-
defaultMeta: { service: 'mock-library' },
|
24
|
-
transports: [
|
25
|
-
new winston.transports.Console(),
|
26
|
-
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
27
|
-
new winston.transports.File({ filename: 'combined.log' }),
|
28
|
-
],
|
29
|
-
})
|
4
|
+
// Server-side mocking
|
5
|
+
export { configureServerMocks, resetServerMocks, mockServerAction, createServerComponentMock } from './serverMock'
|
30
6
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
35
|
-
defaultDelay: 0,
|
36
|
-
errorRate: 0,
|
37
|
-
}
|
7
|
+
// Database and GraphQL mocking
|
8
|
+
export { createMockDatabase } from './mockDatabase'
|
9
|
+
export { createGraphQLMockHandler } from './graphqlMock'
|
38
10
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
let requestLogger: RequestLogger | null = null
|
43
|
-
let mockInterceptor: MockInterceptor | null = null
|
44
|
-
const eventEmitter = new EventEmitter()
|
45
|
-
const middlewarePipeline: MiddlewareFunction[] = []
|
11
|
+
// Plugins and metrics
|
12
|
+
export { registerPlugin } from './plugins'
|
13
|
+
export { reportMetrics } from './monitoring'
|
46
14
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
configureMocks(loadedConfig, options)
|
51
|
-
} catch (error) {
|
52
|
-
logger.error('Error loading configuration:', error)
|
53
|
-
throw new Error('Failed to load mock configuration')
|
54
|
-
}
|
55
|
-
}
|
15
|
+
// Validation and security
|
16
|
+
export { createRequestValidator } from './validation'
|
17
|
+
export { setSecureHeaders } from './security'
|
56
18
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
} catch (error) {
|
70
|
-
logger.error('Error configuring mocks:', error)
|
71
|
-
throw new Error('Failed to configure mocks')
|
72
|
-
}
|
73
|
-
}
|
19
|
+
// Types
|
20
|
+
export type {
|
21
|
+
MockResponse,
|
22
|
+
MockConfig,
|
23
|
+
RequestLogger,
|
24
|
+
MockInterceptor,
|
25
|
+
MockOptions,
|
26
|
+
MiddlewareFunction,
|
27
|
+
MockFunction,
|
28
|
+
AnyRequest,
|
29
|
+
AnyResponse
|
30
|
+
} from './types'
|
74
31
|
|
75
|
-
|
76
|
-
|
77
|
-
mockOptions = { ...defaultOptions }
|
78
|
-
globalDelay = 0
|
79
|
-
requestLogger = null
|
80
|
-
mockInterceptor = null
|
81
|
-
clearCache()
|
82
|
-
resetServerMocks()
|
83
|
-
}
|
32
|
+
// Version check utilities
|
33
|
+
export { isAppRouter, isNextJs12, isNextJs13, isNextJs14, isNextJs15, getNextVersion } from './utils/versionCheck'
|
84
34
|
|
85
|
-
|
86
|
-
|
87
|
-
}
|
35
|
+
// Config validation
|
36
|
+
export { validateConfig } from './configValidator'
|
88
37
|
|
89
|
-
|
90
|
-
|
91
|
-
}
|
38
|
+
// Middleware
|
39
|
+
export { applyMiddleware } from './middleware'
|
92
40
|
|
93
|
-
|
94
|
-
|
95
|
-
}
|
41
|
+
// Caching
|
42
|
+
export { cacheResponse, getCachedResponse, clearCache } from './cache'
|
96
43
|
|
97
|
-
|
98
|
-
|
99
|
-
return () => eventEmitter.off('mockResponse', listener)
|
100
|
-
}
|
44
|
+
// Rate limiting
|
45
|
+
export { createRateLimiter } from './rateLimit'
|
101
46
|
|
102
|
-
export function addMiddleware(middleware: MiddlewareFunction): void {
|
103
|
-
middlewarePipeline.push(middleware)
|
104
|
-
}
|
105
|
-
|
106
|
-
function defaultLogger(req: NextApiRequest, res: NextApiResponse): void {
|
107
|
-
logger.info(`${req.method} ${req.url} - Status: ${res.statusCode}`)
|
108
|
-
}
|
109
|
-
|
110
|
-
function shouldSimulateError(): boolean {
|
111
|
-
return Math.random() < mockOptions.errorRate
|
112
|
-
}
|
113
|
-
|
114
|
-
const limiter = createRateLimiter({
|
115
|
-
windowMs: 15 * 60 * 1000, // 15 minutes
|
116
|
-
max: 100 // limit each IP to 100 requests per windowMs
|
117
|
-
})
|
118
|
-
|
119
|
-
export function createMockHandler(path: string) {
|
120
|
-
return limiter(async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
121
|
-
try {
|
122
|
-
setSecureHeaders(res)
|
123
|
-
|
124
|
-
if (shouldSimulateError()) {
|
125
|
-
throw new Error('Simulated server error')
|
126
|
-
}
|
127
|
-
|
128
|
-
for (const middleware of middlewarePipeline) {
|
129
|
-
await middleware(req, res)
|
130
|
-
}
|
131
|
-
|
132
|
-
const cacheKey = `${req.method}:${path}:${JSON.stringify(req.query)}:${JSON.stringify(req.body)}`
|
133
|
-
const cachedResponse = getCachedResponse(cacheKey)
|
134
|
-
|
135
|
-
if (cachedResponse) {
|
136
|
-
res.status(cachedResponse.status || 200).json(cachedResponse.data)
|
137
|
-
return
|
138
|
-
}
|
139
|
-
|
140
|
-
const { pathname } = parse(req.url || '', true)
|
141
|
-
const matchFn = match(path, { decode: decodeURIComponent })
|
142
|
-
|
143
|
-
if (pathname === null) {
|
144
|
-
res.status(400).json({ error: 'Invalid URL' })
|
145
|
-
return
|
146
|
-
}
|
147
|
-
|
148
|
-
const matched = matchFn(pathname)
|
149
|
-
|
150
|
-
if (!matched) {
|
151
|
-
res.status(404).json({ error: 'Not found' })
|
152
|
-
return
|
153
|
-
}
|
154
|
-
|
155
|
-
let mockResponse: MockResponse<unknown>
|
156
|
-
const mockConfigItem = mockConfig[path]
|
157
|
-
|
158
|
-
if (typeof mockConfigItem === 'function') {
|
159
|
-
mockResponse = await (mockConfigItem as MockFunction)(req)
|
160
|
-
} else {
|
161
|
-
mockResponse = mockConfigItem as MockResponse<unknown>
|
162
|
-
}
|
163
|
-
|
164
|
-
if (!mockResponse) {
|
165
|
-
res.status(404).json({ error: 'Not found' })
|
166
|
-
return
|
167
|
-
}
|
168
|
-
|
169
|
-
mockResponse = await applyMiddleware(req, mockResponse, mockInterceptor)
|
170
|
-
|
171
|
-
if (requestLogger) {
|
172
|
-
requestLogger(req, res)
|
173
|
-
}
|
174
|
-
|
175
|
-
const delay = mockResponse.delay || globalDelay
|
176
|
-
if (delay) {
|
177
|
-
await new Promise(resolve => setTimeout(resolve, delay))
|
178
|
-
}
|
179
|
-
|
180
|
-
if (mockResponse.headers) {
|
181
|
-
Object.entries(mockResponse.headers).forEach(([key, value]) => {
|
182
|
-
res.setHeader(key, value as string)
|
183
|
-
})
|
184
|
-
}
|
185
|
-
|
186
|
-
eventEmitter.emit('mockResponse', path, mockResponse)
|
187
|
-
collectMetrics(req, res, mockResponse)
|
188
|
-
|
189
|
-
cacheResponse(cacheKey, mockResponse, mockOptions.cacheTimeout)
|
190
|
-
|
191
|
-
res.status(mockResponse.status || 200).json(mockResponse.data)
|
192
|
-
} catch (error) {
|
193
|
-
logger.error('Error in mock handler:', error)
|
194
|
-
res.status(500).json({ error: 'Internal server error', details: (error as Error).message })
|
195
|
-
}
|
196
|
-
})
|
197
|
-
}
|
198
|
-
|
199
|
-
export {
|
200
|
-
createMockDatabase,
|
201
|
-
createGraphQLMockHandler,
|
202
|
-
configureServerMocks,
|
203
|
-
resetServerMocks,
|
204
|
-
mockServerAction,
|
205
|
-
createServerComponentMock,
|
206
|
-
registerPlugin,
|
207
|
-
reportMetrics,
|
208
|
-
createRequestValidator
|
209
|
-
}
|
210
47
|
|