@zze/mock-server 0.2.6 → 0.5.0
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 +117 -28
- package/dist/__tests__/mock-server.test.js +52 -0
- package/dist/admin/__tests__/admin-router.test.js +0 -151
- package/dist/admin/admin-router.d.ts.map +1 -1
- package/dist/admin/admin-router.js +0 -205
- package/dist/admin/types.d.ts +0 -84
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/admin/types.js +1 -5
- package/dist/config/config-writer.d.ts +1 -35
- package/dist/config/config-writer.d.ts.map +1 -1
- package/dist/config/config-writer.js +0 -103
- package/dist/mock-server.d.ts +9 -0
- package/dist/mock-server.d.ts.map +1 -1
- package/dist/mock-server.js +3 -0
- package/dist/resolver/__tests__/response-resolver.test.js +134 -0
- package/dist/resolver/response-resolver.d.ts +18 -2
- package/dist/resolver/response-resolver.d.ts.map +1 -1
- package/dist/resolver/response-resolver.js +62 -5
- package/dist/resolver/types.d.ts +8 -0
- package/dist/resolver/types.d.ts.map +1 -1
- package/dist/resolver/types.js +7 -0
- package/dist/schemas/__tests__/endpoint.schema.test.js +48 -0
- package/dist/schemas/endpoint.schema.d.ts +31 -8
- package/dist/schemas/endpoint.schema.d.ts.map +1 -1
- package/dist/schemas/scenario.schema.d.ts +28 -4
- package/dist/schemas/scenario.schema.d.ts.map +1 -1
- package/dist/schemas/scenario.schema.js +23 -3
- package/dist/server/__tests__/mock-server-app.test.js +37 -0
- package/dist/server/mock-server-app.d.ts.map +1 -1
- package/dist/server/mock-server-app.js +5 -1
- package/dist/server/types.d.ts +8 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +1 -0
- package/dist/ui/assets/{index-rj7m-XdG.css → index-C5BZSa2z.css} +1 -1
- package/dist/ui/assets/index-DY4CS66u.js +9 -0
- package/dist/ui/index.html +2 -2
- package/package.json +6 -4
- package/dist/ui/assets/index-SADMOgPE.js +0 -10
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ A reusable Node.js mock server for simulating backend APIs during local microfro
|
|
|
8
8
|
- 🔄 **Hot reload** - Automatic config reloading on file changes
|
|
9
9
|
- 🎭 **Scenarios** - Define multiple response scenarios per endpoint
|
|
10
10
|
- 🌊 **Flows** - Activate groups of scenarios to simulate user journeys
|
|
11
|
+
- 📁 **File download** - Serve binary files (PDFs, images, CSVs) from scenarios
|
|
12
|
+
- 📤 **File upload** - Accept `multipart/form-data` upload requests out of the box
|
|
11
13
|
- 🎨 **Admin UI** - Visual dashboard at `/mock-admin` to control the server
|
|
12
14
|
- 🔌 **REST API** - Programmatic control via admin endpoints
|
|
13
15
|
- 📦 **Single package** - Everything bundled, no external dependencies needed
|
|
@@ -138,12 +140,15 @@ interface ScenarioConfig {
|
|
|
138
140
|
id: string; // Unique within endpoint
|
|
139
141
|
name: string; // Display name in admin UI
|
|
140
142
|
status: number; // HTTP status code (100-599)
|
|
141
|
-
body?: any; // Response body (JSON or function)
|
|
143
|
+
body?: any; // Response body (JSON or function) — mutually exclusive with file
|
|
144
|
+
file?: string; // File path relative to configPath — mutually exclusive with body
|
|
142
145
|
headers?: Record<string, string>; // Custom response headers
|
|
143
146
|
delay?: number; // Response delay in ms
|
|
144
147
|
}
|
|
145
148
|
```
|
|
146
149
|
|
|
150
|
+
> **Note:** `body` and `file` are mutually exclusive. Setting both will cause a validation error at startup.
|
|
151
|
+
|
|
147
152
|
### Flow Config
|
|
148
153
|
|
|
149
154
|
Flows activate multiple scenarios at once to simulate complex user journeys.
|
|
@@ -177,6 +182,107 @@ interface FlowConfig {
|
|
|
177
182
|
|
|
178
183
|
---
|
|
179
184
|
|
|
185
|
+
## File Download
|
|
186
|
+
|
|
187
|
+
A scenario can serve a file instead of JSON by specifying a `file` path relative to `configPath`:
|
|
188
|
+
|
|
189
|
+
**mock-config/endpoints/reports.json**
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"id": "get-report",
|
|
193
|
+
"path": "/reports/:id",
|
|
194
|
+
"method": "GET",
|
|
195
|
+
"defaultScenarioId": "pdf",
|
|
196
|
+
"scenarios": [
|
|
197
|
+
{
|
|
198
|
+
"id": "pdf",
|
|
199
|
+
"name": "PDF Report",
|
|
200
|
+
"status": 200,
|
|
201
|
+
"file": "./assets/sample-report.pdf",
|
|
202
|
+
"headers": { "Content-Disposition": "attachment; filename=report.pdf" }
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "not-found",
|
|
206
|
+
"name": "Not Found",
|
|
207
|
+
"status": 404,
|
|
208
|
+
"body": { "error": "Report not found" }
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Place the file at `mock-config/assets/sample-report.pdf`. The `Content-Type` header is inferred automatically from the file extension if not set explicitly.
|
|
215
|
+
|
|
216
|
+
> `body` and `file` are mutually exclusive — setting both causes a validation error at startup.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## File Upload
|
|
221
|
+
|
|
222
|
+
Upload endpoints work out of the box — no special configuration needed. The server accepts `multipart/form-data` requests, discards the uploaded files, and returns the active scenario response:
|
|
223
|
+
|
|
224
|
+
**mock-config/endpoints/avatar.json**
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"id": "upload-avatar",
|
|
228
|
+
"path": "/users/:id/avatar",
|
|
229
|
+
"method": "POST",
|
|
230
|
+
"defaultScenarioId": "success",
|
|
231
|
+
"scenarios": [
|
|
232
|
+
{
|
|
233
|
+
"id": "success",
|
|
234
|
+
"name": "Upload OK",
|
|
235
|
+
"status": 200,
|
|
236
|
+
"body": { "url": "/cdn/avatar.jpg" }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"id": "too-large",
|
|
240
|
+
"name": "File Too Large",
|
|
241
|
+
"status": 413,
|
|
242
|
+
"body": { "error": "File exceeds maximum size" }
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Global Delay
|
|
251
|
+
|
|
252
|
+
Add a baseline latency to every response without modifying individual scenarios:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const server = new MockServer({
|
|
256
|
+
configPath: './mock-config',
|
|
257
|
+
globalDelay: 200, // every response waits 200 ms
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Or use a random range to simulate variable network conditions:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const server = new MockServer({
|
|
265
|
+
configPath: './mock-config',
|
|
266
|
+
globalDelay: [100, 500], // each response waits a random 100–500 ms
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Priority:** a scenario's own `delay` field always takes precedence. `globalDelay` is only used when the resolved scenario has no `delay` set.
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"id": "fast-scenario",
|
|
275
|
+
"name": "Always fast",
|
|
276
|
+
"status": 200,
|
|
277
|
+
"delay": 0,
|
|
278
|
+
"body": { "ok": true }
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
In the example above, `delay: 0` overrides any `globalDelay` — the response is immediate.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
180
286
|
## TypeScript Configs
|
|
181
287
|
|
|
182
288
|
You can write configs in TypeScript for type safety and dynamic responses:
|
|
@@ -218,9 +324,6 @@ export default endpoint;
|
|
|
218
324
|
The built-in admin UI is available at `/mock-admin` and provides:
|
|
219
325
|
|
|
220
326
|
- **Endpoints Panel**: View all endpoints, expand to see scenarios, click to activate
|
|
221
|
-
- Create new endpoints with the JSON editor
|
|
222
|
-
- Edit existing endpoint configurations
|
|
223
|
-
- Delete endpoints with confirmation
|
|
224
327
|
- **Flows Panel**: View and activate user flows with category grouping
|
|
225
328
|
- Create new flows with the endpoint/scenario picker
|
|
226
329
|
- Edit or duplicate existing flows
|
|
@@ -248,13 +351,6 @@ Control the mock server programmatically:
|
|
|
248
351
|
| POST | `/mock-admin/api/flows/activate` | Activate/deactivate a flow |
|
|
249
352
|
| POST | `/mock-admin/api/reset` | Reset to default state |
|
|
250
353
|
|
|
251
|
-
### Endpoint CRUD
|
|
252
|
-
|
|
253
|
-
| Method | Endpoint | Description |
|
|
254
|
-
|--------|----------|-------------|
|
|
255
|
-
| POST | `/mock-admin/api/endpoints` | Create a new endpoint |
|
|
256
|
-
| PUT | `/mock-admin/api/endpoints/:id` | Update an existing endpoint |
|
|
257
|
-
| DELETE | `/mock-admin/api/endpoints/:id` | Delete an endpoint |
|
|
258
354
|
|
|
259
355
|
### Flow CRUD
|
|
260
356
|
|
|
@@ -289,19 +385,6 @@ curl -X POST http://localhost:3001/mock-admin/api/flows/activate \
|
|
|
289
385
|
# Reset to defaults
|
|
290
386
|
curl -X POST http://localhost:3001/mock-admin/api/reset
|
|
291
387
|
|
|
292
|
-
# Create a new endpoint
|
|
293
|
-
curl -X POST http://localhost:3001/mock-admin/api/endpoints \
|
|
294
|
-
-H "Content-Type: application/json" \
|
|
295
|
-
-d '{"endpoint": {"id": "new-endpoint", "method": "GET", "path": "/api/test", "scenarios": [{"id": "default", "name": "Default", "response": {"status": 200, "body": {}}}]}}'
|
|
296
|
-
|
|
297
|
-
# Update an endpoint
|
|
298
|
-
curl -X PUT http://localhost:3001/mock-admin/api/endpoints/new-endpoint \
|
|
299
|
-
-H "Content-Type: application/json" \
|
|
300
|
-
-d '{"endpoint": {"id": "new-endpoint", "method": "GET", "path": "/api/test-updated", "scenarios": [{"id": "default", "name": "Default", "response": {"status": 200, "body": {"updated": true}}}]}}'
|
|
301
|
-
|
|
302
|
-
# Delete an endpoint
|
|
303
|
-
curl -X DELETE http://localhost:3001/mock-admin/api/endpoints/new-endpoint
|
|
304
|
-
|
|
305
388
|
# Create a new flow
|
|
306
389
|
curl -X POST http://localhost:3001/mock-admin/api/flows \
|
|
307
390
|
-H "Content-Type: application/json" \
|
|
@@ -328,13 +411,13 @@ curl -X DELETE http://localhost:3001/mock-admin/api/flows/my-flow
|
|
|
328
411
|
### MockServer Class
|
|
329
412
|
|
|
330
413
|
```typescript
|
|
331
|
-
import { MockServer } from '@zze/mock-server';
|
|
332
|
-
|
|
333
414
|
const server = new MockServer({
|
|
334
|
-
configPath: './mock-config',
|
|
415
|
+
configPath: './mock-config',
|
|
335
416
|
port: 3001, // Default: 3001
|
|
336
417
|
apiPrefix: '/api', // Default: '/api'
|
|
337
|
-
hotReload: true
|
|
418
|
+
hotReload: true, // Default: true
|
|
419
|
+
globalDelay: 200 // Optional: add 200 ms to every response (unless scenario overrides)
|
|
420
|
+
// globalDelay: [100, 500] // Or a random range in ms
|
|
338
421
|
});
|
|
339
422
|
```
|
|
340
423
|
|
|
@@ -592,6 +675,12 @@ interface MockServerOptions {
|
|
|
592
675
|
apiPrefix?: string;
|
|
593
676
|
hotReload?: boolean;
|
|
594
677
|
watcherOptions?: WatcherOptions;
|
|
678
|
+
/**
|
|
679
|
+
* Global delay fallback for all responses.
|
|
680
|
+
* Scenario-level `delay` always overrides this.
|
|
681
|
+
* A number is a fixed ms value; a tuple is a [min, max] random range.
|
|
682
|
+
*/
|
|
683
|
+
globalDelay?: number | [number, number];
|
|
595
684
|
}
|
|
596
685
|
|
|
597
686
|
// Server state
|
|
@@ -596,4 +596,56 @@ const supertest_1 = __importDefault(require("supertest"));
|
|
|
596
596
|
(0, vitest_1.expect)(mockResponse.status).toBe(500);
|
|
597
597
|
});
|
|
598
598
|
});
|
|
599
|
+
(0, vitest_1.describe)('globalDelay option', () => {
|
|
600
|
+
let server;
|
|
601
|
+
(0, vitest_1.afterEach)(async () => {
|
|
602
|
+
if (server?.getState().running) {
|
|
603
|
+
await server.stop();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
(0, vitest_1.it)('should apply static globalDelay to responses', async () => {
|
|
607
|
+
await createEndpoint('delay-ep', '/delay-ep', 'GET', [
|
|
608
|
+
{ id: 'default', name: 'Default', status: 200, body: { ok: true }, isDefault: true },
|
|
609
|
+
]);
|
|
610
|
+
server = new mock_server_js_1.MockServer({
|
|
611
|
+
configPath: configDir,
|
|
612
|
+
port: 3040,
|
|
613
|
+
hotReload: false,
|
|
614
|
+
globalDelay: 80,
|
|
615
|
+
});
|
|
616
|
+
await server.start();
|
|
617
|
+
const app = server.getApp();
|
|
618
|
+
const start = Date.now();
|
|
619
|
+
const res = await (0, supertest_1.default)(app).get('/api/delay-ep');
|
|
620
|
+
const elapsed = Date.now() - start;
|
|
621
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
622
|
+
(0, vitest_1.expect)(elapsed).toBeGreaterThanOrEqual(70); // 80 ms with small margin
|
|
623
|
+
});
|
|
624
|
+
(0, vitest_1.it)('scenario-level delay should take precedence over globalDelay', async () => {
|
|
625
|
+
await createEndpoint('prio-ep', '/prio-ep', 'GET', [
|
|
626
|
+
{
|
|
627
|
+
id: 'fast',
|
|
628
|
+
name: 'Fast',
|
|
629
|
+
status: 200,
|
|
630
|
+
body: { ok: true },
|
|
631
|
+
// @ts-ignore - delay is valid in EndpointConfig scenarios
|
|
632
|
+
delay: 0,
|
|
633
|
+
isDefault: true,
|
|
634
|
+
},
|
|
635
|
+
]);
|
|
636
|
+
server = new mock_server_js_1.MockServer({
|
|
637
|
+
configPath: configDir,
|
|
638
|
+
port: 3041,
|
|
639
|
+
hotReload: false,
|
|
640
|
+
globalDelay: 5000, // would time out the test if used
|
|
641
|
+
});
|
|
642
|
+
await server.start();
|
|
643
|
+
const app = server.getApp();
|
|
644
|
+
const start = Date.now();
|
|
645
|
+
const res = await (0, supertest_1.default)(app).get('/api/prio-ep');
|
|
646
|
+
const elapsed = Date.now() - start;
|
|
647
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
648
|
+
(0, vitest_1.expect)(elapsed).toBeLessThan(300); // scenario delay=0 wins, should be fast
|
|
649
|
+
});
|
|
650
|
+
});
|
|
599
651
|
});
|
|
@@ -281,131 +281,6 @@ const mockFlows = [
|
|
|
281
281
|
.expect(200);
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
|
-
(0, vitest_1.describe)('Endpoint CRUD operations', () => {
|
|
285
|
-
let mockConfigWriter;
|
|
286
|
-
let onConfigChangeCalled;
|
|
287
|
-
(0, vitest_1.beforeEach)(() => {
|
|
288
|
-
onConfigChangeCalled = false;
|
|
289
|
-
mockConfigWriter = {
|
|
290
|
-
writeEndpoint: vitest_1.vi.fn().mockResolvedValue({ success: true, filePath: '/test/endpoint.json' }),
|
|
291
|
-
writeFlow: vitest_1.vi.fn().mockResolvedValue({ success: true, filePath: '/test/flow.json' }),
|
|
292
|
-
deleteEndpoint: vitest_1.vi.fn().mockResolvedValue({ success: true }),
|
|
293
|
-
deleteFlow: vitest_1.vi.fn().mockResolvedValue({ success: true }),
|
|
294
|
-
getEndpointFilePath: vitest_1.vi.fn().mockReturnValue('/test/endpoint.json'),
|
|
295
|
-
getFlowFilePath: vitest_1.vi.fn().mockReturnValue('/test/flow.json'),
|
|
296
|
-
findEndpointFile: vitest_1.vi.fn().mockResolvedValue(null),
|
|
297
|
-
findFlowFile: vitest_1.vi.fn().mockResolvedValue(null),
|
|
298
|
-
};
|
|
299
|
-
serverApp.setConfigWriter(mockConfigWriter, async () => {
|
|
300
|
-
onConfigChangeCalled = true;
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
(0, vitest_1.describe)('POST /mock-admin/api/endpoints', () => {
|
|
304
|
-
(0, vitest_1.it)('should create a new endpoint', async () => {
|
|
305
|
-
const newEndpoint = {
|
|
306
|
-
id: 'new-endpoint',
|
|
307
|
-
path: '/new',
|
|
308
|
-
method: 'POST',
|
|
309
|
-
scenarios: [
|
|
310
|
-
{ id: 'success', name: 'Success', status: 201, body: { created: true } },
|
|
311
|
-
],
|
|
312
|
-
defaultScenarioId: 'success',
|
|
313
|
-
};
|
|
314
|
-
const res = await (0, supertest_1.default)(serverApp.getApp())
|
|
315
|
-
.post('/mock-admin/api/endpoints')
|
|
316
|
-
.send(newEndpoint)
|
|
317
|
-
.expect(201);
|
|
318
|
-
(0, vitest_1.expect)(res.body.id).toBe('new-endpoint');
|
|
319
|
-
(0, vitest_1.expect)(res.body.filePath).toBeDefined();
|
|
320
|
-
(0, vitest_1.expect)(mockConfigWriter.writeEndpoint).toHaveBeenCalledWith(newEndpoint);
|
|
321
|
-
(0, vitest_1.expect)(onConfigChangeCalled).toBe(true);
|
|
322
|
-
});
|
|
323
|
-
(0, vitest_1.it)('should return 409 if endpoint already exists', async () => {
|
|
324
|
-
const existingEndpoint = {
|
|
325
|
-
id: 'get-users',
|
|
326
|
-
path: '/users',
|
|
327
|
-
method: 'GET',
|
|
328
|
-
scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
|
|
329
|
-
defaultScenarioId: 's',
|
|
330
|
-
};
|
|
331
|
-
const res = await (0, supertest_1.default)(serverApp.getApp())
|
|
332
|
-
.post('/mock-admin/api/endpoints')
|
|
333
|
-
.send(existingEndpoint)
|
|
334
|
-
.expect(409);
|
|
335
|
-
(0, vitest_1.expect)(res.body.error).toContain('already exists');
|
|
336
|
-
});
|
|
337
|
-
(0, vitest_1.it)('should return 400 for invalid endpoint', async () => {
|
|
338
|
-
const res = await (0, supertest_1.default)(serverApp.getApp())
|
|
339
|
-
.post('/mock-admin/api/endpoints')
|
|
340
|
-
.send({ id: 'test' }) // Missing required fields
|
|
341
|
-
.expect(400);
|
|
342
|
-
(0, vitest_1.expect)(res.body.error).toBe('Invalid request body');
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
(0, vitest_1.describe)('PUT /mock-admin/api/endpoints/:id', () => {
|
|
346
|
-
(0, vitest_1.it)('should update an existing endpoint', async () => {
|
|
347
|
-
const updatedEndpoint = {
|
|
348
|
-
id: 'get-users',
|
|
349
|
-
path: '/users/updated',
|
|
350
|
-
method: 'GET',
|
|
351
|
-
scenarios: [
|
|
352
|
-
{ id: 'success', name: 'Success', status: 200, body: { updated: true } },
|
|
353
|
-
],
|
|
354
|
-
defaultScenarioId: 'success',
|
|
355
|
-
};
|
|
356
|
-
const res = await (0, supertest_1.default)(serverApp.getApp())
|
|
357
|
-
.put('/mock-admin/api/endpoints/get-users')
|
|
358
|
-
.send(updatedEndpoint)
|
|
359
|
-
.expect(200);
|
|
360
|
-
(0, vitest_1.expect)(res.body.id).toBe('get-users');
|
|
361
|
-
(0, vitest_1.expect)(mockConfigWriter.writeEndpoint).toHaveBeenCalledWith(updatedEndpoint);
|
|
362
|
-
(0, vitest_1.expect)(onConfigChangeCalled).toBe(true);
|
|
363
|
-
});
|
|
364
|
-
(0, vitest_1.it)('should return 404 if endpoint not found', async () => {
|
|
365
|
-
const endpoint = {
|
|
366
|
-
id: 'nonexistent',
|
|
367
|
-
path: '/test',
|
|
368
|
-
method: 'GET',
|
|
369
|
-
scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
|
|
370
|
-
defaultScenarioId: 's',
|
|
371
|
-
};
|
|
372
|
-
const res = await (0, supertest_1.default)(serverApp.getApp())
|
|
373
|
-
.put('/mock-admin/api/endpoints/nonexistent')
|
|
374
|
-
.send(endpoint)
|
|
375
|
-
.expect(404);
|
|
376
|
-
(0, vitest_1.expect)(res.body.error).toContain('not found');
|
|
377
|
-
});
|
|
378
|
-
(0, vitest_1.it)('should return 400 if ID in body does not match URL', async () => {
|
|
379
|
-
const endpoint = {
|
|
380
|
-
id: 'different-id',
|
|
381
|
-
path: '/users',
|
|
382
|
-
method: 'GET',
|
|
383
|
-
scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
|
|
384
|
-
defaultScenarioId: 's',
|
|
385
|
-
};
|
|
386
|
-
const res = await (0, supertest_1.default)(serverApp.getApp())
|
|
387
|
-
.put('/mock-admin/api/endpoints/get-users')
|
|
388
|
-
.send(endpoint)
|
|
389
|
-
.expect(400);
|
|
390
|
-
(0, vitest_1.expect)(res.body.error).toContain('must match');
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
(0, vitest_1.describe)('DELETE /mock-admin/api/endpoints/:id', () => {
|
|
394
|
-
(0, vitest_1.it)('should delete an existing endpoint', async () => {
|
|
395
|
-
await (0, supertest_1.default)(serverApp.getApp())
|
|
396
|
-
.delete('/mock-admin/api/endpoints/get-users')
|
|
397
|
-
.expect(204);
|
|
398
|
-
(0, vitest_1.expect)(mockConfigWriter.deleteEndpoint).toHaveBeenCalledWith('get-users');
|
|
399
|
-
(0, vitest_1.expect)(onConfigChangeCalled).toBe(true);
|
|
400
|
-
});
|
|
401
|
-
(0, vitest_1.it)('should return 404 if endpoint not found', async () => {
|
|
402
|
-
const res = await (0, supertest_1.default)(serverApp.getApp())
|
|
403
|
-
.delete('/mock-admin/api/endpoints/nonexistent')
|
|
404
|
-
.expect(404);
|
|
405
|
-
(0, vitest_1.expect)(res.body.error).toContain('not found');
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
284
|
(0, vitest_1.describe)('Flow CRUD operations', () => {
|
|
410
285
|
let mockConfigWriter;
|
|
411
286
|
let onConfigChangeCalled;
|
|
@@ -561,32 +436,6 @@ const mockFlows = [
|
|
|
561
436
|
});
|
|
562
437
|
});
|
|
563
438
|
(0, vitest_1.describe)('CRUD operations without ConfigWriter', () => {
|
|
564
|
-
(0, vitest_1.it)('should return 501 for endpoint CRUD when ConfigWriter not configured', async () => {
|
|
565
|
-
const endpoint = {
|
|
566
|
-
id: 'test',
|
|
567
|
-
path: '/test',
|
|
568
|
-
method: 'GET',
|
|
569
|
-
scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
|
|
570
|
-
defaultScenarioId: 's',
|
|
571
|
-
};
|
|
572
|
-
// POST
|
|
573
|
-
let res = await (0, supertest_1.default)(serverApp.getApp())
|
|
574
|
-
.post('/mock-admin/api/endpoints')
|
|
575
|
-
.send(endpoint)
|
|
576
|
-
.expect(501);
|
|
577
|
-
(0, vitest_1.expect)(res.body.error).toContain('not available');
|
|
578
|
-
// PUT
|
|
579
|
-
res = await (0, supertest_1.default)(serverApp.getApp())
|
|
580
|
-
.put('/mock-admin/api/endpoints/get-users')
|
|
581
|
-
.send({ ...endpoint, id: 'get-users' })
|
|
582
|
-
.expect(501);
|
|
583
|
-
(0, vitest_1.expect)(res.body.error).toContain('not available');
|
|
584
|
-
// DELETE
|
|
585
|
-
res = await (0, supertest_1.default)(serverApp.getApp())
|
|
586
|
-
.delete('/mock-admin/api/endpoints/get-users')
|
|
587
|
-
.expect(501);
|
|
588
|
-
(0, vitest_1.expect)(res.body.error).toContain('not available');
|
|
589
|
-
});
|
|
590
439
|
(0, vitest_1.it)('should return 501 for flow CRUD when ConfigWriter not configured', async () => {
|
|
591
440
|
const flow = {
|
|
592
441
|
id: 'test',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-router.d.ts","sourceRoot":"","sources":["../../src/admin/admin-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAmC,MAAM,SAAS,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"admin-router.d.ts","sourceRoot":"","sources":["../../src/admin/admin-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAmC,MAAM,SAAS,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAWvD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,cAAc,EAAE,CAAC;IACrC,QAAQ,EAAE,MAAM,UAAU,EAAE,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,YAAY,GAAG,SAAS,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAAC;AACvE;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,cAAc,EAAE,EACpC,QAAQ,EAAE,MAAM,UAAU,EAAE,EAC5B,YAAY,EAAE,YAAY,GACzB,MAAM,CAAC;AA0cV;;GAEG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC"}
|
|
@@ -140,211 +140,6 @@ function createAdminRouter(optionsOrGetEndpoints, getFlowsArg, stateManagerArg)
|
|
|
140
140
|
res.status(204).end();
|
|
141
141
|
});
|
|
142
142
|
// ============================================
|
|
143
|
-
// Endpoint CRUD Operations
|
|
144
|
-
// ============================================
|
|
145
|
-
/**
|
|
146
|
-
* GET /endpoints/:id
|
|
147
|
-
* Get a single endpoint's full configuration (including scenario bodies)
|
|
148
|
-
*/
|
|
149
|
-
router.get('/endpoints/:id', (req, res) => {
|
|
150
|
-
const { id } = req.params;
|
|
151
|
-
const endpoints = getEndpoints();
|
|
152
|
-
const endpoint = endpoints.find((e) => e.id === id);
|
|
153
|
-
if (!endpoint) {
|
|
154
|
-
const errorResponse = {
|
|
155
|
-
error: 'Endpoint not found',
|
|
156
|
-
details: `No endpoint with id "${id}"`,
|
|
157
|
-
};
|
|
158
|
-
res.status(404).json(errorResponse);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
res.json({
|
|
162
|
-
id: endpoint.id,
|
|
163
|
-
path: endpoint.path,
|
|
164
|
-
method: endpoint.method,
|
|
165
|
-
defaultScenarioId: endpoint.defaultScenarioId,
|
|
166
|
-
scenarios: endpoint.scenarios.map((s) => ({
|
|
167
|
-
id: s.id,
|
|
168
|
-
name: s.name,
|
|
169
|
-
status: s.status,
|
|
170
|
-
body: s.body,
|
|
171
|
-
headers: s.headers,
|
|
172
|
-
delay: s.delay,
|
|
173
|
-
})),
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
/**
|
|
177
|
-
* POST /endpoints
|
|
178
|
-
* Create a new endpoint
|
|
179
|
-
*/
|
|
180
|
-
router.post('/endpoints', async (req, res) => {
|
|
181
|
-
const configWriter = getConfigWriter?.();
|
|
182
|
-
if (!configWriter) {
|
|
183
|
-
const errorResponse = {
|
|
184
|
-
error: 'Endpoint management not available',
|
|
185
|
-
details: 'ConfigWriter not configured',
|
|
186
|
-
};
|
|
187
|
-
res.status(501).json(errorResponse);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const parseResult = types_js_1.EndpointRequestSchema.safeParse(req.body);
|
|
191
|
-
if (!parseResult.success) {
|
|
192
|
-
const errorResponse = {
|
|
193
|
-
error: 'Invalid request body',
|
|
194
|
-
details: parseResult.error.issues,
|
|
195
|
-
};
|
|
196
|
-
res.status(400).json(errorResponse);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const endpoint = parseResult.data;
|
|
200
|
-
// Check if endpoint with this ID already exists
|
|
201
|
-
const existingEndpoint = getEndpoints().find((e) => e.id === endpoint.id);
|
|
202
|
-
if (existingEndpoint) {
|
|
203
|
-
const errorResponse = {
|
|
204
|
-
error: `Endpoint with id '${endpoint.id}' already exists`,
|
|
205
|
-
};
|
|
206
|
-
res.status(409).json(errorResponse);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const result = await configWriter.writeEndpoint(endpoint);
|
|
211
|
-
if (!result.success) {
|
|
212
|
-
const errorResponse = {
|
|
213
|
-
error: 'Failed to create endpoint',
|
|
214
|
-
details: result.error,
|
|
215
|
-
};
|
|
216
|
-
res.status(500).json(errorResponse);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
// Trigger config reload if available
|
|
220
|
-
const onConfigChange = getOnConfigChange?.();
|
|
221
|
-
if (onConfigChange) {
|
|
222
|
-
await onConfigChange();
|
|
223
|
-
}
|
|
224
|
-
res.status(201).json({ id: endpoint.id, filePath: result.filePath });
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
const errorResponse = {
|
|
228
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
229
|
-
};
|
|
230
|
-
res.status(500).json(errorResponse);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
/**
|
|
234
|
-
* PUT /endpoints/:id
|
|
235
|
-
* Update an existing endpoint
|
|
236
|
-
*/
|
|
237
|
-
router.put('/endpoints/:id', async (req, res) => {
|
|
238
|
-
const configWriter = getConfigWriter?.();
|
|
239
|
-
if (!configWriter) {
|
|
240
|
-
const errorResponse = {
|
|
241
|
-
error: 'Endpoint management not available',
|
|
242
|
-
details: 'ConfigWriter not configured',
|
|
243
|
-
};
|
|
244
|
-
res.status(501).json(errorResponse);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
const { id } = req.params;
|
|
248
|
-
const parseResult = types_js_1.EndpointRequestSchema.safeParse(req.body);
|
|
249
|
-
if (!parseResult.success) {
|
|
250
|
-
const errorResponse = {
|
|
251
|
-
error: 'Invalid request body',
|
|
252
|
-
details: parseResult.error.issues,
|
|
253
|
-
};
|
|
254
|
-
res.status(400).json(errorResponse);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
const endpoint = parseResult.data;
|
|
258
|
-
// Ensure the ID in the body matches the URL parameter
|
|
259
|
-
if (endpoint.id !== id) {
|
|
260
|
-
const errorResponse = {
|
|
261
|
-
error: 'Endpoint ID in body must match URL parameter',
|
|
262
|
-
};
|
|
263
|
-
res.status(400).json(errorResponse);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
// Check if endpoint exists
|
|
267
|
-
const existingEndpoint = getEndpoints().find((e) => e.id === id);
|
|
268
|
-
if (!existingEndpoint) {
|
|
269
|
-
const errorResponse = {
|
|
270
|
-
error: `Endpoint with id '${id}' not found`,
|
|
271
|
-
};
|
|
272
|
-
res.status(404).json(errorResponse);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
const result = await configWriter.writeEndpoint(endpoint);
|
|
277
|
-
if (!result.success) {
|
|
278
|
-
const errorResponse = {
|
|
279
|
-
error: 'Failed to update endpoint',
|
|
280
|
-
details: result.error,
|
|
281
|
-
};
|
|
282
|
-
res.status(500).json(errorResponse);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
// Trigger config reload if available
|
|
286
|
-
const onConfigChange = getOnConfigChange?.();
|
|
287
|
-
if (onConfigChange) {
|
|
288
|
-
await onConfigChange();
|
|
289
|
-
}
|
|
290
|
-
res.status(200).json({ id: endpoint.id, filePath: result.filePath });
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
const errorResponse = {
|
|
294
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
295
|
-
};
|
|
296
|
-
res.status(500).json(errorResponse);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
/**
|
|
300
|
-
* DELETE /endpoints/:id
|
|
301
|
-
* Delete an endpoint
|
|
302
|
-
*/
|
|
303
|
-
router.delete('/endpoints/:id', async (req, res) => {
|
|
304
|
-
const configWriter = getConfigWriter?.();
|
|
305
|
-
if (!configWriter) {
|
|
306
|
-
const errorResponse = {
|
|
307
|
-
error: 'Endpoint management not available',
|
|
308
|
-
details: 'ConfigWriter not configured',
|
|
309
|
-
};
|
|
310
|
-
res.status(501).json(errorResponse);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
const { id } = req.params;
|
|
314
|
-
// Check if endpoint exists
|
|
315
|
-
const existingEndpoint = getEndpoints().find((e) => e.id === id);
|
|
316
|
-
if (!existingEndpoint) {
|
|
317
|
-
const errorResponse = {
|
|
318
|
-
error: `Endpoint with id '${id}' not found`,
|
|
319
|
-
};
|
|
320
|
-
res.status(404).json(errorResponse);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
const result = await configWriter.deleteEndpoint(id);
|
|
325
|
-
if (!result.success) {
|
|
326
|
-
const errorResponse = {
|
|
327
|
-
error: 'Failed to delete endpoint',
|
|
328
|
-
details: result.error,
|
|
329
|
-
};
|
|
330
|
-
res.status(500).json(errorResponse);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
// Trigger config reload if available
|
|
334
|
-
const onConfigChange = getOnConfigChange?.();
|
|
335
|
-
if (onConfigChange) {
|
|
336
|
-
await onConfigChange();
|
|
337
|
-
}
|
|
338
|
-
res.status(204).end();
|
|
339
|
-
}
|
|
340
|
-
catch (error) {
|
|
341
|
-
const errorResponse = {
|
|
342
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
343
|
-
};
|
|
344
|
-
res.status(500).json(errorResponse);
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
// ============================================
|
|
348
143
|
// Flow CRUD Operations
|
|
349
144
|
// ============================================
|
|
350
145
|
/**
|