lambder 1.0.132 → 1.0.134
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 +36 -0
- package/dist/LambderMSW.d.ts +11 -6
- package/dist/LambderMSW.js +43 -24
- package/docs/LAMBDER_MSW.md +430 -0
- package/docs/TYPE_SAFE_QUICK_START.md +33 -0
- package/examples/msw-testing-example.ts +277 -0
- package/package.json +1 -1
- package/src/LambderMSW.ts +59 -47
- package/test-type-enforcement.ts +111 -0
package/Readme.md
CHANGED
|
@@ -544,6 +544,42 @@ const user = await caller.api('getUserById', { userId: '123' });
|
|
|
544
544
|
|
|
545
545
|
📖 **[Read the Quick Start Guide](docs/TYPE_SAFE_QUICK_START.md)** for more details and examples!
|
|
546
546
|
|
|
547
|
+
## Testing with LambderMSW
|
|
548
|
+
|
|
549
|
+
LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your APIs. It works perfectly with type-safe API contracts!
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
import { LambderMSW } from 'lambder';
|
|
553
|
+
import { setupServer } from 'msw/node';
|
|
554
|
+
import type { MyApiContract } from './shared/apiContract';
|
|
555
|
+
|
|
556
|
+
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
557
|
+
apiPath: '/api',
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const handlers = [
|
|
561
|
+
// Mock API with full type safety! ✨
|
|
562
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
563
|
+
return {
|
|
564
|
+
id: payload.userId,
|
|
565
|
+
name: 'John Doe',
|
|
566
|
+
email: 'john@example.com'
|
|
567
|
+
};
|
|
568
|
+
}),
|
|
569
|
+
|
|
570
|
+
// Mock errors, delays, and more
|
|
571
|
+
lambderMSW.mockApi('createUser', async (payload) => {
|
|
572
|
+
return { id: '123', ...payload };
|
|
573
|
+
}, { delay: 500 }),
|
|
574
|
+
|
|
575
|
+
lambderMSW.mockSessionExpired('protectedApi'),
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
const server = setupServer(...handlers);
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
📖 **[Read the LambderMSW Guide](docs/LAMBDER_MSW.md)** for complete testing documentation!
|
|
582
|
+
|
|
547
583
|
## Contributing
|
|
548
584
|
|
|
549
585
|
Contributions are welcome! Especially for documentation. If you have an idea for an improvement or have found a bug, please open an issue or submit a pull request.
|
package/dist/LambderMSW.d.ts
CHANGED
|
@@ -23,27 +23,32 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
23
23
|
* @param apiName - The name of the API to mock
|
|
24
24
|
* @param handler - Function that returns the mock payload
|
|
25
25
|
* @param options - Additional response options (session expired, version expired, etc.)
|
|
26
|
+
*
|
|
27
|
+
* Note: TypeScript will check that the return type is assignable to the output type,
|
|
28
|
+
* but due to structural typing, extra properties are allowed. Enable strict checks
|
|
29
|
+
* in your tsconfig.json with "noUncheckedIndexedAccess" and "exactOptionalPropertyTypes"
|
|
30
|
+
* for better type safety.
|
|
26
31
|
*/
|
|
27
|
-
mockApi<TApiName extends keyof TContract
|
|
32
|
+
mockApi<TApiName extends keyof TContract>(apiName: TApiName, handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'], options?: MockApiOptions): RequestHandler;
|
|
28
33
|
/**
|
|
29
34
|
* Mock an API endpoint that returns a session expired error
|
|
30
35
|
*/
|
|
31
|
-
mockSessionExpired<TApiName extends keyof TContract
|
|
36
|
+
mockSessionExpired<TApiName extends keyof TContract>(apiName: TApiName): RequestHandler;
|
|
32
37
|
/**
|
|
33
38
|
* Mock an API endpoint that returns a version expired error
|
|
34
39
|
*/
|
|
35
|
-
mockVersionExpired<TApiName extends keyof TContract
|
|
40
|
+
mockVersionExpired<TApiName extends keyof TContract>(apiName: TApiName): RequestHandler;
|
|
36
41
|
/**
|
|
37
42
|
* Mock an API endpoint that returns a not authorized error
|
|
38
43
|
*/
|
|
39
|
-
mockNotAuthorized<TApiName extends keyof TContract
|
|
44
|
+
mockNotAuthorized<TApiName extends keyof TContract>(apiName: TApiName): RequestHandler;
|
|
40
45
|
/**
|
|
41
46
|
* Mock an API endpoint that returns an error message
|
|
42
47
|
*/
|
|
43
|
-
mockError<TApiName extends keyof TContract
|
|
48
|
+
mockError<TApiName extends keyof TContract>(apiName: TApiName, errorMessage: string): RequestHandler;
|
|
44
49
|
/**
|
|
45
50
|
* Mock an API endpoint with a custom message
|
|
46
51
|
*/
|
|
47
|
-
mockWithMessage<TApiName extends keyof TContract
|
|
52
|
+
mockWithMessage<TApiName extends keyof TContract>(apiName: TApiName, handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'], message: any): RequestHandler;
|
|
48
53
|
}
|
|
49
54
|
export {};
|
package/dist/LambderMSW.js
CHANGED
|
@@ -21,38 +21,57 @@ export default class LambderMSW {
|
|
|
21
21
|
* @param apiName - The name of the API to mock
|
|
22
22
|
* @param handler - Function that returns the mock payload
|
|
23
23
|
* @param options - Additional response options (session expired, version expired, etc.)
|
|
24
|
+
*
|
|
25
|
+
* Note: TypeScript will check that the return type is assignable to the output type,
|
|
26
|
+
* but due to structural typing, extra properties are allowed. Enable strict checks
|
|
27
|
+
* in your tsconfig.json with "noUncheckedIndexedAccess" and "exactOptionalPropertyTypes"
|
|
28
|
+
* for better type safety.
|
|
24
29
|
*/
|
|
25
30
|
mockApi(apiName, handler, options) {
|
|
26
31
|
return this.http.post(this.apiPath, async ({ request }) => {
|
|
32
|
+
let body;
|
|
27
33
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
|
|
44
|
-
...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
|
|
45
|
-
...(options?.message ? { message: options.message } : {}),
|
|
46
|
-
...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
|
|
47
|
-
...(options?.logList?.length ? { logList: options.logList } : {}),
|
|
48
|
-
};
|
|
49
|
-
return this.HttpResponse.json(response);
|
|
50
|
-
}
|
|
34
|
+
body = await request.json();
|
|
35
|
+
}
|
|
36
|
+
catch (parseErr) {
|
|
37
|
+
// If JSON parsing fails, return undefined to let other handlers try
|
|
38
|
+
console.warn("LambderMSW: Failed to parse request body as JSON");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Check if body is valid and has apiName
|
|
42
|
+
if (!body || typeof body.apiName !== 'string') {
|
|
43
|
+
// Invalid request format, let other handlers try
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log("LambderMSW called for:", body.apiName, "matching against:", apiName);
|
|
47
|
+
// Check if this is the API we're mocking
|
|
48
|
+
if (body.apiName !== apiName) {
|
|
51
49
|
// If this handler doesn't match, return undefined to let MSW try other handlers
|
|
52
50
|
return;
|
|
53
51
|
}
|
|
52
|
+
try {
|
|
53
|
+
// Add artificial delay if specified
|
|
54
|
+
if (options?.delay) {
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, options.delay));
|
|
56
|
+
}
|
|
57
|
+
// Call the handler with the payload from the request
|
|
58
|
+
const payload = await handler(body.payload);
|
|
59
|
+
console.log("Matched! Returning payload for:", apiName);
|
|
60
|
+
const response = {
|
|
61
|
+
apiVersion: this.apiVersion,
|
|
62
|
+
payload,
|
|
63
|
+
...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
|
|
64
|
+
...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
|
|
65
|
+
...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
|
|
66
|
+
...(options?.message ? { message: options.message } : {}),
|
|
67
|
+
...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
|
|
68
|
+
...(options?.logList?.length ? { logList: options.logList } : {}),
|
|
69
|
+
};
|
|
70
|
+
return this.HttpResponse.json(response);
|
|
71
|
+
}
|
|
54
72
|
catch (err) {
|
|
55
|
-
|
|
73
|
+
// Only handle errors that occur during handler execution for matched APIs
|
|
74
|
+
console.error("Error in LambderMSW handler for", apiName, ":", err);
|
|
56
75
|
const errorResponse = {
|
|
57
76
|
apiVersion: this.apiVersion,
|
|
58
77
|
payload: null,
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# LambderMSW - Mock Service Worker Integration
|
|
2
|
+
|
|
3
|
+
LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your Lambder APIs. It allows you to mock API endpoints with full type safety when using TypeScript API contracts.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
First, install MSW as a dev dependency:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install msw --save-dev
|
|
11
|
+
# or
|
|
12
|
+
yarn add msw --dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Basic Setup
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { LambderMSW } from 'lambder';
|
|
19
|
+
import { setupServer } from 'msw/node';
|
|
20
|
+
|
|
21
|
+
// Create an MSW instance
|
|
22
|
+
const lambderMSW = new LambderMSW({
|
|
23
|
+
apiPath: '/secure', // Must match your Lambder backend apiPath
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Create mock handlers
|
|
27
|
+
const handlers = [
|
|
28
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
29
|
+
return {
|
|
30
|
+
id: payload.userId,
|
|
31
|
+
name: 'John Doe',
|
|
32
|
+
email: 'john@example.com'
|
|
33
|
+
};
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
lambderMSW.mockApi('createUser', async (payload) => {
|
|
37
|
+
return {
|
|
38
|
+
id: '123',
|
|
39
|
+
name: payload.name,
|
|
40
|
+
email: payload.email
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Setup MSW server
|
|
46
|
+
const server = setupServer(...handlers);
|
|
47
|
+
|
|
48
|
+
// Start server before tests
|
|
49
|
+
beforeAll(() => server.listen());
|
|
50
|
+
afterEach(() => server.resetHandlers());
|
|
51
|
+
afterAll(() => server.close());
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Type-Safe Mocking
|
|
55
|
+
|
|
56
|
+
When using TypeScript API contracts, LambderMSW provides full type safety:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// shared/apiContract.ts
|
|
60
|
+
import type { ApiContract } from 'lambder';
|
|
61
|
+
|
|
62
|
+
export type MyApiContract = ApiContract<{
|
|
63
|
+
getUserById: {
|
|
64
|
+
input: { userId: string },
|
|
65
|
+
output: { id: string, name: string, email: string }
|
|
66
|
+
},
|
|
67
|
+
createUser: {
|
|
68
|
+
input: { name: string, email: string },
|
|
69
|
+
output: { id: string, name: string, email: string }
|
|
70
|
+
}
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
// test/setup.ts
|
|
74
|
+
import { LambderMSW } from 'lambder';
|
|
75
|
+
import type { MyApiContract } from '../shared/apiContract';
|
|
76
|
+
|
|
77
|
+
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
78
|
+
apiPath: '/secure',
|
|
79
|
+
apiVersion: '1.0.0'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Now mockApi is fully typed! ✨
|
|
83
|
+
const handler = lambderMSW.mockApi('getUserById', async (payload) => {
|
|
84
|
+
// payload is automatically typed as { userId: string }
|
|
85
|
+
// Return value is type-checked against output type
|
|
86
|
+
return {
|
|
87
|
+
id: payload.userId,
|
|
88
|
+
name: 'John Doe',
|
|
89
|
+
email: 'john@example.com'
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### `new LambderMSW(options)`
|
|
97
|
+
|
|
98
|
+
Creates a new LambderMSW instance.
|
|
99
|
+
|
|
100
|
+
**Parameters:**
|
|
101
|
+
- `apiPath` (string): The API endpoint path (must match your Lambder backend)
|
|
102
|
+
- `apiVersion` (string, optional): API version to include in responses
|
|
103
|
+
|
|
104
|
+
### `mockApi(apiName, handler, options?)`
|
|
105
|
+
|
|
106
|
+
Mock an API endpoint with a custom handler.
|
|
107
|
+
|
|
108
|
+
**Parameters:**
|
|
109
|
+
- `apiName` (string): Name of the API to mock
|
|
110
|
+
- `handler` (function): Async or sync function that returns the mock payload
|
|
111
|
+
- Input: API payload from the request
|
|
112
|
+
- Output: The mocked response payload
|
|
113
|
+
- `options` (object, optional):
|
|
114
|
+
- `versionExpired` (boolean): Simulate version expired error
|
|
115
|
+
- `sessionExpired` (boolean): Simulate session expired error
|
|
116
|
+
- `notAuthorized` (boolean): Simulate not authorized error
|
|
117
|
+
- `message` (any): Custom message to include in response
|
|
118
|
+
- `errorMessage` (any): Error message to include in response
|
|
119
|
+
- `logList` (array): Array of log entries
|
|
120
|
+
- `delay` (number): Artificial delay in milliseconds to simulate network latency
|
|
121
|
+
|
|
122
|
+
**Returns:** MSW RequestHandler
|
|
123
|
+
|
|
124
|
+
**Example:**
|
|
125
|
+
```typescript
|
|
126
|
+
lambderMSW.mockApi('getCompanyPage', async (payload) => {
|
|
127
|
+
return {
|
|
128
|
+
companyName: payload.companyName,
|
|
129
|
+
description: 'Mock company description',
|
|
130
|
+
employees: 100
|
|
131
|
+
};
|
|
132
|
+
}, {
|
|
133
|
+
delay: 500, // Simulate 500ms network delay
|
|
134
|
+
message: 'Data fetched successfully'
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `mockSessionExpired(apiName)`
|
|
139
|
+
|
|
140
|
+
Mock an API that returns a session expired error.
|
|
141
|
+
|
|
142
|
+
**Parameters:**
|
|
143
|
+
- `apiName` (string): Name of the API to mock
|
|
144
|
+
|
|
145
|
+
**Example:**
|
|
146
|
+
```typescript
|
|
147
|
+
lambderMSW.mockSessionExpired('getProtectedData');
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `mockVersionExpired(apiName)`
|
|
151
|
+
|
|
152
|
+
Mock an API that returns a version expired error.
|
|
153
|
+
|
|
154
|
+
**Parameters:**
|
|
155
|
+
- `apiName` (string): Name of the API to mock
|
|
156
|
+
|
|
157
|
+
**Example:**
|
|
158
|
+
```typescript
|
|
159
|
+
lambderMSW.mockVersionExpired('getUserProfile');
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `mockNotAuthorized(apiName)`
|
|
163
|
+
|
|
164
|
+
Mock an API that returns a not authorized error.
|
|
165
|
+
|
|
166
|
+
**Parameters:**
|
|
167
|
+
- `apiName` (string): Name of the API to mock
|
|
168
|
+
|
|
169
|
+
**Example:**
|
|
170
|
+
```typescript
|
|
171
|
+
lambderMSW.mockNotAuthorized('deleteUser');
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `mockError(apiName, errorMessage)`
|
|
175
|
+
|
|
176
|
+
Mock an API that returns a custom error message.
|
|
177
|
+
|
|
178
|
+
**Parameters:**
|
|
179
|
+
- `apiName` (string): Name of the API to mock
|
|
180
|
+
- `errorMessage` (string): The error message to return
|
|
181
|
+
|
|
182
|
+
**Example:**
|
|
183
|
+
```typescript
|
|
184
|
+
lambderMSW.mockError('submitOrder', 'Payment processing failed');
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### `mockWithMessage(apiName, handler, message)`
|
|
188
|
+
|
|
189
|
+
Mock an API with a custom success message.
|
|
190
|
+
|
|
191
|
+
**Parameters:**
|
|
192
|
+
- `apiName` (string): Name of the API to mock
|
|
193
|
+
- `handler` (function): Handler function that returns the mock payload
|
|
194
|
+
- `message` (any): Custom message to include in response
|
|
195
|
+
|
|
196
|
+
**Example:**
|
|
197
|
+
```typescript
|
|
198
|
+
lambderMSW.mockWithMessage('updateProfile', async (payload) => {
|
|
199
|
+
return { userId: payload.userId, updated: true };
|
|
200
|
+
}, 'Profile updated successfully');
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Complete Testing Example
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// test/api.test.ts
|
|
207
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
208
|
+
import { setupServer } from 'msw/node';
|
|
209
|
+
import { LambderMSW } from 'lambder';
|
|
210
|
+
import { LambderCaller } from 'lambder';
|
|
211
|
+
import type { MyApiContract } from '../shared/apiContract';
|
|
212
|
+
|
|
213
|
+
// Setup MSW
|
|
214
|
+
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
215
|
+
apiPath: '/secure',
|
|
216
|
+
apiVersion: '1.0.0'
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const handlers = [
|
|
220
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
221
|
+
if (payload.userId === '123') {
|
|
222
|
+
return {
|
|
223
|
+
id: '123',
|
|
224
|
+
name: 'John Doe',
|
|
225
|
+
email: 'john@example.com'
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}),
|
|
230
|
+
|
|
231
|
+
lambderMSW.mockApi('createUser', async (payload) => {
|
|
232
|
+
return {
|
|
233
|
+
id: Math.random().toString(),
|
|
234
|
+
name: payload.name,
|
|
235
|
+
email: payload.email
|
|
236
|
+
};
|
|
237
|
+
}, { delay: 100 }), // Simulate 100ms delay
|
|
238
|
+
|
|
239
|
+
lambderMSW.mockSessionExpired('getProtectedData'),
|
|
240
|
+
|
|
241
|
+
lambderMSW.mockError('failingApi', 'Something went wrong')
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const server = setupServer(...handlers);
|
|
245
|
+
|
|
246
|
+
beforeAll(() => server.listen());
|
|
247
|
+
afterEach(() => server.resetHandlers());
|
|
248
|
+
afterAll(() => server.close());
|
|
249
|
+
|
|
250
|
+
// Setup LambderCaller
|
|
251
|
+
const caller = new LambderCaller<MyApiContract>({
|
|
252
|
+
apiPath: '/secure',
|
|
253
|
+
isCorsEnabled: false
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('User APIs', () => {
|
|
257
|
+
it('should fetch user by id', async () => {
|
|
258
|
+
const result = await caller.api('getUserById', { userId: '123' });
|
|
259
|
+
|
|
260
|
+
expect(result).toEqual({
|
|
261
|
+
id: '123',
|
|
262
|
+
name: 'John Doe',
|
|
263
|
+
email: 'john@example.com'
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should create a new user', async () => {
|
|
268
|
+
const result = await caller.api('createUser', {
|
|
269
|
+
name: 'Jane Smith',
|
|
270
|
+
email: 'jane@example.com'
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(result?.name).toBe('Jane Smith');
|
|
274
|
+
expect(result?.email).toBe('jane@example.com');
|
|
275
|
+
expect(result?.id).toBeDefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should handle session expired', async () => {
|
|
279
|
+
try {
|
|
280
|
+
await caller.api('getProtectedData', {});
|
|
281
|
+
// Should not reach here
|
|
282
|
+
expect(true).toBe(false);
|
|
283
|
+
} catch (error: any) {
|
|
284
|
+
expect(error.sessionExpired).toBe(true);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should handle errors', async () => {
|
|
289
|
+
try {
|
|
290
|
+
await caller.api('failingApi', {});
|
|
291
|
+
// Should not reach here
|
|
292
|
+
expect(true).toBe(false);
|
|
293
|
+
} catch (error: any) {
|
|
294
|
+
expect(error.errorMessage).toBe('Something went wrong');
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Browser Testing
|
|
301
|
+
|
|
302
|
+
LambderMSW also works in browser environments with MSW's browser integration:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// test/browser-setup.ts
|
|
306
|
+
import { setupWorker } from 'msw/browser';
|
|
307
|
+
import { LambderMSW } from 'lambder';
|
|
308
|
+
import type { MyApiContract } from '../shared/apiContract';
|
|
309
|
+
|
|
310
|
+
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
311
|
+
apiPath: '/secure'
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const handlers = [
|
|
315
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
316
|
+
return {
|
|
317
|
+
id: payload.userId,
|
|
318
|
+
name: 'John Doe',
|
|
319
|
+
email: 'john@example.com'
|
|
320
|
+
};
|
|
321
|
+
})
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
const worker = setupWorker(...handlers);
|
|
325
|
+
|
|
326
|
+
// Start the worker
|
|
327
|
+
worker.start();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Advanced Usage
|
|
331
|
+
|
|
332
|
+
### Dynamic Mock Responses
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
lambderMSW.mockApi('searchUsers', async (payload) => {
|
|
336
|
+
const { query, limit } = payload;
|
|
337
|
+
|
|
338
|
+
// Return different responses based on input
|
|
339
|
+
if (query === 'admin') {
|
|
340
|
+
return [{
|
|
341
|
+
id: '1',
|
|
342
|
+
name: 'Admin User',
|
|
343
|
+
role: 'admin'
|
|
344
|
+
}];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return Array.from({ length: limit }, (_, i) => ({
|
|
348
|
+
id: `${i}`,
|
|
349
|
+
name: `User ${i}`,
|
|
350
|
+
role: 'user'
|
|
351
|
+
}));
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Simulating Network Conditions
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// Slow network
|
|
359
|
+
lambderMSW.mockApi('slowApi', async () => {
|
|
360
|
+
return { data: 'slow response' };
|
|
361
|
+
}, { delay: 3000 }); // 3 second delay
|
|
362
|
+
|
|
363
|
+
// Intermittent failures
|
|
364
|
+
let callCount = 0;
|
|
365
|
+
lambderMSW.mockApi('flakeyApi', async () => {
|
|
366
|
+
callCount++;
|
|
367
|
+
if (callCount % 3 === 0) {
|
|
368
|
+
throw new Error('Random failure');
|
|
369
|
+
}
|
|
370
|
+
return { success: true };
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Override Handlers Per Test
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
it('should handle specific user', async () => {
|
|
378
|
+
// Override the default handler for this test
|
|
379
|
+
server.use(
|
|
380
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
381
|
+
return {
|
|
382
|
+
id: payload.userId,
|
|
383
|
+
name: 'Special User',
|
|
384
|
+
email: 'special@example.com'
|
|
385
|
+
};
|
|
386
|
+
})
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const result = await caller.api('getUserById', { userId: '999' });
|
|
390
|
+
expect(result?.name).toBe('Special User');
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Benefits
|
|
395
|
+
|
|
396
|
+
✅ **Type Safety** - Full TypeScript support with API contracts
|
|
397
|
+
✅ **Simple API** - Intuitive methods matching Lambder's API structure
|
|
398
|
+
✅ **Flexible** - Mock success, errors, delays, and custom responses
|
|
399
|
+
✅ **Isolated** - Tests run without real backend dependencies
|
|
400
|
+
✅ **Fast** - No network calls, instant test execution
|
|
401
|
+
✅ **Realistic** - Simulate real-world scenarios (delays, errors, etc.)
|
|
402
|
+
|
|
403
|
+
## Troubleshooting
|
|
404
|
+
|
|
405
|
+
### MSW Not Found Error
|
|
406
|
+
|
|
407
|
+
If you see "MSW (Mock Service Worker) is required", make sure MSW is installed:
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
npm install msw --save-dev
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Handler Not Matching
|
|
414
|
+
|
|
415
|
+
LambderMSW matches handlers based on the `apiName` in the request body. Make sure:
|
|
416
|
+
1. Your `apiPath` matches between backend, frontend, and mocks
|
|
417
|
+
2. The API name string matches exactly
|
|
418
|
+
3. Handlers are registered before making requests
|
|
419
|
+
|
|
420
|
+
### Console Warnings
|
|
421
|
+
|
|
422
|
+
LambderMSW logs matching information to console for debugging. To see these logs:
|
|
423
|
+
- Check browser console for "LambderMSW called for:" messages
|
|
424
|
+
- Verify "Matched!" appears when handler should execute
|
|
425
|
+
|
|
426
|
+
## See Also
|
|
427
|
+
|
|
428
|
+
- [MSW Documentation](https://mswjs.io/)
|
|
429
|
+
- [Type-Safe Quick Start](./TYPE_SAFE_QUICK_START.md)
|
|
430
|
+
- [Lambder Main Documentation](../Readme.md)
|
|
@@ -156,6 +156,38 @@ lambder.addApi('anyApi', async (ctx, resolver) => {
|
|
|
156
156
|
|
|
157
157
|
See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.ts) for a complete working example.
|
|
158
158
|
|
|
159
|
+
## Testing Your Type-Safe APIs
|
|
160
|
+
|
|
161
|
+
LambderMSW provides full type safety for testing your APIs with MSW (Mock Service Worker):
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { LambderMSW } from 'lambder';
|
|
165
|
+
import { setupServer } from 'msw/node';
|
|
166
|
+
import type { MyApiContract } from './shared/apiContract';
|
|
167
|
+
|
|
168
|
+
// Create type-safe MSW instance
|
|
169
|
+
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
170
|
+
apiPath: '/api'
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Mock with full type safety! ✨
|
|
174
|
+
const handlers = [
|
|
175
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
176
|
+
// payload is typed as { userId: string }
|
|
177
|
+
// Return value is type-checked against output
|
|
178
|
+
return {
|
|
179
|
+
id: payload.userId,
|
|
180
|
+
name: 'John Doe',
|
|
181
|
+
email: 'john@example.com'
|
|
182
|
+
};
|
|
183
|
+
})
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const server = setupServer(...handlers);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
📖 See [LAMBDER_MSW.md](./LAMBDER_MSW.md) for complete testing documentation.
|
|
190
|
+
|
|
159
191
|
## Key Points
|
|
160
192
|
|
|
161
193
|
- **Contract is just a TypeScript type** - No runtime code!
|
|
@@ -165,3 +197,4 @@ See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.t
|
|
|
165
197
|
- **Opt-in** - Use types when you want them
|
|
166
198
|
- **Simple** - Just pass type to constructor
|
|
167
199
|
- **Autocomplete** - IDE shows available APIs as you type
|
|
200
|
+
- **Testing support** - LambderMSW provides type-safe mocking
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LambderMSW Testing Example
|
|
3
|
+
*
|
|
4
|
+
* This example shows how to use LambderMSW with MSW (Mock Service Worker)
|
|
5
|
+
* to test your Lambder APIs with full type safety.
|
|
6
|
+
*
|
|
7
|
+
* To run this example:
|
|
8
|
+
* 1. Install MSW: npm install msw --save-dev
|
|
9
|
+
* 2. Install a test runner: npm install vitest --save-dev
|
|
10
|
+
* 3. Create this file in your test directory
|
|
11
|
+
*
|
|
12
|
+
* NOTE: This is an example file. Type errors are expected since dependencies
|
|
13
|
+
* may not be installed in the examples directory. Copy this to your project's
|
|
14
|
+
* test directory to use it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// @ts-nocheck - Example file, types may not be available
|
|
18
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
19
|
+
import { setupServer } from 'msw/node';
|
|
20
|
+
import { LambderMSW, LambderCaller, type ApiContract } from '../src/index.ts';
|
|
21
|
+
|
|
22
|
+
// Define your API contract
|
|
23
|
+
type TestApiContract = ApiContract<{
|
|
24
|
+
getUserById: {
|
|
25
|
+
input: { userId: string };
|
|
26
|
+
output: { id: string; name: string; email: string } | null;
|
|
27
|
+
};
|
|
28
|
+
createUser: {
|
|
29
|
+
input: { name: string; email: string };
|
|
30
|
+
output: { id: string; name: string; email: string };
|
|
31
|
+
};
|
|
32
|
+
getUsers: {
|
|
33
|
+
input: { limit?: number };
|
|
34
|
+
output: Array<{ id: string; name: string; email: string }>;
|
|
35
|
+
};
|
|
36
|
+
deleteUser: {
|
|
37
|
+
input: { userId: string };
|
|
38
|
+
output: { success: boolean };
|
|
39
|
+
};
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
// Setup LambderMSW with type safety
|
|
43
|
+
const lambderMSW = new LambderMSW<TestApiContract>({
|
|
44
|
+
apiPath: '/secure',
|
|
45
|
+
apiVersion: '1.0.0',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Create mock handlers
|
|
49
|
+
const handlers = [
|
|
50
|
+
// Mock getUserById - returns user or null
|
|
51
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
52
|
+
const users: Record<string, { id: string; name: string; email: string }> = {
|
|
53
|
+
'1': { id: '1', name: 'John Doe', email: 'john@example.com' },
|
|
54
|
+
'2': { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return users[payload?.userId || ''] || null;
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
// Mock createUser - simulates creating a user
|
|
61
|
+
lambderMSW.mockApi('createUser', async (payload) => {
|
|
62
|
+
return {
|
|
63
|
+
id: Math.random().toString(36).substring(7),
|
|
64
|
+
name: payload?.name || '',
|
|
65
|
+
email: payload?.email || '',
|
|
66
|
+
};
|
|
67
|
+
}, {
|
|
68
|
+
delay: 100, // Simulate 100ms network delay
|
|
69
|
+
message: 'User created successfully'
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
// Mock getUsers - returns a list of users
|
|
73
|
+
lambderMSW.mockApi('getUsers', async (payload) => {
|
|
74
|
+
const allUsers = [
|
|
75
|
+
{ id: '1', name: 'John Doe', email: 'john@example.com' },
|
|
76
|
+
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
|
|
77
|
+
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com' },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const limit = payload?.limit || 10;
|
|
81
|
+
return allUsers.slice(0, limit);
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
// Mock deleteUser - simulate authorization error
|
|
85
|
+
lambderMSW.mockNotAuthorized('deleteUser'),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Setup MSW server
|
|
89
|
+
const server = setupServer(...handlers);
|
|
90
|
+
|
|
91
|
+
// Setup LambderCaller
|
|
92
|
+
const caller = new LambderCaller<TestApiContract>({
|
|
93
|
+
apiPath: '/secure',
|
|
94
|
+
isCorsEnabled: false,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Test suite
|
|
98
|
+
describe('LambderMSW Testing Example', () => {
|
|
99
|
+
beforeAll(() => {
|
|
100
|
+
server.listen({ onUnhandledRequest: 'error' });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
server.resetHandlers();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
server.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should fetch user by id', async () => {
|
|
112
|
+
const user = await caller.api('getUserById', { userId: '1' });
|
|
113
|
+
|
|
114
|
+
expect(user).toEqual({
|
|
115
|
+
id: '1',
|
|
116
|
+
name: 'John Doe',
|
|
117
|
+
email: 'john@example.com',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return null for non-existent user', async () => {
|
|
122
|
+
const user = await caller.api('getUserById', { userId: '999' });
|
|
123
|
+
|
|
124
|
+
expect(user).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should create a new user with delay', async () => {
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
|
|
130
|
+
const newUser = await caller.api('createUser', {
|
|
131
|
+
name: 'Alice Wonder',
|
|
132
|
+
email: 'alice@example.com',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const endTime = Date.now();
|
|
136
|
+
|
|
137
|
+
expect(newUser?.name).toBe('Alice Wonder');
|
|
138
|
+
expect(newUser?.email).toBe('alice@example.com');
|
|
139
|
+
expect(newUser?.id).toBeDefined();
|
|
140
|
+
|
|
141
|
+
// Check that delay was applied (at least 100ms)
|
|
142
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(100);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should fetch list of users', async () => {
|
|
146
|
+
const users = await caller.api('getUsers', { limit: 2 });
|
|
147
|
+
|
|
148
|
+
expect(users).toHaveLength(2);
|
|
149
|
+
expect(users?.[0]?.name).toBe('John Doe');
|
|
150
|
+
expect(users?.[1]?.name).toBe('Jane Smith');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle authorization error', async () => {
|
|
154
|
+
let errorCaught = false;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await caller.api('deleteUser', { userId: '1' });
|
|
158
|
+
} catch (error: any) {
|
|
159
|
+
errorCaught = true;
|
|
160
|
+
expect(error.notAuthorized).toBe(true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
expect(errorCaught).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should override handler for specific test', async () => {
|
|
167
|
+
// Override the getUserById handler for this test only
|
|
168
|
+
server.use(
|
|
169
|
+
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
170
|
+
return {
|
|
171
|
+
id: payload?.userId || '',
|
|
172
|
+
name: 'Override User',
|
|
173
|
+
email: 'override@example.com',
|
|
174
|
+
};
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const user = await caller.api('getUserById', { userId: '999' });
|
|
179
|
+
|
|
180
|
+
expect(user?.name).toBe('Override User');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should simulate session expired', async () => {
|
|
184
|
+
// Add a handler that simulates session expiration
|
|
185
|
+
server.use(
|
|
186
|
+
lambderMSW.mockSessionExpired('getUserById')
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
let errorCaught = false;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await caller.api('getUserById', { userId: '1' });
|
|
193
|
+
} catch (error: any) {
|
|
194
|
+
errorCaught = true;
|
|
195
|
+
expect(error.sessionExpired).toBe(true);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
expect(errorCaught).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should simulate custom error', async () => {
|
|
202
|
+
server.use(
|
|
203
|
+
lambderMSW.mockError('createUser', 'Email already exists')
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
let errorCaught = false;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await caller.api('createUser', {
|
|
210
|
+
name: 'Duplicate',
|
|
211
|
+
email: 'john@example.com',
|
|
212
|
+
});
|
|
213
|
+
} catch (error: any) {
|
|
214
|
+
errorCaught = true;
|
|
215
|
+
expect(error.errorMessage).toBe('Email already exists');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
expect(errorCaught).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Example: Testing with dynamic responses
|
|
223
|
+
describe('Dynamic Response Testing', () => {
|
|
224
|
+
beforeAll(() => server.listen());
|
|
225
|
+
afterEach(() => server.resetHandlers());
|
|
226
|
+
afterAll(() => server.close());
|
|
227
|
+
|
|
228
|
+
it('should handle different query parameters', async () => {
|
|
229
|
+
server.use(
|
|
230
|
+
lambderMSW.mockApi('getUsers', async (payload) => {
|
|
231
|
+
const limit = payload?.limit || 10;
|
|
232
|
+
|
|
233
|
+
// Generate mock users based on limit
|
|
234
|
+
return Array.from({ length: limit }, (_, i) => ({
|
|
235
|
+
id: `${i + 1}`,
|
|
236
|
+
name: `User ${i + 1}`,
|
|
237
|
+
email: `user${i + 1}@example.com`,
|
|
238
|
+
}));
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const users3 = await caller.api('getUsers', { limit: 3 });
|
|
243
|
+
expect(users3).toHaveLength(3);
|
|
244
|
+
|
|
245
|
+
const users5 = await caller.api('getUsers', { limit: 5 });
|
|
246
|
+
expect(users5).toHaveLength(5);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Example: Testing error conditions
|
|
251
|
+
describe('Error Handling', () => {
|
|
252
|
+
beforeAll(() => server.listen());
|
|
253
|
+
afterEach(() => server.resetHandlers());
|
|
254
|
+
afterAll(() => server.close());
|
|
255
|
+
|
|
256
|
+
it('should handle handler throwing error', async () => {
|
|
257
|
+
server.use(
|
|
258
|
+
lambderMSW.mockApi('getUserById', async () => {
|
|
259
|
+
throw new Error('Database connection failed');
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
let errorCaught = false;
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await caller.api('getUserById', { userId: '1' });
|
|
267
|
+
} catch (error: any) {
|
|
268
|
+
errorCaught = true;
|
|
269
|
+
expect(error.errorMessage).toBe('Database connection failed');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
expect(errorCaught).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
console.log('✅ LambderMSW example tests configured!');
|
|
277
|
+
console.log('📖 See docs/LAMBDER_MSW.md for more information');
|
package/package.json
CHANGED
package/src/LambderMSW.ts
CHANGED
|
@@ -50,52 +50,68 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
50
50
|
* @param apiName - The name of the API to mock
|
|
51
51
|
* @param handler - Function that returns the mock payload
|
|
52
52
|
* @param options - Additional response options (session expired, version expired, etc.)
|
|
53
|
+
*
|
|
54
|
+
* Note: TypeScript will check that the return type is assignable to the output type,
|
|
55
|
+
* but due to structural typing, extra properties are allowed. Enable strict checks
|
|
56
|
+
* in your tsconfig.json with "noUncheckedIndexedAccess" and "exactOptionalPropertyTypes"
|
|
57
|
+
* for better type safety.
|
|
53
58
|
*/
|
|
54
|
-
mockApi<
|
|
55
|
-
TApiName extends keyof TContract & string,
|
|
56
|
-
TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any,
|
|
57
|
-
TInput = TApiName extends keyof TContract ? TContract[TApiName]['input'] : any
|
|
58
|
-
>(
|
|
59
|
+
mockApi<TApiName extends keyof TContract>(
|
|
59
60
|
apiName: TApiName,
|
|
60
|
-
handler: (payload?:
|
|
61
|
+
handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
|
|
61
62
|
options?: MockApiOptions
|
|
62
63
|
): RequestHandler {
|
|
63
64
|
return this.http.post(this.apiPath, async ({ request }: any) => {
|
|
65
|
+
let body: any;
|
|
66
|
+
|
|
64
67
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
apiVersion: this.apiVersion,
|
|
83
|
-
payload,
|
|
84
|
-
...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
|
|
85
|
-
...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
|
|
86
|
-
...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
|
|
87
|
-
...(options?.message ? { message: options.message } : {}),
|
|
88
|
-
...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
|
|
89
|
-
...(options?.logList?.length ? { logList: options.logList } : {}),
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
return this.HttpResponse.json(response);
|
|
93
|
-
}
|
|
94
|
-
|
|
68
|
+
body = await request.json();
|
|
69
|
+
} catch (parseErr) {
|
|
70
|
+
// If JSON parsing fails, return undefined to let other handlers try
|
|
71
|
+
console.warn("LambderMSW: Failed to parse request body as JSON");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if body is valid and has apiName
|
|
76
|
+
if (!body || typeof body.apiName !== 'string') {
|
|
77
|
+
// Invalid request format, let other handlers try
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("LambderMSW called for:", body.apiName, "matching against:", apiName);
|
|
82
|
+
|
|
83
|
+
// Check if this is the API we're mocking
|
|
84
|
+
if (body.apiName !== apiName) {
|
|
95
85
|
// If this handler doesn't match, return undefined to let MSW try other handlers
|
|
96
86
|
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Add artificial delay if specified
|
|
91
|
+
if (options?.delay) {
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, options.delay));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Call the handler with the payload from the request
|
|
96
|
+
const payload = await handler(body.payload as TContract[TApiName]['input']);
|
|
97
|
+
|
|
98
|
+
console.log("Matched! Returning payload for:", apiName);
|
|
99
|
+
|
|
100
|
+
const response: MockApiResponse<TContract[TApiName]['output']> = {
|
|
101
|
+
apiVersion: this.apiVersion,
|
|
102
|
+
payload,
|
|
103
|
+
...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
|
|
104
|
+
...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
|
|
105
|
+
...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
|
|
106
|
+
...(options?.message ? { message: options.message } : {}),
|
|
107
|
+
...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
|
|
108
|
+
...(options?.logList?.length ? { logList: options.logList } : {}),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return this.HttpResponse.json(response);
|
|
97
112
|
} catch (err: any) {
|
|
98
|
-
|
|
113
|
+
// Only handle errors that occur during handler execution for matched APIs
|
|
114
|
+
console.error("Error in LambderMSW handler for", apiName, ":", err);
|
|
99
115
|
|
|
100
116
|
const errorResponse: MockApiResponse<null> = {
|
|
101
117
|
apiVersion: this.apiVersion,
|
|
@@ -111,7 +127,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
111
127
|
/**
|
|
112
128
|
* Mock an API endpoint that returns a session expired error
|
|
113
129
|
*/
|
|
114
|
-
mockSessionExpired<TApiName extends keyof TContract
|
|
130
|
+
mockSessionExpired<TApiName extends keyof TContract>(
|
|
115
131
|
apiName: TApiName
|
|
116
132
|
): RequestHandler {
|
|
117
133
|
return this.mockApi(
|
|
@@ -124,7 +140,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
124
140
|
/**
|
|
125
141
|
* Mock an API endpoint that returns a version expired error
|
|
126
142
|
*/
|
|
127
|
-
mockVersionExpired<TApiName extends keyof TContract
|
|
143
|
+
mockVersionExpired<TApiName extends keyof TContract>(
|
|
128
144
|
apiName: TApiName
|
|
129
145
|
): RequestHandler {
|
|
130
146
|
return this.mockApi(
|
|
@@ -137,7 +153,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
137
153
|
/**
|
|
138
154
|
* Mock an API endpoint that returns a not authorized error
|
|
139
155
|
*/
|
|
140
|
-
mockNotAuthorized<TApiName extends keyof TContract
|
|
156
|
+
mockNotAuthorized<TApiName extends keyof TContract>(
|
|
141
157
|
apiName: TApiName
|
|
142
158
|
): RequestHandler {
|
|
143
159
|
return this.mockApi(
|
|
@@ -150,7 +166,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
150
166
|
/**
|
|
151
167
|
* Mock an API endpoint that returns an error message
|
|
152
168
|
*/
|
|
153
|
-
mockError<TApiName extends keyof TContract
|
|
169
|
+
mockError<TApiName extends keyof TContract>(
|
|
154
170
|
apiName: TApiName,
|
|
155
171
|
errorMessage: string
|
|
156
172
|
): RequestHandler {
|
|
@@ -164,13 +180,9 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
164
180
|
/**
|
|
165
181
|
* Mock an API endpoint with a custom message
|
|
166
182
|
*/
|
|
167
|
-
mockWithMessage<
|
|
168
|
-
TApiName extends keyof TContract & string,
|
|
169
|
-
TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any,
|
|
170
|
-
TInput = TApiName extends keyof TContract ? TContract[TApiName]['input'] : any
|
|
171
|
-
>(
|
|
183
|
+
mockWithMessage<TApiName extends keyof TContract>(
|
|
172
184
|
apiName: TApiName,
|
|
173
|
-
handler: (payload?:
|
|
185
|
+
handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
|
|
174
186
|
message: any
|
|
175
187
|
): RequestHandler {
|
|
176
188
|
return this.mockApi(apiName, handler, { message });
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Test file to verify LambderMSW type enforcement
|
|
2
|
+
import LambderMSW from './src/LambderMSW';
|
|
3
|
+
import type { ApiContract } from './src/LambderApiContract';
|
|
4
|
+
|
|
5
|
+
// Define a test API contract
|
|
6
|
+
type TestContract = ApiContract<{
|
|
7
|
+
'public.getInitialPageData': {
|
|
8
|
+
input: void;
|
|
9
|
+
output: {
|
|
10
|
+
userLocationData: {
|
|
11
|
+
country: string;
|
|
12
|
+
region: string;
|
|
13
|
+
regionCode: string;
|
|
14
|
+
city: string;
|
|
15
|
+
zipCode: string;
|
|
16
|
+
lat: number;
|
|
17
|
+
lng: number;
|
|
18
|
+
};
|
|
19
|
+
sessionUser: {
|
|
20
|
+
id: string;
|
|
21
|
+
username: string;
|
|
22
|
+
email: string;
|
|
23
|
+
name: string;
|
|
24
|
+
bio: string;
|
|
25
|
+
// NOTE: 's' property should NOT be here
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
const apiMocker = new LambderMSW<TestContract>({
|
|
32
|
+
apiPath: '/secure',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// TEST 1: Extra properties - TypeScript allows this due to structural typing
|
|
36
|
+
const handler1 = apiMocker.mockApi(
|
|
37
|
+
'public.getInitialPageData',
|
|
38
|
+
async () => {
|
|
39
|
+
return {
|
|
40
|
+
userLocationData: {
|
|
41
|
+
country: 'United States',
|
|
42
|
+
region: 'California',
|
|
43
|
+
regionCode: 'CA',
|
|
44
|
+
city: 'San Francisco',
|
|
45
|
+
zipCode: '94102',
|
|
46
|
+
lat: 37.7749,
|
|
47
|
+
lng: -122.4194,
|
|
48
|
+
},
|
|
49
|
+
sessionUser: {
|
|
50
|
+
id: 'mock-user-123',
|
|
51
|
+
username: 'mockuser',
|
|
52
|
+
email: 'mock@example.com',
|
|
53
|
+
name: 'Mock User',
|
|
54
|
+
bio: 'This is a mock user for development',
|
|
55
|
+
s: 43 // ⚠️ Extra property - TypeScript structural typing allows this
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// TEST 2: Missing required property - THIS WILL CAUSE AN ERROR!
|
|
62
|
+
const handler2 = apiMocker.mockApi(
|
|
63
|
+
'public.getInitialPageData',
|
|
64
|
+
async () => {
|
|
65
|
+
return {
|
|
66
|
+
userLocationData: {
|
|
67
|
+
country: 'United States',
|
|
68
|
+
region: 'California',
|
|
69
|
+
regionCode: 'CA',
|
|
70
|
+
city: 'San Francisco',
|
|
71
|
+
zipCode: '94102',
|
|
72
|
+
lat: 37.7749,
|
|
73
|
+
lng: -122.4194,
|
|
74
|
+
},
|
|
75
|
+
sessionUser: {
|
|
76
|
+
id: 'mock-user-123',
|
|
77
|
+
username: 'mockuser',
|
|
78
|
+
email: 'mock@example.com',
|
|
79
|
+
name: 'Mock User',
|
|
80
|
+
// bio: 'This is a mock user for development', // ❌ Missing required property - ERROR!
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// TEST 3: Wrong type - THIS WILL CAUSE AN ERROR!
|
|
87
|
+
const handler3 = apiMocker.mockApi(
|
|
88
|
+
'public.getInitialPageData',
|
|
89
|
+
async () => {
|
|
90
|
+
return {
|
|
91
|
+
userLocationData: {
|
|
92
|
+
country: 'United States',
|
|
93
|
+
region: 'California',
|
|
94
|
+
regionCode: 'CA',
|
|
95
|
+
city: 'San Francisco',
|
|
96
|
+
zipCode: '94102',
|
|
97
|
+
lat: 37.7749,
|
|
98
|
+
lng: -122.4194,
|
|
99
|
+
},
|
|
100
|
+
sessionUser: {
|
|
101
|
+
id: 123, // ❌ Wrong type - should be string - ERROR!
|
|
102
|
+
username: 'mockuser',
|
|
103
|
+
email: 'mock@example.com',
|
|
104
|
+
name: 'Mock User',
|
|
105
|
+
bio: 'This is a mock user for development',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
console.log('Check for TypeScript errors above!');
|