@n1k1t/mock-server 0.1.5 → 0.1.7

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.
Files changed (37) hide show
  1. package/README.md +1123 -43
  2. package/lib/package.json +1 -1
  3. package/lib/src/client/utils.d.ts.map +1 -1
  4. package/lib/src/client/utils.js +3 -0
  5. package/lib/src/client/utils.js.map +1 -1
  6. package/lib/src/config/model.d.ts +1 -0
  7. package/lib/src/config/model.d.ts.map +1 -1
  8. package/lib/src/config/model.js +1 -0
  9. package/lib/src/config/model.js.map +1 -1
  10. package/lib/src/expectations/models/expectation.d.ts +1 -1
  11. package/lib/src/expectations/models/expectation.d.ts.map +1 -1
  12. package/lib/src/expectations/models/expectation.js +1 -1
  13. package/lib/src/expectations/models/expectation.js.map +1 -1
  14. package/lib/src/expectations/operators/merge.operator.d.ts +10 -3
  15. package/lib/src/expectations/operators/merge.operator.d.ts.map +1 -1
  16. package/lib/src/expectations/operators/merge.operator.js +21 -4
  17. package/lib/src/expectations/operators/merge.operator.js.map +1 -1
  18. package/lib/src/expectations/types.d.ts +0 -1
  19. package/lib/src/expectations/types.d.ts.map +1 -1
  20. package/lib/src/expectations/types.js.map +1 -1
  21. package/lib/src/logger/index.d.ts +2 -2
  22. package/lib/src/logger/index.d.ts.map +1 -1
  23. package/lib/src/logger/index.js +1 -1
  24. package/lib/src/logger/index.js.map +1 -1
  25. package/lib/src/server/endpoints/config.get.endpoint.d.ts +3 -0
  26. package/lib/src/server/endpoints/config.get.endpoint.d.ts.map +1 -1
  27. package/lib/src/server/index.d.ts +1 -0
  28. package/lib/src/server/index.d.ts.map +1 -1
  29. package/lib/src/server/index.js +4 -1
  30. package/lib/src/server/index.js.map +1 -1
  31. package/lib/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
  33. package/public/scripts/main.js +75 -29
  34. package/public/styles/main.css +67 -0
  35. package/screenshots/history.png +0 -0
  36. package/screenshots/preview.png +0 -0
  37. package/screenshots/strategy.png +0 -0
package/README.md CHANGED
@@ -1,88 +1,1168 @@
1
1
 
2
+
2
3
  # Mock server
3
4
 
