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
package/src/middleware.ts
CHANGED
@@ -1,14 +1,32 @@
|
|
1
|
-
import {
|
2
|
-
import { MockResponse, MockInterceptor } from './types'
|
1
|
+
import { AnyRequest, AnyResponse, MockResponse, MockInterceptor, MiddlewareFunction } from './types'
|
3
2
|
|
4
3
|
export async function applyMiddleware(
|
5
|
-
req:
|
6
|
-
|
4
|
+
req: AnyRequest,
|
5
|
+
mockResponse: MockResponse<unknown>,
|
7
6
|
interceptor: MockInterceptor | null
|
8
|
-
): Promise<MockResponse
|
7
|
+
): Promise<MockResponse<unknown>> {
|
9
8
|
if (interceptor) {
|
10
|
-
return await interceptor(req,
|
9
|
+
return await interceptor(req, mockResponse)
|
11
10
|
}
|
12
|
-
return
|
11
|
+
return mockResponse
|
13
12
|
}
|
14
13
|
|
14
|
+
export function createMiddleware(fn: MiddlewareFunction): MiddlewareFunction {
|
15
|
+
return async (req: AnyRequest, res: AnyResponse) => {
|
16
|
+
await fn(req, res)
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
export const corsMiddleware = createMiddleware(async (req: AnyRequest, res: AnyResponse) => {
|
21
|
+
if ('setHeader' in res) {
|
22
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
23
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
24
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
25
|
+
}
|
26
|
+
})
|
27
|
+
|
28
|
+
export const loggingMiddleware = createMiddleware(async (req: AnyRequest, res: AnyResponse) => {
|
29
|
+
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`)
|
30
|
+
})
|
31
|
+
|
32
|
+
|
package/src/monitoring.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import * as prometheus from 'prom-client'
|
2
2
|
import { NextApiRequest, NextApiResponse } from 'next'
|
3
|
-
import {
|
3
|
+
import { NextResponse } from 'next/server'
|
4
|
+
import { AnyRequest, AnyResponse, MockResponse } from './types'
|
4
5
|
|
5
6
|
const requestCounter = new prometheus.Counter({
|
6
7
|
name: 'mock_requests_total',
|
@@ -20,21 +21,31 @@ const responseTimeHistogram = new prometheus.Histogram({
|
|
20
21
|
* @param res The outgoing response.
|
21
22
|
* @param mockResponse The mock response.
|
22
23
|
*/
|
23
|
-
export function collectMetrics(req:
|
24
|
+
export function collectMetrics(req: AnyRequest, res: AnyResponse, mockResponse: MockResponse): void {
|
24
25
|
const labels = {
|
25
26
|
method: req.method || 'UNKNOWN',
|
26
|
-
path: req.url || 'UNKNOWN',
|
27
|
-
status: mockResponse.status.toString(),
|
27
|
+
path: 'url' in req ? req.url : (req as NextApiRequest).url || 'UNKNOWN',
|
28
|
+
status: (mockResponse.status || 200).toString(),
|
28
29
|
}
|
29
30
|
|
30
31
|
requestCounter.inc(labels)
|
31
32
|
|
32
33
|
const responseTime = process.hrtime()
|
33
|
-
|
34
|
+
|
35
|
+
if ('on' in res) {
|
36
|
+
// NextApiResponse
|
37
|
+
(res as NextApiResponse).on('finish', () => {
|
38
|
+
const [seconds, nanoseconds] = process.hrtime(responseTime)
|
39
|
+
const duration = seconds + nanoseconds / 1e9
|
40
|
+
responseTimeHistogram.observe({ method: labels.method, path: labels.path }, duration)
|
41
|
+
})
|
42
|
+
} else {
|
43
|
+
// NextResponse
|
44
|
+
// For NextResponse, we can't hook into 'finish' event, so we'll record the time immediately
|
34
45
|
const [seconds, nanoseconds] = process.hrtime(responseTime)
|
35
46
|
const duration = seconds + nanoseconds / 1e9
|
36
47
|
responseTimeHistogram.observe({ method: labels.method, path: labels.path }, duration)
|
37
|
-
}
|
48
|
+
}
|
38
49
|
}
|
39
50
|
|
40
51
|
/**
|
@@ -43,5 +54,4 @@ export function collectMetrics(req: NextApiRequest, res: NextApiResponse, mockRe
|
|
43
54
|
*/
|
44
55
|
export async function reportMetrics(): Promise<string> {
|
45
56
|
return await prometheus.register.metrics()
|
46
|
-
}
|
47
|
-
|
57
|
+
}
|
package/src/serverMock.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
2
|
-
import { MockResponse, MockConfig, ServerComponentMock } from './types'
|
2
|
+
import { MockResponse, MockConfig, ServerComponentMock, NextRequestType, NextResponseType, AnyRequest } from './types'
|
3
|
+
import { isAppRouter, hasServerActions } from './utils/versionCheck'
|
3
4
|
|
4
5
|
let serverMockConfig: MockConfig = {}
|
5
6
|
let mockBaseUrl = 'https://example.com'
|
@@ -20,6 +21,10 @@ export async function mockServerAction<TArgs extends unknown[], TReturn>(
|
|
20
21
|
actionName: string,
|
21
22
|
...args: TArgs
|
22
23
|
): Promise<TReturn> {
|
24
|
+
if (!hasServerActions) {
|
25
|
+
throw new Error('Server Actions are not supported in this Next.js version')
|
26
|
+
}
|
27
|
+
|
23
28
|
const mockConfig = serverMockConfig[actionName]
|
24
29
|
if (typeof mockConfig === 'function') {
|
25
30
|
const mockRequest = await createMockNextRequest({
|
@@ -37,8 +42,12 @@ export async function mockServerAction<TArgs extends unknown[], TReturn>(
|
|
37
42
|
|
38
43
|
export function createServerComponentMock<TProps>(
|
39
44
|
componentName: string
|
40
|
-
): ServerComponentMock<TProps, Promise<
|
41
|
-
|
45
|
+
): ServerComponentMock<TProps, Promise<NextResponseType>> {
|
46
|
+
if (!isAppRouter) {
|
47
|
+
throw new Error('Server Components are not supported in this Next.js version')
|
48
|
+
}
|
49
|
+
|
50
|
+
return async (props: TProps): Promise<NextResponseType> => {
|
42
51
|
const mockConfig = serverMockConfig[componentName]
|
43
52
|
if (typeof mockConfig === 'function') {
|
44
53
|
const searchParams = new URLSearchParams(props as Record<string, string>)
|
@@ -61,7 +70,7 @@ async function createMockNextRequest(options: {
|
|
61
70
|
url?: string;
|
62
71
|
headers?: HeadersInit;
|
63
72
|
body?: BodyInit;
|
64
|
-
}): Promise<
|
73
|
+
}): Promise<NextRequestType> {
|
65
74
|
const url = options.url || mockBaseUrl
|
66
75
|
|
67
76
|
const init: RequestInit = {
|
@@ -93,27 +102,12 @@ async function createMockNextRequest(options: {
|
|
93
102
|
return nextRequest
|
94
103
|
}
|
95
104
|
|
96
|
-
function createMockNextResponse(mockResponse: MockResponse):
|
105
|
+
function createMockNextResponse(mockResponse: MockResponse): NextResponseType {
|
97
106
|
const { data, status = 200, headers = {} } = mockResponse
|
98
107
|
|
99
|
-
|
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, {
|
108
|
+
return NextResponse.json(data, {
|
114
109
|
status,
|
115
110
|
headers: new Headers(headers),
|
116
|
-
url: mockBaseUrl
|
117
111
|
})
|
118
112
|
}
|
119
113
|
|
package/src/types.ts
CHANGED
@@ -1,44 +1,54 @@
|
|
1
|
-
import { NextApiRequest, NextApiResponse } from 'next'
|
2
|
-
import { NextRequest, NextResponse } from 'next/server'
|
1
|
+
import type { NextApiRequest, NextApiResponse } from 'next'
|
2
|
+
import type { NextRequest, NextResponse } from 'next/server'
|
3
3
|
|
4
|
-
export type
|
5
|
-
|
4
|
+
export type AnyRequest = NextApiRequest | NextRequest
|
5
|
+
export type AnyResponse = NextApiResponse | NextResponse
|
6
|
+
|
7
|
+
export interface MockResponse<T = unknown> {
|
8
|
+
status?: number
|
6
9
|
data: T
|
7
|
-
delay?: number
|
8
10
|
headers?: Record<string, string>
|
11
|
+
delay?: number
|
9
12
|
}
|
10
13
|
|
11
|
-
export type MockFunction = (req:
|
14
|
+
export type MockFunction = (req: AnyRequest) => Promise<MockResponse>
|
12
15
|
|
13
|
-
export
|
14
|
-
[
|
16
|
+
export interface MockConfig {
|
17
|
+
[path: string]: MockResponse | MockFunction
|
15
18
|
}
|
16
19
|
|
17
|
-
export type RequestLogger = (req:
|
20
|
+
export type RequestLogger = (req: AnyRequest, res: AnyResponse) => void
|
18
21
|
|
19
|
-
export type MockInterceptor = (req:
|
22
|
+
export type MockInterceptor = (req: AnyRequest, mockResponse: MockResponse) => Promise<MockResponse>
|
20
23
|
|
21
|
-
export
|
22
|
-
|
23
|
-
export type ServerComponentMock<TProps, TReturn = Promise<NextResponse>> = (props: TProps) => TReturn
|
24
|
-
|
25
|
-
export type MockOptions = {
|
24
|
+
export interface MockOptions {
|
26
25
|
enableLogging: boolean
|
27
26
|
cacheTimeout: number
|
28
27
|
defaultDelay: number
|
29
28
|
errorRate: number
|
30
29
|
}
|
31
30
|
|
32
|
-
export type MiddlewareFunction = (req:
|
31
|
+
export type MiddlewareFunction = (req: AnyRequest, res: AnyResponse) => Promise<void>
|
32
|
+
|
33
|
+
export interface RateLimitOptions {
|
34
|
+
windowMs: number
|
35
|
+
max: number
|
36
|
+
}
|
33
37
|
|
34
|
-
export
|
38
|
+
export interface Plugin {
|
35
39
|
name: string
|
36
40
|
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
41
|
}
|
44
42
|
|
43
|
+
export function isNextApiRequest(req: AnyRequest): req is NextApiRequest {
|
44
|
+
return 'query' in req && 'body' in req && !('nextUrl' in req)
|
45
|
+
}
|
46
|
+
|
47
|
+
export function isNextRequest(req: AnyRequest): req is NextRequest {
|
48
|
+
return 'nextUrl' in req && 'geo' in req
|
49
|
+
}
|
50
|
+
|
51
|
+
export type NextRequestType = NextRequest
|
52
|
+
export type NextResponseType = NextResponse
|
53
|
+
|
54
|
+
export type ServerComponentMock<TProps, TReturn> = (props: TProps) => TReturn
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import semver from 'semver'
|
2
|
+
import { version as nextVersion } from 'next/package.json'
|
3
|
+
|
4
|
+
const coercedVersion = semver.coerce(nextVersion)
|
5
|
+
|
6
|
+
if (!coercedVersion) {
|
7
|
+
throw new Error('Unable to determine Next.js version')
|
8
|
+
}
|
9
|
+
|
10
|
+
export const isNextJs12 = semver.satisfies(coercedVersion, '12.x')
|
11
|
+
export const isNextJs13 = semver.satisfies(coercedVersion, '13.x')
|
12
|
+
export const isNextJs14 = semver.satisfies(coercedVersion, '14.x')
|
13
|
+
export const isNextJs15 = semver.satisfies(coercedVersion, '15.x')
|
14
|
+
|
15
|
+
export const isAppRouter = semver.gte(coercedVersion, '13.0.0')
|
16
|
+
export const hasServerActions = semver.gte(coercedVersion, '13.4.0')
|
17
|
+
|
18
|
+
export function getNextVersion(): string {
|
19
|
+
return nextVersion
|
20
|
+
}
|