lambder 1.0.129 → 1.0.130
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +49 -0
- package/dist/LambderMSW.d.ts +104 -0
- package/dist/LambderMSW.js +203 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/docs/MSW_MOCKING.md +590 -0
- package/examples/msw-mocking-example.ts +241 -0
- package/package.json +9 -1
- package/src/LambderMSW.ts +330 -0
- package/src/index.ts +11 -0
- package/tests/msw-integration.test.ts +183 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using MSW to mock Lambder API endpoints
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to use Mock Service Worker (MSW) with Lambder
|
|
5
|
+
* to create type-safe mocks for your API endpoints.
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* - Install MSW: npm install --save-dev msw
|
|
9
|
+
* - Define your API contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
mockLambderApi,
|
|
14
|
+
mockLambderApiError,
|
|
15
|
+
mockLambderSessionExpired,
|
|
16
|
+
mockLambderNotAuthorized,
|
|
17
|
+
mockLambderVersionExpired,
|
|
18
|
+
type ApiContractShape
|
|
19
|
+
} from '../src/index.js';
|
|
20
|
+
|
|
21
|
+
// 1. Define your API contract
|
|
22
|
+
const myApiContract = {
|
|
23
|
+
'user.getProfile': {
|
|
24
|
+
input: { userId: '' as string },
|
|
25
|
+
output: { name: '' as string, email: '' as string, role: '' as string }
|
|
26
|
+
},
|
|
27
|
+
'user.updateProfile': {
|
|
28
|
+
input: { userId: '' as string, name: '' as string, email: '' as string },
|
|
29
|
+
output: { success: true as boolean, message: '' as string }
|
|
30
|
+
},
|
|
31
|
+
'user.deleteAccount': {
|
|
32
|
+
input: { userId: '' as string },
|
|
33
|
+
output: { success: true as boolean }
|
|
34
|
+
},
|
|
35
|
+
'admin.listUsers': {
|
|
36
|
+
input: { page: 0 as number, limit: 0 as number },
|
|
37
|
+
output: { users: [] as Array<{ id: string; name: string }>, total: 0 as number }
|
|
38
|
+
}
|
|
39
|
+
} satisfies ApiContractShape;
|
|
40
|
+
|
|
41
|
+
type MyApiContract = typeof myApiContract;
|
|
42
|
+
|
|
43
|
+
// 2. Create mock handlers
|
|
44
|
+
|
|
45
|
+
// Basic success mock
|
|
46
|
+
export const getUserProfileHandler = mockLambderApi<MyApiContract, 'user.getProfile'>(
|
|
47
|
+
'user.getProfile',
|
|
48
|
+
(input) => {
|
|
49
|
+
console.log('Mocking user.getProfile for userId:', input.userId);
|
|
50
|
+
|
|
51
|
+
// Return mock data with full type safety
|
|
52
|
+
return {
|
|
53
|
+
name: 'John Doe',
|
|
54
|
+
email: 'john@example.com',
|
|
55
|
+
role: 'user'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Mock with dynamic response based on input
|
|
61
|
+
export const updateProfileHandler = mockLambderApi<MyApiContract, 'user.updateProfile'>(
|
|
62
|
+
'user.updateProfile',
|
|
63
|
+
(input) => {
|
|
64
|
+
console.log('Mocking user.updateProfile:', input);
|
|
65
|
+
|
|
66
|
+
// Validate input
|
|
67
|
+
if (!input.name || input.name.length < 2) {
|
|
68
|
+
throw new Error('Name must be at least 2 characters');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
message: `Profile updated for ${input.name}`
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
delay: 500 // Simulate network delay
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Mock with error response
|
|
82
|
+
export const updateProfileErrorHandler = mockLambderApiError<MyApiContract, 'user.updateProfile'>(
|
|
83
|
+
'user.updateProfile',
|
|
84
|
+
'Failed to update profile due to server error',
|
|
85
|
+
{
|
|
86
|
+
delay: 300
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Mock session expired
|
|
91
|
+
export const deleteAccountSessionExpiredHandler = mockLambderSessionExpired<MyApiContract, 'user.deleteAccount'>(
|
|
92
|
+
'user.deleteAccount'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Mock not authorized
|
|
96
|
+
export const listUsersNotAuthorizedHandler = mockLambderNotAuthorized<MyApiContract, 'admin.listUsers'>(
|
|
97
|
+
'admin.listUsers'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Mock version expired
|
|
101
|
+
export const getProfileVersionExpiredHandler = mockLambderVersionExpired<MyApiContract, 'user.getProfile'>(
|
|
102
|
+
'user.getProfile'
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Mock with pagination
|
|
106
|
+
export const listUsersHandler = mockLambderApi<MyApiContract, 'admin.listUsers'>(
|
|
107
|
+
'admin.listUsers',
|
|
108
|
+
(input) => {
|
|
109
|
+
// Generate mock paginated data
|
|
110
|
+
const allUsers = Array.from({ length: 50 }, (_, i) => ({
|
|
111
|
+
id: `user-${i + 1}`,
|
|
112
|
+
name: `User ${i + 1}`
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const start = input.page * input.limit;
|
|
116
|
+
const end = start + input.limit;
|
|
117
|
+
const users = allUsers.slice(start, end);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
users,
|
|
121
|
+
total: allUsers.length
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// 3. Set up MSW handlers
|
|
127
|
+
|
|
128
|
+
// For Node.js testing (Vitest, Jest, etc.)
|
|
129
|
+
/**
|
|
130
|
+
* Setup example for Node.js tests:
|
|
131
|
+
*
|
|
132
|
+
* ```typescript
|
|
133
|
+
* // test-setup.ts
|
|
134
|
+
* import { setupServer } from 'msw/node';
|
|
135
|
+
* import { getUserProfileHandler, updateProfileHandler } from './msw-example';
|
|
136
|
+
*
|
|
137
|
+
* export const server = setupServer(
|
|
138
|
+
* getUserProfileHandler,
|
|
139
|
+
* updateProfileHandler,
|
|
140
|
+
* listUsersHandler
|
|
141
|
+
* );
|
|
142
|
+
*
|
|
143
|
+
* // vitest.setup.ts
|
|
144
|
+
* beforeAll(() => server.listen());
|
|
145
|
+
* afterEach(() => server.resetHandlers());
|
|
146
|
+
* afterAll(() => server.close());
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
// For browser development/Storybook
|
|
151
|
+
/**
|
|
152
|
+
* Setup example for browser:
|
|
153
|
+
*
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // mocks/browser.ts
|
|
156
|
+
* import { setupWorker } from 'msw/browser';
|
|
157
|
+
* import { getUserProfileHandler, updateProfileHandler } from './msw-example';
|
|
158
|
+
*
|
|
159
|
+
* export const worker = setupWorker(
|
|
160
|
+
* getUserProfileHandler,
|
|
161
|
+
* updateProfileHandler,
|
|
162
|
+
* listUsersHandler
|
|
163
|
+
* );
|
|
164
|
+
*
|
|
165
|
+
* // main.tsx
|
|
166
|
+
* if (import.meta.env.DEV) {
|
|
167
|
+
* const { worker } = await import('./mocks/browser');
|
|
168
|
+
* worker.start();
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
// 4. Using in tests
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Test example:
|
|
177
|
+
*
|
|
178
|
+
* ```typescript
|
|
179
|
+
* import { describe, it, expect } from 'vitest';
|
|
180
|
+
* import { server } from './test-setup';
|
|
181
|
+
* import { caller } from './api-client';
|
|
182
|
+
*
|
|
183
|
+
* describe('User Profile', () => {
|
|
184
|
+
* it('should get user profile', async () => {
|
|
185
|
+
* const profile = await caller.api('user.getProfile', { userId: '123' });
|
|
186
|
+
*
|
|
187
|
+
* expect(profile).toEqual({
|
|
188
|
+
* name: 'John Doe',
|
|
189
|
+
* email: 'john@example.com',
|
|
190
|
+
* role: 'user'
|
|
191
|
+
* });
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* it('should handle update errors', async () => {
|
|
195
|
+
* // Override handler for this test
|
|
196
|
+
* server.use(updateProfileErrorHandler);
|
|
197
|
+
*
|
|
198
|
+
* const result = await caller.api('user.updateProfile', {
|
|
199
|
+
* userId: '123',
|
|
200
|
+
* name: 'New Name',
|
|
201
|
+
* email: 'new@example.com'
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* expect(result).toBeNull(); // Error handler returns null
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* it('should handle session expiry', async () => {
|
|
208
|
+
* server.use(deleteAccountSessionExpiredHandler);
|
|
209
|
+
*
|
|
210
|
+
* const result = await caller.api('user.deleteAccount', { userId: '123' });
|
|
211
|
+
*
|
|
212
|
+
* expect(result).toBeNull();
|
|
213
|
+
* // Verify sessionExpiredHandler was called if configured
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* it('should handle not authorized', async () => {
|
|
217
|
+
* server.use(listUsersNotAuthorizedHandler);
|
|
218
|
+
*
|
|
219
|
+
* const result = await caller.api('admin.listUsers', { page: 0, limit: 10 });
|
|
220
|
+
*
|
|
221
|
+
* expect(result).toBeNull();
|
|
222
|
+
* // Verify notAuthorizedHandler was called if configured
|
|
223
|
+
* });
|
|
224
|
+
* });
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
// Export all handlers for easy setup
|
|
229
|
+
export const handlers = [
|
|
230
|
+
getUserProfileHandler,
|
|
231
|
+
updateProfileHandler,
|
|
232
|
+
listUsersHandler,
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// Export error handlers separately
|
|
236
|
+
export const errorHandlers = [
|
|
237
|
+
updateProfileErrorHandler,
|
|
238
|
+
deleteAccountSessionExpiredHandler,
|
|
239
|
+
listUsersNotAuthorizedHandler,
|
|
240
|
+
getProfileVersionExpiredHandler,
|
|
241
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.130",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
"path-to-regexp": "^6.2.1",
|
|
28
28
|
"querystring": "^0.2.1"
|
|
29
29
|
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"msw": "^2.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"msw": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
30
38
|
"devDependencies": {
|
|
31
39
|
"@types/aws-lambda": "^8.10.136",
|
|
32
40
|
"@types/cookie": "^0.6.0",
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { ApiContractShape } from './LambderApiContract.js';
|
|
2
|
+
|
|
3
|
+
// Type for MSW HttpHandler (defined locally to avoid hard dependency on msw types)
|
|
4
|
+
export type HttpHandler = (info: any) => Promise<Response | undefined> | Response | undefined;
|
|
5
|
+
|
|
6
|
+
// Dynamic imports to avoid requiring MSW as a hard dependency
|
|
7
|
+
let http: any;
|
|
8
|
+
let HttpResponse: any;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize MSW dependencies dynamically
|
|
12
|
+
* This allows the module to be imported even if MSW is not installed
|
|
13
|
+
*/
|
|
14
|
+
async function ensureMSW() {
|
|
15
|
+
if (!http || !HttpResponse) {
|
|
16
|
+
try {
|
|
17
|
+
// @ts-ignore - MSW is an optional peer dependency
|
|
18
|
+
const msw = await import('msw');
|
|
19
|
+
http = msw.http;
|
|
20
|
+
HttpResponse = msw.HttpResponse;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'MSW is required to use Lambder mocking utilities. ' +
|
|
24
|
+
'Install it with: npm install --save-dev msw'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a mock handler for a Lambder API endpoint
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* mockLambderApi('public.getInitialPageData', () => ({
|
|
36
|
+
* userLocationData: { ... },
|
|
37
|
+
* sessionUser: null
|
|
38
|
+
* }))
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example with dynamic response based on input
|
|
42
|
+
* ```typescript
|
|
43
|
+
* mockLambderApi('user.updateProfile', (input) => ({
|
|
44
|
+
* success: true,
|
|
45
|
+
* userId: input.userId
|
|
46
|
+
* }))
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function mockLambderApi<
|
|
50
|
+
TContract extends ApiContractShape,
|
|
51
|
+
TApiName extends keyof TContract & string
|
|
52
|
+
>(
|
|
53
|
+
apiName: TApiName,
|
|
54
|
+
responseFactory: (
|
|
55
|
+
input: TContract[TApiName]['input']
|
|
56
|
+
) => TContract[TApiName]['output'] | Promise<TContract[TApiName]['output']>,
|
|
57
|
+
options?: {
|
|
58
|
+
/** Custom API path, defaults to '/api' */
|
|
59
|
+
apiPath?: string
|
|
60
|
+
/** Delay in ms before responding (for testing loading states) */
|
|
61
|
+
delay?: number
|
|
62
|
+
/** Custom apiVersion to include in response */
|
|
63
|
+
apiVersion?: string | null
|
|
64
|
+
}
|
|
65
|
+
): HttpHandler {
|
|
66
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
67
|
+
const delay = options?.delay ?? 0;
|
|
68
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
69
|
+
|
|
70
|
+
// Return a handler that will be initialized when MSW is available
|
|
71
|
+
return (async ({ request }: any) => {
|
|
72
|
+
await ensureMSW();
|
|
73
|
+
|
|
74
|
+
const body = (await request.json()) as {
|
|
75
|
+
apiName: string;
|
|
76
|
+
payload?: TContract[TApiName]['input'];
|
|
77
|
+
version?: string;
|
|
78
|
+
token?: string;
|
|
79
|
+
siteHost?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Only handle this specific API
|
|
83
|
+
if (body.apiName !== apiName) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`[MSW Lambder] Mocking ${apiName}`, body.payload);
|
|
88
|
+
|
|
89
|
+
// Apply delay if specified
|
|
90
|
+
if (delay > 0) {
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Generate the response
|
|
95
|
+
const payload = await responseFactory(body.payload as TContract[TApiName]['input']);
|
|
96
|
+
|
|
97
|
+
// Return in Lambder API response format
|
|
98
|
+
return HttpResponse.json({
|
|
99
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
100
|
+
payload,
|
|
101
|
+
});
|
|
102
|
+
}) as any;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a mock handler that returns an error for a Lambder API endpoint
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* mockLambderApiError('user.deleteAccount', 'Account deletion failed')
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function mockLambderApiError<
|
|
114
|
+
TContract extends ApiContractShape,
|
|
115
|
+
TApiName extends keyof TContract & string
|
|
116
|
+
>(
|
|
117
|
+
apiName: TApiName,
|
|
118
|
+
errorMessage: string,
|
|
119
|
+
options?: {
|
|
120
|
+
/** Custom API path, defaults to '/api' */
|
|
121
|
+
apiPath?: string;
|
|
122
|
+
/** Delay in ms before responding */
|
|
123
|
+
delay?: number;
|
|
124
|
+
/** Custom apiVersion to include in response */
|
|
125
|
+
apiVersion?: string | null;
|
|
126
|
+
}
|
|
127
|
+
): HttpHandler {
|
|
128
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
129
|
+
const delay = options?.delay ?? 0;
|
|
130
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
131
|
+
|
|
132
|
+
return (async ({ request }: any) => {
|
|
133
|
+
await ensureMSW();
|
|
134
|
+
|
|
135
|
+
const body = (await request.json()) as {
|
|
136
|
+
apiName: string;
|
|
137
|
+
payload?: any;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (body.apiName !== apiName) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`[MSW Lambder] Mocking error for ${apiName}:`, errorMessage);
|
|
145
|
+
|
|
146
|
+
if (delay > 0) {
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return HttpResponse.json({
|
|
151
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
152
|
+
payload: null,
|
|
153
|
+
errorMessage,
|
|
154
|
+
});
|
|
155
|
+
}) as any;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates a mock handler that simulates a session expired error
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* mockLambderSessionExpired('user.updateProfile')
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export function mockLambderSessionExpired<
|
|
167
|
+
TContract extends ApiContractShape,
|
|
168
|
+
TApiName extends keyof TContract & string
|
|
169
|
+
>(
|
|
170
|
+
apiName: TApiName,
|
|
171
|
+
options?: {
|
|
172
|
+
/** Custom API path, defaults to '/api' */
|
|
173
|
+
apiPath?: string;
|
|
174
|
+
/** Custom apiVersion to include in response */
|
|
175
|
+
apiVersion?: string | null;
|
|
176
|
+
}
|
|
177
|
+
): HttpHandler {
|
|
178
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
179
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
180
|
+
|
|
181
|
+
return (async ({ request }: any) => {
|
|
182
|
+
await ensureMSW();
|
|
183
|
+
|
|
184
|
+
const body = (await request.json()) as {
|
|
185
|
+
apiName: string;
|
|
186
|
+
payload?: any;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (body.apiName !== apiName) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(`[MSW Lambder] Mocking session expired for ${apiName}`);
|
|
194
|
+
|
|
195
|
+
return HttpResponse.json({
|
|
196
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
197
|
+
payload: null,
|
|
198
|
+
sessionExpired: true,
|
|
199
|
+
errorMessage: 'Session expired',
|
|
200
|
+
});
|
|
201
|
+
}) as any;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Creates a mock handler that simulates a "not authorized" error
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* mockLambderNotAuthorized('admin.deleteUser')
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export function mockLambderNotAuthorized<
|
|
213
|
+
TContract extends ApiContractShape,
|
|
214
|
+
TApiName extends keyof TContract & string
|
|
215
|
+
>(
|
|
216
|
+
apiName: TApiName,
|
|
217
|
+
options?: {
|
|
218
|
+
/** Custom API path, defaults to '/api' */
|
|
219
|
+
apiPath?: string;
|
|
220
|
+
/** Custom apiVersion to include in response */
|
|
221
|
+
apiVersion?: string | null;
|
|
222
|
+
}
|
|
223
|
+
): HttpHandler {
|
|
224
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
225
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
226
|
+
|
|
227
|
+
return (async ({ request }: any) => {
|
|
228
|
+
await ensureMSW();
|
|
229
|
+
|
|
230
|
+
const body = (await request.json()) as {
|
|
231
|
+
apiName: string;
|
|
232
|
+
payload?: any;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (body.apiName !== apiName) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(`[MSW Lambder] Mocking not authorized for ${apiName}`);
|
|
240
|
+
|
|
241
|
+
return HttpResponse.json({
|
|
242
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
243
|
+
payload: null,
|
|
244
|
+
notAuthorized: true,
|
|
245
|
+
errorMessage: 'Not authorized',
|
|
246
|
+
});
|
|
247
|
+
}) as any;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Creates a mock handler that simulates a version expired error
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* mockLambderVersionExpired('user.updateProfile')
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
export function mockLambderVersionExpired<
|
|
259
|
+
TContract extends ApiContractShape,
|
|
260
|
+
TApiName extends keyof TContract & string
|
|
261
|
+
>(
|
|
262
|
+
apiName: TApiName,
|
|
263
|
+
options?: {
|
|
264
|
+
/** Custom API path, defaults to '/api' */
|
|
265
|
+
apiPath?: string;
|
|
266
|
+
/** Custom apiVersion to include in response */
|
|
267
|
+
apiVersion?: string | null;
|
|
268
|
+
}
|
|
269
|
+
): HttpHandler {
|
|
270
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
271
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
272
|
+
|
|
273
|
+
return (async ({ request }: any) => {
|
|
274
|
+
await ensureMSW();
|
|
275
|
+
|
|
276
|
+
const body = (await request.json()) as {
|
|
277
|
+
apiName: string;
|
|
278
|
+
payload?: any;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (body.apiName !== apiName) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log(`[MSW Lambder] Mocking version expired for ${apiName}`);
|
|
286
|
+
|
|
287
|
+
return HttpResponse.json({
|
|
288
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
289
|
+
payload: null,
|
|
290
|
+
versionExpired: true,
|
|
291
|
+
errorMessage: 'Version expired',
|
|
292
|
+
});
|
|
293
|
+
}) as any;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Helper function to create the MSW http.post handler with proper path matching
|
|
298
|
+
* This is useful when you need to set up multiple handlers with the same path
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* import { setupServer } from 'msw/node';
|
|
303
|
+
*
|
|
304
|
+
* const server = setupServer(
|
|
305
|
+
* createLambderApiHandler('/api', [
|
|
306
|
+
* mockLambderApi('user.getProfile', () => ({ name: 'Test User' })),
|
|
307
|
+
* mockLambderApi('user.updateProfile', (input) => ({ success: true }))
|
|
308
|
+
* ])
|
|
309
|
+
* );
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function createLambderApiHandler(
|
|
313
|
+
apiPath: string,
|
|
314
|
+
handlers: HttpHandler[]
|
|
315
|
+
): HttpHandler {
|
|
316
|
+
return (async (info: any) => {
|
|
317
|
+
await ensureMSW();
|
|
318
|
+
|
|
319
|
+
// Call each handler in sequence until one handles the request
|
|
320
|
+
for (const handler of handlers) {
|
|
321
|
+
const result = await handler(info);
|
|
322
|
+
if (result) {
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// No handler matched
|
|
328
|
+
return;
|
|
329
|
+
}) as any;
|
|
330
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,3 +13,14 @@ export {
|
|
|
13
13
|
type ApiInput,
|
|
14
14
|
type ApiOutput,
|
|
15
15
|
} from "./LambderApiContract.js";
|
|
16
|
+
|
|
17
|
+
// MSW mocking utilities (optional - requires msw to be installed)
|
|
18
|
+
export {
|
|
19
|
+
mockLambderApi,
|
|
20
|
+
mockLambderApiError,
|
|
21
|
+
mockLambderSessionExpired,
|
|
22
|
+
mockLambderNotAuthorized,
|
|
23
|
+
mockLambderVersionExpired,
|
|
24
|
+
createLambderApiHandler,
|
|
25
|
+
type HttpHandler,
|
|
26
|
+
} from "./LambderMSW.js";
|