@zze/mock-server 0.2.6 → 0.6.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 +58 -1
- package/dist/admin/__tests__/admin-router.test.js +2 -151
- package/dist/admin/admin-router.d.ts +4 -4
- package/dist/admin/admin-router.d.ts.map +1 -1
- package/dist/admin/admin-router.js +1 -205
- package/dist/admin/types.d.ts +1 -84
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/admin/types.js +1 -5
- package/dist/config/__tests__/config-loader.test.js +18 -0
- package/dist/config/config-loader.d.ts +3 -3
- package/dist/config/config-loader.d.ts.map +1 -1
- package/dist/config/config-loader.js +4 -1
- 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/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/types.d.ts +9 -1
- package/dist/config/types.d.ts.map +1 -1
- 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 +40 -4
- package/dist/server/mock-server-app.d.ts +4 -4
- package/dist/server/mock-server-app.d.ts.map +1 -1
- package/dist/server/mock-server-app.js +5 -3
- 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/state/__tests__/state-manager.test.js +33 -0
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +9 -6
- package/dist/ui/assets/index-C2lhc6Pd.css +1 -0
- package/dist/ui/assets/index-D8nVOwBC.js +9 -0
- package/dist/ui/index.html +2 -2
- package/package.json +5 -3
- package/dist/ui/assets/index-SADMOgPE.js +0 -10
- package/dist/ui/assets/index-rj7m-XdG.css +0 -1
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
|
|
@@ -231,7 +231,10 @@ const supertest_1 = __importDefault(require("supertest"));
|
|
|
231
231
|
}
|
|
232
232
|
});
|
|
233
233
|
(0, vitest_1.it)('should reload configuration manually', async () => {
|
|
234
|
-
await createEndpoint('test1', '/test1'
|
|
234
|
+
await createEndpoint('test1', '/test1', 'GET', [
|
|
235
|
+
{ id: 'success', name: 'Success', status: 200, body: { message: 'ok' }, isDefault: true },
|
|
236
|
+
{ id: 'error', name: 'Error', status: 500, body: { error: 'failed' } },
|
|
237
|
+
]);
|
|
235
238
|
server = new mock_server_js_1.MockServer({
|
|
236
239
|
configPath: configDir,
|
|
237
240
|
port: 3015,
|
|
@@ -239,10 +242,12 @@ const supertest_1 = __importDefault(require("supertest"));
|
|
|
239
242
|
});
|
|
240
243
|
await server.start();
|
|
241
244
|
(0, vitest_1.expect)(server.getEndpoints().length).toBe(1);
|
|
245
|
+
server.activateScenario('test1', 'error');
|
|
242
246
|
// Add another endpoint
|
|
243
247
|
await createEndpoint('test2', '/test2');
|
|
244
248
|
await server.reload();
|
|
245
249
|
(0, vitest_1.expect)(server.getEndpoints().length).toBe(2);
|
|
250
|
+
(0, vitest_1.expect)(server.getState().activeScenarios.test1).toBe('error');
|
|
246
251
|
});
|
|
247
252
|
(0, vitest_1.it)('should throw if server is not running', async () => {
|
|
248
253
|
server = new mock_server_js_1.MockServer({
|
|
@@ -596,4 +601,56 @@ const supertest_1 = __importDefault(require("supertest"));
|
|
|
596
601
|
(0, vitest_1.expect)(mockResponse.status).toBe(500);
|
|
597
602
|
});
|
|
598
603
|
});
|
|
604
|
+
(0, vitest_1.describe)('globalDelay option', () => {
|
|
605
|
+
let server;
|
|
606
|
+
(0, vitest_1.afterEach)(async () => {
|
|
607
|
+
if (server?.getState().running) {
|
|
608
|
+
await server.stop();
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
(0, vitest_1.it)('should apply static globalDelay to responses', async () => {
|
|
612
|
+
await createEndpoint('delay-ep', '/delay-ep', 'GET', [
|
|
613
|
+
{ id: 'default', name: 'Default', status: 200, body: { ok: true }, isDefault: true },
|
|
614
|
+
]);
|
|
615
|
+
server = new mock_server_js_1.MockServer({
|
|
616
|
+
configPath: configDir,
|
|
617
|
+
port: 3040,
|
|
618
|
+
hotReload: false,
|
|
619
|
+
globalDelay: 80,
|
|
620
|
+
});
|
|
621
|
+
await server.start();
|
|
622
|
+
const app = server.getApp();
|
|
623
|
+
const start = Date.now();
|
|
624
|
+
const res = await (0, supertest_1.default)(app).get('/api/delay-ep');
|
|
625
|
+
const elapsed = Date.now() - start;
|
|
626
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
627
|
+
(0, vitest_1.expect)(elapsed).toBeGreaterThanOrEqual(70); // 80 ms with small margin
|
|
628
|
+
});
|
|
629
|
+
(0, vitest_1.it)('scenario-level delay should take precedence over globalDelay', async () => {
|
|
630
|
+
await createEndpoint('prio-ep', '/prio-ep', 'GET', [
|
|
631
|
+
{
|
|
632
|
+
id: 'fast',
|
|
633
|
+
name: 'Fast',
|
|
634
|
+
status: 200,
|
|
635
|
+
body: { ok: true },
|
|
636
|
+
// @ts-ignore - delay is valid in EndpointConfig scenarios
|
|
637
|
+
delay: 0,
|
|
638
|
+
isDefault: true,
|
|
639
|
+
},
|
|
640
|
+
]);
|
|
641
|
+
server = new mock_server_js_1.MockServer({
|
|
642
|
+
configPath: configDir,
|
|
643
|
+
port: 3041,
|
|
644
|
+
hotReload: false,
|
|
645
|
+
globalDelay: 5000, // would time out the test if used
|
|
646
|
+
});
|
|
647
|
+
await server.start();
|
|
648
|
+
const app = server.getApp();
|
|
649
|
+
const start = Date.now();
|
|
650
|
+
const res = await (0, supertest_1.default)(app).get('/api/prio-ep');
|
|
651
|
+
const elapsed = Date.now() - start;
|
|
652
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
653
|
+
(0, vitest_1.expect)(elapsed).toBeLessThan(300); // scenario delay=0 wins, should be fast
|
|
654
|
+
});
|
|
655
|
+
});
|
|
599
656
|
});
|
|
@@ -10,6 +10,7 @@ const mock_server_app_js_1 = require("../../server/mock-server-app.js");
|
|
|
10
10
|
const mockEndpoints = [
|
|
11
11
|
{
|
|
12
12
|
id: 'get-users',
|
|
13
|
+
sourceFile: 'accounts/users.ts',
|
|
13
14
|
path: '/users',
|
|
14
15
|
method: 'GET',
|
|
15
16
|
scenarios: [
|
|
@@ -74,6 +75,7 @@ const mockFlows = [
|
|
|
74
75
|
(0, vitest_1.expect)(usersEndpoint).toBeDefined();
|
|
75
76
|
(0, vitest_1.expect)(usersEndpoint.path).toBe('/users');
|
|
76
77
|
(0, vitest_1.expect)(usersEndpoint.method).toBe('GET');
|
|
78
|
+
(0, vitest_1.expect)(usersEndpoint.sourceFile).toBe('accounts/users.ts');
|
|
77
79
|
(0, vitest_1.expect)(usersEndpoint.scenarios).toHaveLength(2);
|
|
78
80
|
(0, vitest_1.expect)(usersEndpoint.defaultScenarioId).toBe('success');
|
|
79
81
|
});
|
|
@@ -281,131 +283,6 @@ const mockFlows = [
|
|
|
281
283
|
.expect(200);
|
|
282
284
|
});
|
|
283
285
|
});
|
|
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
286
|
(0, vitest_1.describe)('Flow CRUD operations', () => {
|
|
410
287
|
let mockConfigWriter;
|
|
411
288
|
let onConfigChangeCalled;
|
|
@@ -561,32 +438,6 @@ const mockFlows = [
|
|
|
561
438
|
});
|
|
562
439
|
});
|
|
563
440
|
(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
441
|
(0, vitest_1.it)('should return 501 for flow CRUD when ConfigWriter not configured', async () => {
|
|
591
442
|
const flow = {
|
|
592
443
|
id: 'test',
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import type {
|
|
2
|
+
import type { FlowConfig } from '../schemas/index.js';
|
|
3
3
|
import type { StateManager } from '../state/index.js';
|
|
4
|
-
import type { ConfigWriter } from '../config/index.js';
|
|
4
|
+
import type { ConfigWriter, RuntimeEndpointConfig } from '../config/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* Options for creating the admin router
|
|
7
7
|
*/
|
|
8
8
|
export interface AdminRouterOptions {
|
|
9
|
-
getEndpoints: () =>
|
|
9
|
+
getEndpoints: () => RuntimeEndpointConfig[];
|
|
10
10
|
getFlows: () => FlowConfig[];
|
|
11
11
|
stateManager: StateManager;
|
|
12
12
|
getConfigWriter?: () => ConfigWriter | undefined;
|
|
@@ -19,7 +19,7 @@ export declare function createAdminRouter(options: AdminRouterOptions): Router;
|
|
|
19
19
|
/**
|
|
20
20
|
* @deprecated Use options object instead
|
|
21
21
|
*/
|
|
22
|
-
export declare function createAdminRouter(getEndpoints: () =>
|
|
22
|
+
export declare function createAdminRouter(getEndpoints: () => RuntimeEndpointConfig[], getFlows: () => FlowConfig[], stateManager: StateManager): Router;
|
|
23
23
|
/**
|
|
24
24
|
* Admin API path constant
|
|
25
25
|
*/
|
|
@@ -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,
|
|
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,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAW9E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,qBAAqB,EAAE,CAAC;IAC5C,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,qBAAqB,EAAE,EAC3C,QAAQ,EAAE,MAAM,UAAU,EAAE,EAC5B,YAAY,EAAE,YAAY,GACzB,MAAM,CAAC;AA2cV;;GAEG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC"}
|