4
- This package is actually what you need but everything about it will be described later...
5
+ Mock, match, modify and manipulate a HTTP request/response payload using flexible expectations with types
6
+
7
+ ![screenshot](screenshots/preview.png)
8
+
9
+ # Navigation
10
+
11
+ - [Basics](#basics)
12
+ - [How it works](#how-it-works)
13
+ - [Install](#install)
14
+ - [Start](#start)
15
+ - [GUI](#gui)
16
+ - [Mock](#mock)
17
+ - [Expectations](#expectations)
18
+ - [Schema](#schema)
19
+ - [Context](#context)
20
+ - [Utils](#utils)
21
+ - [Operators](#operators)
22
+ - [Typings](#typings)
23
+ - [State](#state)
24
+ - [Seeds](#seeds)
25
+ - [XML](#xml)
26
+ - [API](#api)
27
+ - [Ping](#ping)
28
+ - [Create expectation](#create-expectation)
29
+ - [Update expectation](#update-expectation)
30
+ - [Delete expectation](#delete-expectation)
31
+ - [Additional](#additional)
32
+ - [Configuration](#configuration)
33
+ - [Logger](#logger)
34
+ - [Meta](#meta)
35
+ - [Plugins](#plugins)
36
+
37
+ # Basics
38
+
39
+ ## How it works
40
+
41
+ ![screenshot](screenshots/strategy.png)
42
+
43
+ According on the picture above, main idea is to generate or modify response from some backend service. The mock server provides many scenarios to do that
44
+
45
+ **In case of mocking without request forwarding:**
46
+
47
+ 1. Start mock server (for example on `localhost:8080`)
48
+ 2. Register expectation using CLI (cURL) or application lib
49
+ 3. Make request to `localhost:8080/...`
50
+ 1. The mock server matches a request payload with registred expectations
51
+ 2. Build a response using an expectation configuration
5
52
 
6
- ### Install
53
+ **In case of mocking with request forwarding:**
54
+
55
+ 0. Lets imagine that you have a service that hosts on `localhost:8081`
56
+ 1. Start mock server (for example on `localhost:8080`)
57
+ 2. Register expectation using CLI (cURL) or application lib
58
+ 3. Make request to `localhost:8080/...`
59
+ 1. The mock server matches a request payload with registred expectations
60
+ 2. Next is forwarding a request payload to `localhost:8081/...`
61
+ 3. Using response fetched from `localhost:8081/...` the mock server builds a response
62
+
63
+
64
+ ## Install
7
65
 
8
66
  ```bash
9
- npm i -g @n1k1t/mock-server
67
+ npm i @n1k1t/mock-server
10
68
  ```
11
69
 
12
- ### Start
70
+ ## Start
71
+
72
+ ### CLI
13
73
 
14
- Using console
15
74
  ```bash
16
- # It starts mock server on localhost:8080
17
- npx @n1k1t/mock-server -p 8080
75
+ npx @n1k1t/mock-server -h localhost -p 8080
18
76
  ```
19
77
 
20
- Using JavaScript
78
+ ### JavaScript
79
+
21
80
  ```js
22
81
  const { MockServer } = require('@n1k1t/mock-server');
82
+ MockServer.start({ host: 'localhost', port: 8080 });
83
+ ```
84
+
85
+ ### TypeScript
23
86
 
87
+ ```ts
88
+ import { MockServer } from '@n1k1t/mock-server';
24
89
  MockServer.start({ host: 'localhost', port: 8080 });
25
90
  ```
26
91
 
27
- Using TypeScript
92
+ ## GUI
93
+
94
+ The mock server provides built-in web panel to track everything that is going through. There are two tabs `Expectations` and `History`
95
+
96
+ By default it can be found on `/_mock/gui` of a host of mock server. Example: `localhost:8080/_mock/gui`
97
+
98
+ Also it provides convenient util to navigate through payload of expectations and requests payload
99
+
100
+ ![screenshot](screenshots/history.png)
101
+
102
+ ## Mock
103
+
104
+ Simple examples can be found in [expectation creation API](#create-expectation)
105
+
106
+ # Expectations
107
+
108
+ ## Schema
109
+
110
+ An expectation schema can contain some rules to handle `request`, `response` and `forward`
111
+
112
+ | Property | Nested | Type | Optional | Description |
113
+ |--|--|--|--|--|
114
+ | request | [Operators](#operators) | `object` | * | Describes a way to catch by request and how to manipulate it |
115
+ | response | [Operators](#operators) | `object` | * | Describes how to manipulate response. Also can be used to catch response in case of forwarding |
116
+ | forward | | `object` | * | Describes configuration to forward a request to another host |
117
+ | | url | `string` | * | Absolute URL to target |
118
+ | | baseUrl | `string` | * | Base URL to target. The path will be provided from request |
119
+
120
+ **Example**
121
+
122
+ ```ts
123
+ await server.client.createExpectation({
124
+ schema: {
125
+ request: {
126
+ $and: [],
127
+ },
128
+ response: {
129
+ $or: [],
130
+ },
131
+ forward: {
132
+ baseUrl: 'https://example.com',
133
+ url: '/some/path',
134
+ },
135
+ },
136
+ });
137
+ ```
138
+
139
+ ## Context
140
+
141
+ | Property | Nested | `$location` | Type | Optional | Description |
142
+ |--|--|--|--|--|--|
143
+ | seed | | `seed` | `string` | * | Incoming request [seed](#seeds) |
144
+ | state | | `state` | `object` | | Incoming request [state](#state) with a custom data while request finish |
145
+ | incoming | | | `object` | | Payload with data of incoming request |
146
+ | | path | `path` | `string` | | Incoming request path |
147
+ | | method | `method` | `string` | | Incoming request method in **uppercase** |
148
+ | | headers | `incoming.headers` | `object` | | Incoming request headers with keys in **lowercase** |
149
+ | | bodyRaw | `incoming.bodyRaw` | `string` | | Incoming request source body |
150
+ | | body | `incoming.body` | `object` | * | Incoming request parsed body |
151
+ | | query | `incoming.query` | `object` | * | Incoming request query search parameters |
152
+ | | delay | `delay` | `number` | * | Delay that can be applied with [operators](#operators) |
153
+ | | error | `error` | `string` | * | Error that can be applied with [operators](#operators) |
154
+ | outgoing | | | `object` | | Payload with data of response |
155
+ | | status | `outgoing.status` | `number` | | Response status code |
156
+ | | headers | `outgoing.headers` | `number` | | Response headers |
157
+ | | dataRaw | `outgoing.dataRaw` | `string` | | Response source data |
158
+ | | data | `outgoing.data` | `any` | * | Response data |
159
+
160
+ ## Utils
161
+
162
+ Additional utils in `$exec` operator
163
+
164
+ | Property | Description |
165
+ |--|--|
166
+ | `context` | A request [context](#context) |
167
+ | `logger` | [Logger](#logger) of mock server |
168
+ | `mode` | A mode of expectation execution. Has `match` on catching request or `manipulate` on manipulation over [context](#context) |
169
+ | `meta` | A [meta](#meta) of a request |
170
+ | `_` | [Lodash](https://www.npmjs.com/package/lodash) |
171
+ | `d` | [DayJS](https://www.npmjs.com/package/dayjs) |
172
+ | `faker` | [Faker](https://www.npmjs.com/package/@faker-js/faker). Uses [seed](#seeds) if it was provided |
173
+
174
+ ## Operators
175
+
176
+ > **!NOTE** Each schema that using operators can have only one nested operator. To use more than one operator use `$and` or `$or` operators
177
+
178
+ | Operator | Optional | Description |
179
+ |--|--|--|
180
+ | [$has](#has) | * | Catches a request/response or checks a payload in [context](#context) |
181
+ | [$set](#set) | * | Sets payload in [context](#context) |
182
+ | [$merge](#merge) | * | Merges object payload in [context](#context) with provided `$value` |
183
+ | [$remove](#remove) | * | Removes payload in [context](#context) |
184
+ | [$exec](#exec) | * | Function to catch a request/response or check/manipulate payload in [context](#context) |
185
+ | [$and](#and) | * | Logical `and` |
186
+ | [$or](#or) | * | Logical `or` |
187
+ | [$not](#not) | * | Logical `not` |
188
+ | [$if](#if) | * | Logical `if` |
189
+ | [$switch](#switch) | * | Logical `switch/case` |
190
+
191
+ **Example**
192
+
193
+ ```ts
194
+ await server.client.createExpectation({
195
+ schema: {
196
+ request: {
197
+ $and: [
198
+ {
199
+ $has: {
200
+ $location: 'path',
201
+ $value: '/foo',
202
+ },
203
+ },
204
+ {
205
+ $has: {
206
+ $location: 'method',
207
+ $value: 'GET',
208
+ },
209
+ },
210
+ ],
211
+ },
212
+ },
213
+ });
214
+ ```
215
+
216
+ ### $has
217
+
218
+ > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
219
+
220
+ | Property | Type (application) | Type (cURL) | Optional | Description |
221
+ |--|--|--|--|--|
222
+ | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
223
+ | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
224
+ | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
225
+ | $value | `any` | `any` | * | Checks by value equality in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
226
+ | $valueAnyOf | `any[]` | `any[]` | * | Checks by any of value equality in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
227
+ | $regExp | `RegExp` | `{ source: string, flags?: string }` | * | Checks by regular expression in context using `$location` (and `$path`, `$jsonPath` if it was specified) |
228
+ | $regExpAnyOf | `RegExp[]` | `{ source: string, flags?: string }[]` | * | Checks by any of regular expression in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
229
+ | $match | `string ∣ object` | `string ∣ object` | * | Checks by minimatch for `string` and `number` (example `/foo/*/bar` or `2**`) or similar `object` by passing object payload in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
230
+ | $matchAnyOf | `(string ∣ object)[]` | `(string ∣ object)[]` | * | Checks by any of minimatch for `string` and `number` (example `/foo/*/bar` or `2**`) or similar `object` by passing object payload in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
231
+ | $exec | `(payload, utils) => boolean` | `string` | * | Checks payload in [context](#context) by function with arguments where `payload` is selected entity using `$location` (and `$path`, `$jsonPath` if it was specified) and `utils` is [utils](#utils) |
232
+
233
+ **Example using application**
234
+
235
+ ```ts
236
+ await server.client.createExpectation({
237
+ schema: {
238
+ request: {
239
+ $has: {
240
+ $location: 'path',
241
+ $regExp: /^\/foo/,
242
+ },
243
+ },
244
+ },
245
+ });
246
+ ```
247
+
248
+ **Example using cURL**
249
+
250
+ ```bash
251
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
252
+ {
253
+ "schema": {
254
+ "request": {
255
+ "\$has": {
256
+ "\$location": "method",
257
+ "\$regExp": { "source": "^\/foo" }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ EOF
263
+ ```
264
+
265
+ ### $set
266
+
267
+ > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
268
+
269
+ | Property | Type (application) | Type (cURL) | Optional | Description |
270
+ |--|--|--|--|--|
271
+ | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
272
+ | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
273
+ | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
274
+ | $value | `any` | `any` | * | Sets value to [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
275
+ | $exec | `(payload, utils) => any` | `string` | * | Sets payload in [context](#context) by function with arguments where `payload` is selected entity using `$location` (and `$path`, `$jsonPath` if it was specified) and `utils` is [utils](#utils) |
276
+
277
+ **Example using application**
278
+
279
+ ```ts
280
+ await server.client.createExpectation({
281
+ schema: {
282
+ request: {
283
+ $set: {
284
+ $location: 'incoming.body',
285
+ $path: 'foo',
286
+ $exec: (payload, { _ }) => _.clamp(payload, 0, 10),
287
+ },
288
+ },
289
+ },
290
+ });
291
+ ```
292
+
293
+ **Example using cURL**
294
+
295
+ ```bash
296
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
297
+ {
298
+ "schema": {
299
+ "request": {
300
+ "\$set": {
301
+ "\$location": "incoming.body",
302
+ "\$path": "foo",
303
+ "\$exec": "_.clamp(payload, 0, 10)"
304
+ }
305
+ }
306
+ }
307
+ }
308
+ EOF
309
+ ```
310
+
311
+ ### $merge
312
+
313
+ > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
314
+
315
+ | Property | Type (application) | Type (cURL) | Optional | Description |
316
+ |--|--|--|--|--|
317
+ | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
318
+ | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
319
+ | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
320
+ | $value | `object` | `object` | * | Merges value in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
321
+ | $exec | `(payload, utils) => any` | `string` | * | Merges payload in [context](#context) by function with arguments where `payload` is selected entity using `$location` (and `$path`, `$jsonPath` if it was specified) and `utils` is [utils](#utils) |
322
+
323
+ **Example using application**
324
+
325
+ ```ts
326
+ await server.client.createExpectation({
327
+ schema: {
328
+ request: {
329
+ $merge: {
330
+ $location: 'incoming.body',
331
+ $value: { has_mocked: true },
332
+ },
333
+ },
334
+ },
335
+ });
336
+ ```
337
+
338
+ **Example using cURL**
339
+
340
+ ```bash
341
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
342
+ {
343
+ "schema": {
344
+ "request": {
345
+ "\$merge": {
346
+ "\$location": "incoming.body",
347
+ "\$value": {"has_mocked": true}
348
+ }
349
+ }
350
+ }
351
+ }
352
+ EOF
353
+ ```
354
+
355
+ ### $remove
356
+
357
+ | Property | Type (application) | Type (cURL) | Optional | Description |
358
+ |--|--|--|--|--|
359
+ | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
360
+ | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
361
+ | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
362
+
363
+ **Example using application**
364
+
365
+ ```ts
366
+ await server.client.createExpectation({
367
+ schema: {
368
+ request: {
369
+ $remove: { $location: 'outgoing.data' },
370
+ },
371
+ },
372
+ });
373
+ ```
374
+
375
+ **Example using cURL**
376
+
377
+ ```bash
378
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
379
+ {
380
+ "schema": {
381
+ "request": {
382
+ "\$remove": {"\$location": "outgoing.data"}
383
+ }
384
+ }
385
+ }
386
+ EOF
387
+ ```
388
+
389
+ ### $exec
390
+
391
+ > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
392
+
393
+ | Type (application) | Type (cURL) | Description |
394
+ |--|--|--|
395
+ | `(utils) => boolean ∣ unknown` | `string` | Does something you want or catch request/response payload in [context](#context) by function with arguments where `utils` is [utils](#utils) |
396
+
397
+ **Example using application**
398
+
399
+ ```ts
400
+ await server.client.createExpectation({
401
+ schema: {
402
+ request: {
403
+ $exec: ({ context, logger }) => {
404
+ logger.info(context);
405
+ return context.incoming.path === '/foo';
406
+ },
407
+ },
408
+ },
409
+ });
410
+ ```
411
+
412
+ **Example using cURL**
413
+
414
+ ```bash
415
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
416
+ {
417
+ "schema": {
418
+ "request": {
419
+ "\$exec": "{ logger.info(context); return context.incoming.path === '/foo' }"
420
+ }
421
+ }
422
+ }
423
+ EOF
424
+ ```
425
+
426
+ ### $and
427
+
428
+ | Type (application) | Type (cURL) | Description |
429
+ |--|--|--|
430
+ | `object[]` | `object[]` | Provides [operators](#operators) schemas |
431
+
432
+ **Example using application**
433
+
434
+ ```ts
435
+ await server.client.createExpectation({
436
+ schema: {
437
+ request: {
438
+ $and: [
439
+ { $has: { $location: 'path', $match: 'foo/*' } },
440
+ { $has: { $location: 'method', $valueAnyOf: ['GET', 'POST'] } },
441
+ ],
442
+ },
443
+ },
444
+ });
445
+ ```
446
+
447
+ **Example using cURL**
448
+
449
+ ```bash
450
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
451
+ {
452
+ "schema": {
453
+ "request": {
454
+ "\$and": [
455
+ {"\$has": {"\$location": "path", "\$match": "foo/*"}},
456
+ {"\$has": {"\$location": "method", "\$valueAnyOf": ["GET", "POST"]}}
457
+ ]
458
+ }
459
+ }
460
+ }
461
+ EOF
462
+ ```
463
+
464
+ ### $or
465
+
466
+ | Type (application) | Type (cURL) | Description |
467
+ |--|--|--|
468
+ | `object[]` | `object[]` | Provides [operators](#operators) schemas |
469
+
470
+ **Example using application**
471
+
472
+ ```ts
473
+ await server.client.createExpectation({
474
+ schema: {
475
+ request: {
476
+ $or: [
477
+ { $has: { $location: 'path', $match: 'foo/*' } },
478
+ { $has: { $location: 'method', $valueAnyOf: ['GET', 'POST'] } },
479
+ ],
480
+ },
481
+ },
482
+ });
483
+ ```
484
+
485
+ **Example using cURL**
486
+
487
+ ```bash
488
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
489
+ {
490
+ "schema": {
491
+ "request": {
492
+ "\$or": [
493
+ {"\$has": {"\$location": "path", "\$match": "foo/*"}},
494
+ {"\$has": {"\$location": "method", "\$valueAnyOf": ["GET", "POST"]}}
495
+ ]
496
+ }
497
+ }
498
+ }
499
+ EOF
500
+ ```
501
+
502
+ ### $not
503
+
504
+ | Type (application) | Type (cURL) | Description |
505
+ |--|--|--|
506
+ | `object` | `object` | Provides an [operators](#operators) schema |
507
+
508
+ **Example using application**
509
+
510
+ ```ts
511
+ await server.client.createExpectation({
512
+ schema: {
513
+ request: {
514
+ $not: { $has: { $location: 'path', $match: 'foo/*' } },
515
+ },
516
+ },
517
+ });
518
+ ```
519
+
520
+ **Example using cURL**
521
+
522
+ ```bash
523
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
524
+ {
525
+ "schema": {
526
+ "request": {
527
+ "\$not": {"\$has": {"\$location": "path", "\$match": "foo/*"}}
528
+ }
529
+ }
530
+ }
531
+ EOF
532
+ ```
533
+
534
+ ### $if
535
+
536
+ | Property | Type (application) | Type (cURL) | Optional | Description |
537
+ |--|--|--|--|--|
538
+ | $condition | `object` | `object` | | Condition to check. Should contain one of `$and`, `$exec`, `$has`, `$or` or `$not` [operators](#operators) schema |
539
+ | $then | `object` | `object` | * | Logical `then`. Should contain an [operators](#operators) schema |
540
+ | $else | `object` | `object` | * | Logical `else`. Should contain an [operators](#operators) schema |
541
+
542
+ **Example using application**
543
+
544
+ ```ts
545
+ await server.client.createExpectation({
546
+ schema: {
547
+ request: {
548
+ $if: {
549
+ $condition: { $has: { $location: 'path', $match: 'foo/*' } },
550
+ $then: { $set: { $location: 'delay', $value: 5000 } },
551
+ $else: { $set: { $location: 'error', $value: 'ECONNABORTED' } },
552
+ },
553
+ },
554
+ },
555
+ });
556
+ ```
557
+
558
+ **Example using cURL**
559
+
560
+ ```bash
561
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
562
+ {
563
+ "schema": {
564
+ "request": {
565
+ "\$if": {
566
+ "\$condition": {"\$has": {"\$location": "path", "\$match": "foo/*"}},
567
+ "\$then": {"\$set": {"\$location": "delay", "\$value": 5000}},
568
+ "\$else": {"\$set": {"\$location": "error", "\$value": "ECONNABORTED"}}
569
+ }
570
+ }
571
+ }
572
+ }
573
+ EOF
574
+ ```
575
+
576
+ ### $switch
577
+
578
+ > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
579
+
580
+ | Property | Type (application) | Type (cURL) | Optional | Description |
581
+ |--|--|--|--|--|
582
+ | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
583
+ | $cases | `Record<string ∣ number, object>` | `Record<string ∣ number, object>` | | An object where `key` is an extracted value from [enum](#context) using `$location` (and `$path`, `$exec` if it was specified) and `value` is an [operators](#operators) schema |
584
+ | $default | `object` | `object` | * | Default behavior as an [operators](#operators) schema |
585
+ | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
586
+ | $exec | `(payload, utils) => any` | `string` | * | Sets payload in [context](#context) by function with arguments where `payload` is selected entity using `$location` and `utils` is [utils](#utils) |
587
+
588
+ **Example using application**
589
+
590
+ ```ts
591
+ await server.client.createExpectation({
592
+ schema: {
593
+ request: {
594
+ $switch: {
595
+ $location: 'method',
596
+ $cases: {
597
+ 'GET': { $set: { $location: 'delay', $value: 2000 } },
598
+ 'POST': { $set: { $location: 'delay', $value: 5000 } },
599
+ },
600
+ $default: {
601
+ $set: { $location: 'error', $value: 'ECONNABORTED' }
602
+ },
603
+ },
604
+ },
605
+ },
606
+ });
607
+ ```
608
+
609
+ **Example using cURL**
610
+
611
+ ```bash
612
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
613
+ {
614
+ "schema": {
615
+ "request": {
616
+ "\$switch": {
617
+ "\$location": "method",
618
+ "\$cases": {
619
+ "GET": {"\$set": {"\$location": "delay", "\$value": 2000}},
620
+ "POST": {"\$set": {"\$location": "delay", "\$value": 5000}}
621
+ },
622
+ "\$default": {
623
+ "\$set": {"\$location": "error", "\$value": "ECONNABORTED"}
624
+ }
625
+ }
626
+ }
627
+ }
628
+ }
629
+ EOF
630
+ ```
631
+
632
+ ## Typings
633
+
634
+ The application client lib provides approach to keep typings using function predicate to `create` or `update` expectation with a generic argument. The generic type should have the same schema like [context](#context)
635
+
636
+ The function predicate provides an object argument with `$` that contains simplified API to build typed expectation schemas. Some operators have `using` predicate that can contain `$path`, `$jsonPath` or `$exec` selectors
637
+
638
+ **Examples**
639
+
640
+ ```ts
641
+ await client.createExpectation<{
642
+ incoming: {
643
+ query: {
644
+ foo: 'a' | 'b' | 'c';
645
+ bar?: string;
646
+ };
647
+ };
648
+ }>(({ $ }) => ({
649
+ schema: {
650
+ request: $.or([
651
+ $.has('incoming.query', '$path', 'foo', { $value: 'a' }),
652
+ $.has('incoming.query', { $match: { foo: 'b' } }),
653
+ ]),
654
+ },
655
+ }));
656
+ ```
657
+
658
+ ```ts
659
+ await client.createExpectation<{
660
+ incoming: {
661
+ query: {
662
+ foo: 'a' | 'b' | 'c';
663
+ bar?: string;
664
+ };
665
+ };
666
+ outgoing: {
667
+ data: {
668
+ foo: 'a' | 'b' | 'c';
669
+ bar?: {
670
+ baz: 'a' | 'b' | 'c';
671
+ };
672
+ };
673
+ };
674
+ }>(({ $ }) => ({
675
+ schema: {
676
+ response: $.and([
677
+ $.switch('incoming.query', '$exec', (payload) => payload.foo, {
678
+ $cases: {
679
+ 'a': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'a' }),
680
+ 'b': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'b' }),
681
+ },
682
+ }),
683
+
684
+ $.switch('incoming.query', '$path', 'bar', {
685
+ $cases: {
686
+ 'something': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'c' }),
687
+ },
688
+ }),
689
+ ]),
690
+ },
691
+ }));
692
+ ```
693
+
694
+ ## State
695
+
696
+ State is a unique storage of each request. It can be used to handle complex expectations
697
+
698
+ By default an object of state extracts from `X-Use-Mock-State` in `incoming.headers` (as serialized json in **base64 encoding**) or creates an empty object
699
+
700
+ **Example**
701
+
702
+ ```ts
703
+ await client.createExpectation<{
704
+ state: {
705
+ id?: number;
706
+ };
707
+ incoming: {
708
+ query: {
709
+ foo: 'a' | 'b' | 'c';
710
+ };
711
+ };
712
+ outgoing: {
713
+ data: {
714
+ id: number;
715
+ };
716
+ };
717
+ }>(({ $ }) => ({
718
+ schema: {
719
+ request: $.and([
720
+ $.switch('incoming.query', '$exec', (payload) => payload.foo, {
721
+ $cases: {
722
+ 'a': $.set('state', '$path', 'id', { $value: 1 }),
723
+ 'b': $.set('state', '$path', 'id', { $value: 2 }),
724
+ },
725
+ }),
726
+ ]),
727
+ response: $.set('outgoing.data', {
728
+ $exec: (payload, { state }) => ({ id: state.id ?? 0 }),
729
+ }),
730
+ },
731
+ }));
732
+ ```
733
+
734
+ ## Seeds
735
+
736
+ Seeds can help to generate content with the same values each request using [faker](https://www.npmjs.com/package/@faker-js/faker)
737
+
738
+ By default a number of seed takes from `X-Use-Mock-Seed` in `incoming.headers`
739
+
740
+ **Example**
741
+
742
+ ```ts
743
+ await client.createExpectation(({ $ }) => ({
744
+ schema: {
745
+ request: $.and([
746
+ $.set('seed', { $exec: (seed) => seed ?? 123 }),
747
+ ]),
748
+ response: $.set('outgoing.data', {
749
+ $exec: (payload, { faker }) => ({
750
+ id: faker.number.int({ max: 1000, min: 500 }),
751
+ first_name: faker.person.firstName('male'),
752
+ last_name: faker.person.lastName('male'),
753
+ }),
754
+ }),
755
+ },
756
+ }));
757
+ ```
758
+
759
+ ## XML
760
+
761
+ The mock server uses the [fast-xml-parser](https://www.npmjs.com/package/fast-xml-parser) package to parse and serialize XML payload with options:
762
+
763
+ ```ts
764
+ {
765
+ ignoreAttributes: false,
766
+ }
767
+ ```
768
+
769
+ To define a `incoming.data` as XML in incoming request `incoming.headers` should have `Content-Type: application/xml`.
770
+
771
+ The same with `outgoing.data` and `outgoing.headers`
772
+
773
+ **Example of serialized XML**
774
+
775
+ ```xml
776
+ <tag type="default">
777
+ <nested type="nested">456</nested>
778
+ 123
779
+ </tag>
780
+ ```
781
+
782
+ **Example of parsed XML**
783
+
784
+ ```json
785
+ {
786
+ "tag":{
787
+ "nested":{
788
+ "#text":456,
789
+ "@_type":"nested"
790
+ },
791
+ "#text":123,
792
+ "@_type":"default"
793
+ }
794
+ }
795
+ ```
796
+
797
+ To parse an XML manually the application lib provides utils:
798
+
799
+ ```ts
800
+ import { parsePayload, serializePayload } from '@n1k1t/mock-server';
801
+
802
+ const parsed = parsePayload('xml', '<tag>123</tag>'); // { tag: 123 }
803
+ const serialized = serializePayload('xml', parsed); // '<tag>123</tag>'
804
+ ```
805
+
806
+ # API
807
+
808
+ The mock server provides 3 different ways to work with. There are: `HTTP API` (eg using cURL), `RemoteClient` provided by application lib to connect and work with existent mock server on another host and `MockServer.client` on the same host (application script)
809
+
810
+ The `HTTP API` and `RemoteClient` have some usage restrictions like:
811
+ - Every `$exec` operator **cannot have an access to variables outside the function**. If you need to use some extra variables or modules that implemented in outer scope you have to use the `MockServer.client` to setup everything on the mock server side host
812
+ - Plugins are not supported
813
+
814
+ ## Ping
815
+
816
+ `INPUT` → `GET /_mock/ping`
817
+
818
+ `OUTPUT`
819
+
820
+ | Type | Description |
821
+ |--|--|
822
+ | `string` | A `pong` message |
823
+
824
+ **Using cURL**
825
+
826
+ ```bash
827
+ curl -H "Content-type: application/json" --location "localhost:8080/_mock/ping"
828
+ ```
829
+
830
+ **Using application lib on server side**
831
+
28
832
  ```ts
29
833
  import { MockServer } from '@n1k1t/mock-server';
30
834
 
31
- MockServer.start({ host: 'localhost', port: 8080 });
835
+ const server = await MockServer.start({ host: 'localhost', port: 8080 });
836
+ await server.client.ping();
837
+ ```
838
+
839
+ **Using application lib on remotely**
840
+
841
+ ```ts
842
+ import { RemoteClient } from '@n1k1t/mock-server';
843
+
844
+ const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
845
+ await client.ping();
32
846
  ```
33
847
 
34
- ### Add expectations
848
+ ## Create expectation
35
849
 
36
- Using curl
850
+ `INPUT` → `POST /_mock/expectations`
851
+
852
+ | Property | Nested | Type | Optional | Description |
853
+ |--|--|--|--|--|
854
+ | schema | [Schema](#schema) | `object` | | An expectation schema |
855
+ | name | | `string` | * | A preferred name for an expectation |
856
+
857
+ `OUTPUT`
858
+
859
+ | Property | Nested | Type | Optional | Description |
860
+ |--|--|--|--|--|
861
+ | id | | `string` | | An expectation ID |
862
+ | name | | `string` | | An expectation name |
863
+ | schema | [Schema](#schema) | `object` | | Provided schema |
864
+
865
+ **Using cURL**
37
866
 
38
867
  ```bash
39
- # Create a passtrough expectation to port 80
40
- curl -X POST -d '{"forward": {"protocol": "HTTP", "host": "localhost", "port": 80}}' 'localhost:8080/_mock/expectations'
868
+ curl -H "Content-type: application/json" -X POST --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
869
+ {
870
+ "schema": {
871
+ "request": {
872
+ "\$has": {
873
+ "\$location": "method",
874
+ "\$value": "GET"
875
+ }
876
+ }
877
+ }
878
+ }
879
+ EOF
880
+ ```
881
+
882
+ **Using application lib on server side**
883
+
884
+ ```ts
885
+ import { MockServer } from '@n1k1t/mock-server';
886
+
887
+ const server = await MockServer.start({ host: 'localhost', port: 8080 });
888
+ const expectation = await server.client.createExpectation({
889
+ schema: {
890
+ request: {
891
+ $has: {
892
+ $location: 'method',
893
+ $value: 'GET',
894
+ },
895
+ },
896
+ },
897
+ });
898
+
899
+ console.log('Mock expectation has created', expectation.id);
41
900
  ```
42
901
 
43
- Using remote client
902
+ **Using application lib on remotely**
44
903
 
45
904
  ```ts
46
905
  import { RemoteClient } from '@n1k1t/mock-server';
47
906
 
48
- RemoteClient
49
- .connect({ host: 'localhost', port: 8080 })
50
- .then(async (client) => {
51
- await client.createExpectation({
52
- forward: {
53
- timeout: 2 * 60 * 1000,
54
- protocol: 'HTTP',
55
- host: 'localhost',
56
- port: 80,
907
+ const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
908
+ const expectation = await client.createExpectation({
909
+ schema: {
910
+ request: {
911
+ $has: {
912
+ $location: 'method',
913
+ $value: 'GET',
57
914
  },
58
- });
59
- });
915
+ },
916
+ },
917
+ });
918
+
919
+ console.log('Mock expectation has created', expectation.id);
60
920
  ```
61
921
 
62
- Using client on mock server side
922
+ ## Update expectation
923
+
924
+ `INPUT` → `PUT /_mock/expectations`
925
+
926
+ | Property | Nested | Type | Optional | Description |
927
+ |--|--|--|--|--|
928
+ | id | | `string` | | ID of a registred expectation |
929
+ | set | | `object` | | A payload to set |
930
+ | | name | `string` | * | A preferred name for an expectation |
931
+ | | schema | [Schema](#schema) | * | An expectation schema |
932
+
933
+ `OUTPUT`
934
+
935
+ | Property | Nested | Type | Optional | Description |
936
+ |--|--|--|--|--|
937
+ | id | | `string` | | An expectation ID |
938
+ | name | | `string` | | An expectation name |
939
+ | schema | [Schema](#schema) | `object` | | Provided schema |
940
+
941
+ **Using cURL**
942
+
943
+ ```bash
944
+ curl -H "Content-type: application/json" -X PUT --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
945
+ {
946
+ "id": "...",
947
+ "set": {"name": "The expectation"}
948
+ }
949
+ EOF
950
+ ```
951
+
952
+ **Using application lib on server side**
63
953
 
64
954
  ```ts
65
955
  import { MockServer } from '@n1k1t/mock-server';
66
956
 
67
- MockServer
68
- .start({ host: 'localhost', port: 8080 })
69
- .then(async ({ client }) => {
70
- await client.createExpectation({
71
- forward: {
72
- timeout: 2 * 60 * 1000,
73
- protocol: 'HTTP',
74
- host: 'localhost',
75
- port: 80,
76
- },
77
- });
78
- });
957
+ const server = await MockServer.start({ host: 'localhost', port: 8080 });
958
+ const expectation = await server.client.updateExpectation({
959
+ id: '...',
960
+ set: { name: 'The expectation' }
961
+ });
962
+
963
+ console.log('Mock expectation has updated', expectation);
964
+ ```
965
+
966
+ **Using application lib on remotely**
967
+
968
+ ```ts
969
+ import { RemoteClient } from '@n1k1t/mock-server';
970
+
971
+ const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
972
+ const expectation = await client.updateExpectation({
973
+ id: '...',
974
+ set: { name: 'The expectation' }
975
+ });
976
+
977
+ console.log('Mock expectation has updated', expectation);
79
978
  ```
80
979
 
81
- ### GUI
980
+ ## Delete expectation
981
+
982
+ `INPUT` → `DELETE /_mock/expectations`
983
+
984
+ | Property | Nested | Type | Optional | Description |
985
+ |--|--|--|--|--|
986
+ | ids | | `string[]` | * | An expectation IDs list to delete. Or **delete all expectations** if not provided |
82
987
 
83
- To access the GUI of mock server you have to navigate on `/_mock/gui/`
988
+ **Using cURL**
84
989
 
85
- Example
86
990
  ```bash
87
- localhost:8080/_mock/gui/
991
+ curl -H "Content-type: application/json" -X DELETE --location "localhost:8080/_mock/expectations" --data-binary @- << EOF
992
+ {
993
+ "ids": ["..."]
994
+ }
995
+ EOF
996
+ ```
997
+
998
+ **Using application lib on server side**
999
+
1000
+ ```ts
1001
+ import { MockServer } from '@n1k1t/mock-server';
1002
+
1003
+ const server = await MockServer.start({ host: 'localhost', port: 8080 });
1004
+ await server.client.deleteExpectations({
1005
+ ids: ['...'],
1006
+ });
1007
+ ```
1008
+
1009
+ **Using application lib on remotely**
1010
+
1011
+ ```ts
1012
+ import { RemoteClient } from '@n1k1t/mock-server';
1013
+
1014
+ const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
1015
+ await client.deleteExpectations({
1016
+ ids: ['...'],
1017
+ });
1018
+ ```
1019
+
1020
+ # Additional
1021
+
1022
+ ## Configuration
1023
+
1024
+ > **!NOTE** Configuration must be provided in the same script like mock server
1025
+
1026
+ ```ts
1027
+ import { config } from '@n1k1t/mock-server';
1028
+
1029
+ config.merge({
1030
+ logger: {
1031
+ level: 'D', // Logger level (default: D)
1032
+ },
1033
+
1034
+ gui: {
1035
+ title: 'My app', // Title for a GUI application page (default: Mock server)
1036
+ },
1037
+
1038
+ history: {
1039
+ limit: 100, // Limit for history of requests (default: 100)
1040
+ },
1041
+ });
1042
+ ```
1043
+
1044
+ ## Logger
1045
+
1046
+ > **!NOTE** Configuration must be provided in the same script like mock server
1047
+
1048
+ ```ts
1049
+ import { Logger } from '@n1k1t/mock-server';
1050
+
1051
+ // It defines your own logger methods
1052
+ Logger.useExternal({
1053
+ debug: (...messages: string[]) => console.debug(...messages),
1054
+ info: (...messages: string[]) => console.log(...messages),
1055
+ warn: (...messages: string[]) => console.warn(...messages),
1056
+ error: (...messages: string[]) => console.error(...messages),
1057
+ fatal: (...messages: string[]) => console.error(...messages),
1058
+ });
1059
+
1060
+ // It defines a JSON serializers to mask some private data by keys on objects
1061
+ Logger.useSerializers({
1062
+ cvv: () => '***',
1063
+ card: (payload: string) => payload.slice(0, 8) + 'xxxx',
1064
+ });
1065
+ ```
1066
+
1067
+ ## Meta
1068
+
1069
+ Some loggers (like `banyan` and etc) provide a meta context for logs with some data. To keep a meta contexts between requests the mock server provides a `metaStorage` using native node `AsyncLocalStorage`.
1070
+
1071
+ The `metaStorage.provide()` returns an instance of `meta` that contains basic data like:
1072
+
1073
+ | Property | Type | Optional | Description |
1074
+ |--|--|--|--|
1075
+ | operationId | `string` | | UUID v4 |
1076
+ | requestId | `string` | * | `X-Request-Id` from `incoming.headers` |
1077
+
1078
+ **Setup**
1079
+
1080
+ ```ts
1081
+ import { Logger, metaStorage } from '@n1k1t/mock-server';
1082
+
1083
+ // Some external logger with meta context support
1084
+ const external = {...};
1085
+
1086
+ // It defines your own logger methods
1087
+ Logger.useExternal({
1088
+ debug: (...messages: string[]) => external.debug(metaStorage.provide(), ...messages),
1089
+ info: (...messages: string[]) => external.log(metaStorage.provide(), ...messages),
1090
+ warn: (...messages: string[]) => external.warn(metaStorage.provide(), ...messages),
1091
+ error: (...messages: string[]) => external.error(metaStorage.provide(), ...messages),
1092
+ fatal: (...messages: string[]) => external.error(metaStorage.provide(), ...messages),
1093
+ });
1094
+ ```
1095
+
1096
+ **Usage**
1097
+
1098
+ ```ts
1099
+ await server.client.createExpectation({
1100
+ schema: {
1101
+ request: {
1102
+ $exec: ({ context, logger }) => {
1103
+ // Here logger should have a meta context like { operationId: '...' }
1104
+ logger.info('Before')
1105
+ },
1106
+ $exec: ({ context, logger, meta }) => {
1107
+ // It enriches meta context for further logs of request
1108
+ meta.merge({ foo: 'bar' });
1109
+ },
1110
+ $exec: ({ context, logger, meta }) => {
1111
+ // Now logger should have a meta context like { foo: 'bar', operationId: '...' }
1112
+ logger.info('After')
1113
+ },
1114
+ },
1115
+ },
1116
+ });
1117
+ ```
1118
+
1119
+ ## Plugins
1120
+
1121
+ > **!NOTE** Configuration must be provided in the same script like mock server
1122
+
1123
+ | Plugin | Description |
1124
+ |--|--|
1125
+ | `incoming.body` | Describes how to handle incoming body |
1126
+ | `outgoing.response` | Describes how to reply |
1127
+ | `forward.request` | Describes how provide an [axios](https://www.npmjs.com/package/axios) request config to forward a request |
1128
+ | `forward.response` | Describes how to parse [axios](https://www.npmjs.com/package/axios) response of a forwarded request |
1129
+
1130
+ **Example of `incoming.body` plugin**
1131
+
1132
+ ```ts
1133
+ server.context.plugins.register('incoming.body', async (request) => {
1134
+ let raw = '';
1135
+
1136
+ request.on('data', chunk => raw += chunk);
1137
+ await new Promise(resolve => request.on('end', resolve));
1138
+
1139
+ return { raw };
1140
+ });
1141
+ ```
1142
+
1143
+ **Example of `outgoing.response` plugin**
1144
+
1145
+ ```ts
1146
+ server.context.plugins.register('outgoing.response', (response, context) => {
1147
+ context.response.writeHead(context.outgoing.status ?? 200, context.outgoing.headers);
1148
+ context.response.write(context.outgoing.dataRaw);
1149
+ context.response.end();
1150
+ });
1151
+ ```
1152
+
1153
+ **Example of `forward.request` plugin**
1154
+
1155
+ ```ts
1156
+ server.context.plugins.register('forward.request', (config) => ({
1157
+ ...config,
1158
+ url: config.url.replace('/api_v1', '/api_v2'),
1159
+ }));
1160
+ ```
1161
+
1162
+ **Example of `forward.response` plugin**
1163
+
1164
+ ```ts
1165
+ server.context.plugins.register('forward.response', async (response: AxiosResponse<Buffer>) => ({
1166
+ raw: response.data.toString(),
1167
+ }));
88
1168
  ```