next-api-mock 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc.js +19 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/misc.xml +17 -0
- package/.idea/modules.xml +8 -0
- package/.idea/next-api-mock.iml +9 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +34 -0
- package/__tests__/configValidator.test.ts +69 -0
- package/__tests__/graphqlMock.test.ts +53 -0
- package/dist/cache.d.ts +19 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +37 -0
- package/dist/configValidator.d.ts +8 -0
- package/dist/configValidator.d.ts.map +1 -0
- package/dist/configValidator.js +23 -0
- package/dist/graphqlMock.d.ts +5 -0
- package/dist/graphqlMock.d.ts.map +1 -0
- package/dist/graphqlMock.js +49 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +242 -0
- package/dist/middleware.d.ts +4 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +20 -0
- package/dist/mockDatabase.d.ts +12 -0
- package/dist/mockDatabase.d.ts.map +1 -0
- package/dist/mockDatabase.js +35 -0
- package/dist/monitoring.d.ts +15 -0
- package/dist/monitoring.d.ts.map +1 -0
- package/dist/monitoring.js +86 -0
- package/dist/plugins.d.ts +12 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +19 -0
- package/dist/security.d.ts +7 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +40 -0
- package/dist/serverMock.d.ts +11 -0
- package/dist/serverMock.d.ts.map +1 -0
- package/dist/serverMock.js +115 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +19 -0
- package/jest.config.js +13 -0
- package/package.json +57 -0
- package/src/cache.ts +36 -0
- package/src/configValidator.ts +23 -0
- package/src/graphqlMock.ts +44 -0
- package/src/index.ts +210 -0
- package/src/middleware.ts +14 -0
- package/src/mockDatabase.ts +33 -0
- package/src/monitoring.ts +47 -0
- package/src/plugins.ts +16 -0
- package/src/rateLimit.ts +26 -0
- package/src/security.ts +42 -0
- package/src/serverMock.ts +127 -0
- package/src/types.ts +44 -0
- package/src/validation.ts +19 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
import { NextApiRequest } from 'next'
|
2
|
+
import { graphql, GraphQLSchema } from 'graphql'
|
3
|
+
import { MockResponse } from './types'
|
4
|
+
|
5
|
+
export function createGraphQLMockHandler(schema: GraphQLSchema) {
|
6
|
+
return async (req: NextApiRequest): Promise<MockResponse> => {
|
7
|
+
if (req.method !== 'POST') {
|
8
|
+
return {
|
9
|
+
status: 405,
|
10
|
+
data: { errors: [{ message: 'Method not allowed' }] }
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
const { query, variables, operationName } = req.body
|
15
|
+
|
16
|
+
if (!query) {
|
17
|
+
return {
|
18
|
+
status: 400,
|
19
|
+
data: { errors: [{ message: 'Query is required' }] }
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
try {
|
24
|
+
const result = await graphql({
|
25
|
+
schema,
|
26
|
+
source: query,
|
27
|
+
variableValues: variables,
|
28
|
+
operationName
|
29
|
+
})
|
30
|
+
|
31
|
+
return {
|
32
|
+
status: 200,
|
33
|
+
data: result
|
34
|
+
}
|
35
|
+
} catch (error) {
|
36
|
+
console.error('GraphQL execution error:', error)
|
37
|
+
return {
|
38
|
+
status: 500,
|
39
|
+
data: { errors: [{ message: 'Internal server error' }] }
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
package/src/index.ts
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next'
|
2
|
+
import { parse } from 'url'
|
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'
|
19
|
+
|
20
|
+
const logger = winston.createLogger({
|
21
|
+
level: config.get<string>('logLevel') || 'info',
|
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
|
+
})
|
30
|
+
|
31
|
+
const defaultConfig: MockConfig = {}
|
32
|
+
const defaultOptions: MockOptions = {
|
33
|
+
enableLogging: false,
|
34
|
+
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
35
|
+
defaultDelay: 0,
|
36
|
+
errorRate: 0,
|
37
|
+
}
|
38
|
+
|
39
|
+
let mockConfig: MockConfig = { ...defaultConfig }
|
40
|
+
let mockOptions: MockOptions = { ...defaultOptions }
|
41
|
+
let globalDelay = 0
|
42
|
+
let requestLogger: RequestLogger | null = null
|
43
|
+
let mockInterceptor: MockInterceptor | null = null
|
44
|
+
const eventEmitter = new EventEmitter()
|
45
|
+
const middlewarePipeline: MiddlewareFunction[] = []
|
46
|
+
|
47
|
+
export async function configureMocksAsync(configPromise: Promise<MockConfig>, options?: Partial<MockOptions>): Promise<void> {
|
48
|
+
try {
|
49
|
+
const loadedConfig = await configPromise
|
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
|
+
}
|
56
|
+
|
57
|
+
export function configureMocks(loadedConfig: MockConfig, options?: Partial<MockOptions>): void {
|
58
|
+
try {
|
59
|
+
validateConfig(loadedConfig)
|
60
|
+
mockConfig = { ...defaultConfig, ...loadedConfig }
|
61
|
+
mockOptions = { ...defaultOptions, ...options }
|
62
|
+
if (mockOptions.enableLogging) {
|
63
|
+
setRequestLogger(defaultLogger)
|
64
|
+
}
|
65
|
+
if (mockOptions.defaultDelay) {
|
66
|
+
setGlobalDelay(mockOptions.defaultDelay)
|
67
|
+
}
|
68
|
+
initializePlugins(mockConfig)
|
69
|
+
} catch (error) {
|
70
|
+
logger.error('Error configuring mocks:', error)
|
71
|
+
throw new Error('Failed to configure mocks')
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
export function resetMocks(): void {
|
76
|
+
mockConfig = { ...defaultConfig }
|
77
|
+
mockOptions = { ...defaultOptions }
|
78
|
+
globalDelay = 0
|
79
|
+
requestLogger = null
|
80
|
+
mockInterceptor = null
|
81
|
+
clearCache()
|
82
|
+
resetServerMocks()
|
83
|
+
}
|
84
|
+
|
85
|
+
export function setGlobalDelay(delay: number): void {
|
86
|
+
globalDelay = delay
|
87
|
+
}
|
88
|
+
|
89
|
+
export function setRequestLogger(logger: RequestLogger): void {
|
90
|
+
requestLogger = logger
|
91
|
+
}
|
92
|
+
|
93
|
+
export function setMockInterceptor(interceptor: MockInterceptor): void {
|
94
|
+
mockInterceptor = interceptor
|
95
|
+
}
|
96
|
+
|
97
|
+
export function onMockResponse(listener: (path: string, response: MockResponse<unknown>) => void): () => void {
|
98
|
+
eventEmitter.on('mockResponse', listener)
|
99
|
+
return () => eventEmitter.off('mockResponse', listener)
|
100
|
+
}
|
101
|
+
|
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
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { NextApiRequest } from 'next'
|
2
|
+
import { MockResponse, MockInterceptor } from './types'
|
3
|
+
|
4
|
+
export async function applyMiddleware(
|
5
|
+
req: NextApiRequest,
|
6
|
+
response: MockResponse,
|
7
|
+
interceptor: MockInterceptor | null
|
8
|
+
): Promise<MockResponse> {
|
9
|
+
if (interceptor) {
|
10
|
+
return await interceptor(req, response)
|
11
|
+
}
|
12
|
+
return response
|
13
|
+
}
|
14
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
export function createMockDatabase<T extends { id: string | number }>() {
|
2
|
+
let data: T[] = []
|
3
|
+
|
4
|
+
return {
|
5
|
+
getAll: () => [...data],
|
6
|
+
getById: (id: string | number) => data.find(item => item.id === id),
|
7
|
+
create: (item: T) => {
|
8
|
+
data.push(item)
|
9
|
+
return item
|
10
|
+
},
|
11
|
+
update: (id: string | number, updates: Partial<T>) => {
|
12
|
+
const index = data.findIndex(item => item.id === id)
|
13
|
+
if (index !== -1) {
|
14
|
+
data[index] = { ...data[index], ...updates }
|
15
|
+
return data[index]
|
16
|
+
}
|
17
|
+
return null
|
18
|
+
},
|
19
|
+
delete: (id: string | number) => {
|
20
|
+
const index = data.findIndex(item => item.id === id)
|
21
|
+
if (index !== -1) {
|
22
|
+
const deleted = data[index]
|
23
|
+
data.splice(index, 1)
|
24
|
+
return deleted
|
25
|
+
}
|
26
|
+
return null
|
27
|
+
},
|
28
|
+
reset: () => {
|
29
|
+
data = []
|
30
|
+
},
|
31
|
+
query: (predicate: (item: T) => boolean) => data.filter(predicate)
|
32
|
+
}
|
33
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import * as prometheus from 'prom-client'
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next'
|
3
|
+
import { MockResponse } from './types'
|
4
|
+
|
5
|
+
const requestCounter = new prometheus.Counter({
|
6
|
+
name: 'mock_requests_total',
|
7
|
+
help: 'Total number of mock requests',
|
8
|
+
labelNames: ['method', 'path', 'status'],
|
9
|
+
})
|
10
|
+
|
11
|
+
const responseTimeHistogram = new prometheus.Histogram({
|
12
|
+
name: 'mock_response_time_seconds',
|
13
|
+
help: 'Response time in seconds',
|
14
|
+
labelNames: ['method', 'path'],
|
15
|
+
})
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Collects metrics for a mock request.
|
19
|
+
* @param req The incoming request.
|
20
|
+
* @param res The outgoing response.
|
21
|
+
* @param mockResponse The mock response.
|
22
|
+
*/
|
23
|
+
export function collectMetrics(req: NextApiRequest, res: NextApiResponse, mockResponse: MockResponse): void {
|
24
|
+
const labels = {
|
25
|
+
method: req.method || 'UNKNOWN',
|
26
|
+
path: req.url || 'UNKNOWN',
|
27
|
+
status: mockResponse.status.toString(),
|
28
|
+
}
|
29
|
+
|
30
|
+
requestCounter.inc(labels)
|
31
|
+
|
32
|
+
const responseTime = process.hrtime()
|
33
|
+
res.on('finish', () => {
|
34
|
+
const [seconds, nanoseconds] = process.hrtime(responseTime)
|
35
|
+
const duration = seconds + nanoseconds / 1e9
|
36
|
+
responseTimeHistogram.observe({ method: labels.method, path: labels.path }, duration)
|
37
|
+
})
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Reports the collected metrics.
|
42
|
+
* @returns A Promise that resolves to a string representation of the collected metrics.
|
43
|
+
*/
|
44
|
+
export async function reportMetrics(): Promise<string> {
|
45
|
+
return await prometheus.register.metrics()
|
46
|
+
}
|
47
|
+
|
package/src/plugins.ts
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
import { MockConfig, Plugin } from './types'
|
2
|
+
const plugins: Plugin[] = []
|
3
|
+
/**
|
4
|
+
* Registers a new plugin.
|
5
|
+
* @param plugin The plugin to register.
|
6
|
+
*/
|
7
|
+
export function registerPlugin(plugin: Plugin): void {
|
8
|
+
plugins.push(plugin)
|
9
|
+
}
|
10
|
+
/**
|
11
|
+
* Initializes all registered plugins with the provided configuration.
|
12
|
+
* @param config The mock configuration.
|
13
|
+
*/
|
14
|
+
export function initializePlugins(config: MockConfig): void {
|
15
|
+
plugins.forEach(plugin => plugin.initialize(config))
|
16
|
+
}
|
package/src/rateLimit.ts
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next'
|
2
|
+
import { rateLimit, Options } from 'express-rate-limit'
|
3
|
+
import { Request, Response, NextFunction } from 'express'
|
4
|
+
|
5
|
+
type NextApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise<void>
|
6
|
+
|
7
|
+
export function createRateLimiter(options: Partial<Options>) {
|
8
|
+
const limiter = rateLimit(options)
|
9
|
+
|
10
|
+
return function rateLimitMiddleware(handler: NextApiHandler): NextApiHandler {
|
11
|
+
return async (req: NextApiRequest, res: NextApiResponse) => {
|
12
|
+
const expressReq = req as unknown as Request
|
13
|
+
const expressRes = res as unknown as Response
|
14
|
+
|
15
|
+
return new Promise<void>((resolve, reject) => {
|
16
|
+
limiter(expressReq, expressRes, (result: any) => {
|
17
|
+
if (result instanceof Error) {
|
18
|
+
return reject(result)
|
19
|
+
}
|
20
|
+
return resolve(handler(req, res))
|
21
|
+
})
|
22
|
+
})
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
package/src/security.ts
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
import { NextApiResponse } from 'next'
|
2
|
+
import helmet from 'helmet'
|
3
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Sets secure headers on the response using helmet.
|
7
|
+
* @param res The Next.js API response object.
|
8
|
+
*/
|
9
|
+
export function setSecureHeaders(res: NextApiResponse): void {
|
10
|
+
const helmetMiddleware = helmet({
|
11
|
+
contentSecurityPolicy: {
|
12
|
+
directives: {
|
13
|
+
defaultSrc: ["'self'"],
|
14
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
15
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
16
|
+
imgSrc: ["'self'", "data:", "https:"],
|
17
|
+
},
|
18
|
+
},
|
19
|
+
referrerPolicy: {
|
20
|
+
policy: 'strict-origin-when-cross-origin',
|
21
|
+
},
|
22
|
+
})
|
23
|
+
|
24
|
+
// Create a mock request object
|
25
|
+
const mockReq = {} as IncomingMessage
|
26
|
+
|
27
|
+
// Cast NextApiResponse to ServerResponse
|
28
|
+
const nodeRes = res as unknown as ServerResponse
|
29
|
+
|
30
|
+
// Define a named function to handle potential errors
|
31
|
+
const handleHelmetError = (err?: unknown): void => {
|
32
|
+
if (err) {
|
33
|
+
console.error('Helmet middleware error:', err)
|
34
|
+
// You might want to set a default security header if Helmet fails
|
35
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
// Apply helmet middleware with error handling
|
40
|
+
helmetMiddleware(mockReq, nodeRes, handleHelmetError)
|
41
|
+
}
|
42
|
+
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
2
|
+
import { MockResponse, MockConfig, ServerComponentMock } from './types'
|
3
|
+
|
4
|
+
let serverMockConfig: MockConfig = {}
|
5
|
+
let mockBaseUrl = 'https://example.com'
|
6
|
+
|
7
|
+
export function configureServerMocks(config: MockConfig, baseUrl?: string): void {
|
8
|
+
serverMockConfig = { ...serverMockConfig, ...config }
|
9
|
+
if (baseUrl) {
|
10
|
+
mockBaseUrl = baseUrl
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
export function resetServerMocks(): void {
|
15
|
+
serverMockConfig = {}
|
16
|
+
mockBaseUrl = 'https://example.com'
|
17
|
+
}
|
18
|
+
|
19
|
+
export async function mockServerAction<TArgs extends unknown[], TReturn>(
|
20
|
+
actionName: string,
|
21
|
+
...args: TArgs
|
22
|
+
): Promise<TReturn> {
|
23
|
+
const mockConfig = serverMockConfig[actionName]
|
24
|
+
if (typeof mockConfig === 'function') {
|
25
|
+
const mockRequest = await createMockNextRequest({
|
26
|
+
method: 'POST',
|
27
|
+
body: JSON.stringify(args),
|
28
|
+
})
|
29
|
+
|
30
|
+
const result = await mockConfig(mockRequest)
|
31
|
+
return result.data as TReturn
|
32
|
+
} else if (mockConfig) {
|
33
|
+
return mockConfig.data as TReturn
|
34
|
+
}
|
35
|
+
throw new Error(`No mock configured for server action: ${actionName}`)
|
36
|
+
}
|
37
|
+
|
38
|
+
export function createServerComponentMock<TProps>(
|
39
|
+
componentName: string
|
40
|
+
): ServerComponentMock<TProps, Promise<NextResponse>> {
|
41
|
+
return async (props: TProps): Promise<NextResponse> => {
|
42
|
+
const mockConfig = serverMockConfig[componentName]
|
43
|
+
if (typeof mockConfig === 'function') {
|
44
|
+
const searchParams = new URLSearchParams(props as Record<string, string>)
|
45
|
+
const mockRequest = await createMockNextRequest({
|
46
|
+
method: 'GET',
|
47
|
+
url: `${mockBaseUrl}?${searchParams.toString()}`
|
48
|
+
})
|
49
|
+
|
50
|
+
const mockResponse = await mockConfig(mockRequest)
|
51
|
+
return createMockNextResponse(mockResponse)
|
52
|
+
} else if (mockConfig) {
|
53
|
+
return createMockNextResponse(mockConfig as MockResponse)
|
54
|
+
}
|
55
|
+
throw new Error(`No mock configured for server component: ${componentName}`)
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
async function createMockNextRequest(options: {
|
60
|
+
method?: string;
|
61
|
+
url?: string;
|
62
|
+
headers?: HeadersInit;
|
63
|
+
body?: BodyInit;
|
64
|
+
}): Promise<NextRequest> {
|
65
|
+
const url = options.url || mockBaseUrl
|
66
|
+
|
67
|
+
const init: RequestInit = {
|
68
|
+
method: options.method || 'GET',
|
69
|
+
headers: options.headers,
|
70
|
+
body: options.body,
|
71
|
+
}
|
72
|
+
|
73
|
+
const request = new Request(url, init)
|
74
|
+
const nextRequest = new NextRequest(request)
|
75
|
+
|
76
|
+
Object.defineProperties(nextRequest, {
|
77
|
+
geo: {
|
78
|
+
value: {
|
79
|
+
city: 'Mock City',
|
80
|
+
country: 'Mock Country',
|
81
|
+
region: 'Mock Region',
|
82
|
+
latitude: '0',
|
83
|
+
longitude: '0'
|
84
|
+
},
|
85
|
+
writable: true
|
86
|
+
},
|
87
|
+
ip: {
|
88
|
+
value: '127.0.0.1',
|
89
|
+
writable: true
|
90
|
+
}
|
91
|
+
})
|
92
|
+
|
93
|
+
return nextRequest
|
94
|
+
}
|
95
|
+
|
96
|
+
function createMockNextResponse(mockResponse: MockResponse): NextResponse {
|
97
|
+
const { data, status = 200, headers = {} } = mockResponse
|
98
|
+
|
99
|
+
const bodyContent = typeof data === 'string' ? data : JSON.stringify(data)
|
100
|
+
|
101
|
+
const stream = new ReadableStream({
|
102
|
+
start(controller) {
|
103
|
+
controller.enqueue(new TextEncoder().encode(bodyContent))
|
104
|
+
controller.close()
|
105
|
+
}
|
106
|
+
})
|
107
|
+
|
108
|
+
const response = new Response(stream, {
|
109
|
+
status,
|
110
|
+
headers: new Headers(headers)
|
111
|
+
})
|
112
|
+
|
113
|
+
return new NextResponse(response.body, {
|
114
|
+
status,
|
115
|
+
headers: new Headers(headers),
|
116
|
+
url: mockBaseUrl
|
117
|
+
})
|
118
|
+
}
|
119
|
+
|
120
|
+
export interface TypedMockResponse<T> extends MockResponse {
|
121
|
+
data: T
|
122
|
+
}
|
123
|
+
|
124
|
+
export function createTypedMock<T>(response: TypedMockResponse<T>): TypedMockResponse<T> {
|
125
|
+
return response
|
126
|
+
}
|
127
|
+
|
package/src/types.ts
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next'
|
2
|
+
import { NextRequest, NextResponse } from 'next/server'
|
3
|
+
|
4
|
+
export type MockResponse<T = unknown> = {
|
5
|
+
status: number
|
6
|
+
data: T
|
7
|
+
delay?: number
|
8
|
+
headers?: Record<string, string>
|
9
|
+
}
|
10
|
+
|
11
|
+
export type MockFunction = (req: NextApiRequest | NextRequest) => MockResponse<unknown> | Promise<MockResponse<unknown>>
|
12
|
+
|
13
|
+
export type MockConfig = {
|
14
|
+
[key: string]: MockResponse<unknown> | MockFunction
|
15
|
+
}
|
16
|
+
|
17
|
+
export type RequestLogger = (req: NextApiRequest, res: NextApiResponse) => void
|
18
|
+
|
19
|
+
export type MockInterceptor = (req: NextApiRequest | NextRequest, mockResponse: MockResponse<unknown>) => MockResponse<unknown> | Promise<MockResponse<unknown>>
|
20
|
+
|
21
|
+
export type ServerActionMock<TArgs extends unknown[], TReturn> = (...args: TArgs) => TReturn
|
22
|
+
|
23
|
+
export type ServerComponentMock<TProps, TReturn = Promise<NextResponse>> = (props: TProps) => TReturn
|
24
|
+
|
25
|
+
export type MockOptions = {
|
26
|
+
enableLogging: boolean
|
27
|
+
cacheTimeout: number
|
28
|
+
defaultDelay: number
|
29
|
+
errorRate: number
|
30
|
+
}
|
31
|
+
|
32
|
+
export type MiddlewareFunction = (req: NextApiRequest, res: NextApiResponse) => Promise<void>
|
33
|
+
|
34
|
+
export type Plugin = {
|
35
|
+
name: string
|
36
|
+
initialize: (config: MockConfig) => void
|
37
|
+
beforeRequest?: (req: NextApiRequest | NextRequest) => void
|
38
|
+
afterResponse?: (req: NextApiRequest | NextRequest, response: MockResponse<unknown>) => void
|
39
|
+
errorHandler?: (error: Error, req: NextApiRequest | NextRequest) => MockResponse<unknown>
|
40
|
+
transformResponse?: (response: MockResponse<unknown>) => MockResponse<unknown>
|
41
|
+
validateConfig?: (config: MockConfig) => boolean
|
42
|
+
cleanup?: () => void
|
43
|
+
}
|
44
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import * as Joi from 'joi'
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next'
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Creates a request validator middleware using the provided Joi schema.
|
6
|
+
* @param schema The Joi schema to validate against.
|
7
|
+
* @returns A middleware function that validates incoming requests.
|
8
|
+
*/
|
9
|
+
export function createRequestValidator(schema: Joi.ObjectSchema) {
|
10
|
+
return (req: NextApiRequest, res: NextApiResponse, next: () => void) => {
|
11
|
+
const { error } = schema.validate(req.body)
|
12
|
+
if (error) {
|
13
|
+
res.status(400).json({ error: 'Invalid request', details: error.details })
|
14
|
+
} else {
|
15
|
+
next()
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
package/tsconfig.json
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "es5",
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
5
|
+
"allowJs": true,
|
6
|
+
"skipLibCheck": true,
|
7
|
+
"strict": true,
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
9
|
+
"noEmit": true,
|
10
|
+
"esModuleInterop": true,
|
11
|
+
"module": "esnext",
|
12
|
+
"moduleResolution": "node",
|
13
|
+
"resolveJsonModule": true,
|
14
|
+
"isolatedModules": true,
|
15
|
+
"jsx": "preserve",
|
16
|
+
"incremental": true,
|
17
|
+
"plugins": [
|
18
|
+
{
|
19
|
+
"name": "next"
|
20
|
+
}
|
21
|
+
],
|
22
|
+
"paths": {
|
23
|
+
"@/*": ["./*"]
|
24
|
+
}
|
25
|
+
},
|
26
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
27
|
+
"exclude": ["node_modules"]
|
28
|
+
}
|
29
|
+
|