UNPKG

@n1k1t/mock-server

Version:

Powerful util to setup mocks over HTTP APIs

1,256 lines (1,013 loc) 38.7 kB
<div align='center'> <h1>Mock server</h1> <p>Mock, match, modify and manipulate a HTTP request/response payload using flexible expectations with types</p> <img src="https://raw.githubusercontent.com/n1k1t/mock-server/refs/heads/master/images/preview.png?raw=true" /> <br /> <br /> ![License](https://img.shields.io/badge/License-MIT-yellow.svg) ![npm version](https://badge.fury.io/js/@n1k1t%2Fmock-server.svg) ![Dynamic XML Badge](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fgithub.com%2Fn1k1t%2Fmock-server%2Fblob%2Fmaster%2Fcoverage%2Fcobertura-coverage.xml%3Fraw%3Dtrue&query=round(%2Fcoverage%2F%40line-rate%20*%201000)%20div%201000&label=coverage) </div> # Navigation - [Basics](#basics) - [How it works](#how-it-works) - [Install](#install) - [Start](#start) - [GUI](#gui) - [Mock](#mock) - [Expectations](#expectations) - [Schema](#schema) - [Forwarding](#forwarding) - [Context](#context) - [Utils](#utils) - [Operators](#operators) - [Typings](#typings) - [Storage](#storage) - [Containers](#containers) - [Cache](#cache) - [State](#state) - [Seeds](#seeds) - [XML](#xml) - [API](#api) - [Ping](#ping) - [Create expectation](#create-expectation) - [Update expectation](#update-expectation) - [Delete expectation](#delete-expectation) - [Additional](#additional) - [Configuration](#configuration) - [Logger](#logger) - [Meta](#meta) # Basics ## Install ```bash npm i @n1k1t/mock-server ``` ## How it works ![screenshot](https://raw.githubusercontent.com/n1k1t/mock-server/refs/heads/master/images/strategy.png?raw=true) 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 **In case of mocking without request forwarding:** 1. Start mock server (for example on `localhost:8080`) 2. Register expectation using CLI (cURL) or application lib 3. Make request to `localhost:8080/...` 1. The mock server matches a request payload with registered expectations 2. Build a response using an expectation configuration **In case of mocking with request forwarding:** 0. Lets imagine that you have a service that hosts on `localhost:8081` 1. Start mock server (for example on `localhost:8080`) 2. Register expectation using CLI (cURL) or application lib 3. Make request to `localhost:8080/...` 1. The mock server matches a request payload with registered expectations 2. Next is forwarding a request payload to `localhost:8081/...` 3. Using response fetched from `localhost:8081/...` the mock server builds a response ## Start ### CLI ```bash npx mock -h localhost -p 8080 ``` ### JavaScript ```js const { MockServer } = require('@n1k1t/mock-server'); MockServer.start({ host: 'localhost', port: 8080 }); ``` ### TypeScript ```ts import { MockServer } from '@n1k1t/mock-server'; MockServer.start({ host: 'localhost', port: 8080 }); ``` ## GUI The mock server provides built-in web panel to track everything that is going through. There are two tabs `Expectations` and `History` By default it can be found on `/_system/gui` of a host of mock server. Example: `localhost:8080/_system/gui` Also it provides convenient util to navigate through payload of expectations and requests payload ## Mock Simple examples can be found in [expectation creation API](#create-expectation) # Expectations ## Schema An expectation schema can contain some rules to handle `request`, `response` and `forward` | Property | Nested | Type | Optional | Description | |--|--|--|--|--| | request | [Operators](#operators) | `object` | * | Describes a way to catch by request and how to manipulate it | | response | [Operators](#operators) | `object` | * | Describes how to manipulate response. Also can be used to catch response in case of forwarding | | forward | [Forwarding](#forwarding) | `object` | * | Describes configuration to forward a request to another host | **Example** ```ts await server.client.createExpectation({ schema: { request: { $and: [], }, response: { $or: [], }, forward: { baseUrl: 'https://example.com', url: '/some/path', }, }, }); ``` ## Forwarding | Property | Nested | Type | Optional | Description | |--|--|--|--|--| | url | | `string` | * | Absolute URL to target | | baseUrl | | `string` | * | Base URL to target. The path will be provided from request | | options | | `string` | * | Forwarding options | | | host | `origin` | * | Provides `Host` header as same as mock server host (if not specified). If specified to `origin` then value for `Host` header will be taken from url | | cache | | `object` | * | [Cache](#cache) configuration for a payload of forwarded requests | | | storage | `redis` | * | Storage to read/write a cache | | | key | `string` | * | Key to get read/write access of cached payload | | | prefix | `string` | * | Prefix of the `key` of cache | | | ttl | `number` | * | Time to live of cache in seconds | ## Context | Property | Nested | `$location` | Type | Optional | Description | |--|--|--|--|--|--| | storage | [Storage](#storage) | | `object` | | A storage of `container` entities | | container | [Container](#containers) | `container` | `object` | * | A temporary cell in `storage`. Should be useful to sync expectations between each other or store and use any data each request | | state | | `state` | `object` | | An [object](#state) with custom data | | seed | | `seed` | `string` | * | Incoming request [seed](#seeds) | | cache | | `cache` | `object` | | [Cache](#cache) configuration | | | isEnabled | | `boolean` | | Toggle of cache usage | | | key | | `string ∣ object` | * | Key to get read/write access of cached payload. Value provided as `object` will hashed using [FNV1A-64](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) algorithm | | | prefix | | `string` | * | Prefix of the `key` of cache | | | ttl | | `number` | * | Time to live of cache in seconds | | incoming | | | `object` | | Payload with data of incoming request | | | path | `path` | `string` | | Incoming request path | | | method | `method` | `string` | | Incoming request method in **uppercase** | | | headers | `incoming.headers` | `object` | | Incoming request headers with keys in **lowercase** | | | dataRaw | `incoming.dataRaw` | `string` | | Incoming request source data | | | data | `incoming.data` | `object` | * | Incoming request parsed data | | | query | `incoming.query` | `object` | * | Incoming request query search parameters | | | delay | `delay` | `number` | * | Delay that can be applied with [operators](#operators) | | | error | `error` | `string` | * | Error that can be applied with [operators](#operators) | | outgoing | | | `object` | | Payload with data of response | | | status | `outgoing.status` | `number` | | Response status code | | | headers | `outgoing.headers` | `number` | | Response headers | | | dataRaw | `outgoing.dataRaw` | `string` | | Response source data | | | data | `outgoing.data` | `any` | * | Response data | ## Utils Additional utils in `$exec` operator | Property | Description | |--|--| | `context` | A request [context](#context) | | `logger` | [Logger](#logger) of mock server | | `mode` | A mode of expectation execution. Has `match` on catching request or `manipulate` on manipulation over [context](#context) | | `meta` | A [meta](#meta) of a request | | `_` | [Lodash](https://www.npmjs.com/package/lodash) | | `d` | [DayJS](https://www.npmjs.com/package/dayjs) | | `faker` | [Faker](https://www.npmjs.com/package/@faker-js/faker). Uses [seed](#seeds) if it was provided | ## Operators > **!NOTE** Each schema that using operators can have only one nested operator. To use more than one operator use `$and` or `$or` operators | Operator | Optional | Description | |--|--|--| | [$has](#has) | * | Catches a request/response or checks a payload in [context](#context) | | [$set](#set) | * | Sets payload in [context](#context) | | [$merge](#merge) | * | Merges object payload in [context](#context) with provided `$value` | | [$remove](#remove) | * | Removes payload in [context](#context) | | [$exec](#exec) | * | Function to catch a request/response or check/manipulate payload in [context](#context) | | [$and](#and) | * | Logical `and` | | [$or](#or) | * | Logical `or` | | [$not](#not) | * | Logical `not` | | [$if](#if) | * | Logical `if` | | [$switch](#switch) | * | Logical `switch/case` | **Example** ```ts await server.client.createExpectation({ schema: { request: { $and: [ { $has: { $location: 'path', $value: '/foo', }, }, { $has: { $location: 'method', $value: 'GET', }, }, ], }, }, }); ``` ### $has > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient` | Property | Type (application) | Type (cURL) | Optional | Description | |--|--|--|--|--| | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with | | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) | | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) | | $value | `any` | `any` | * | Checks by value equality in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) | | $valueAnyOf | `any[]` | `any[]` | * | Checks by any of value equality in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) | | $regExp | `RegExp` | `{ source: string, flags?: string }` | * | Checks by regular expression in context using `$location` (and `$path`, `$jsonPath` if it was specified) | | $regExpAnyOf | `RegExp[]` | `{ source: string, flags?: string }[]` | * | Checks by any of regular expression in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) | | $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) | | $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) | | $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) | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $has: { $location: 'path', $regExp: /^\/foo/, }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$has": { "\$location": "method", "\$regExp": { "source": "^\/foo" } } } } } EOF ``` ### $set > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient` | Property | Type (application) | Type (cURL) | Optional | Description | |--|--|--|--|--| | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with | | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) | | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) | | $value | `any` | `any` | * | Sets value to [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) | | $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) | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $set: { $location: 'incoming.data', $path: 'foo', $exec: (payload, { _ }) => _.clamp(payload, 0, 10), }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$set": { "\$location": "incoming.data", "\$path": "foo", "\$exec": "_.clamp(payload, 0, 10)" } } } } EOF ``` ### $merge > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient` | Property | Type (application) | Type (cURL) | Optional | Description | |--|--|--|--|--| | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with | | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) | | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) | | $value | `object` | `object` | * | Merges value in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) | | $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) | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $merge: { $location: 'incoming.data', $value: { has_mocked: true }, }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$merge": { "\$location": "incoming.data", "\$value": {"has_mocked": true} } } } } EOF ``` ### $remove | Property | Type (application) | Type (cURL) | Optional | Description | |--|--|--|--|--| | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with | | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) | | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $remove: { $location: 'outgoing.data' }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$remove": {"\$location": "outgoing.data"} } } } EOF ``` ### $exec > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient` | Type (application) | Type (cURL) | Description | |--|--|--| | `(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) | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $exec: ({ context, logger }) => { logger.info(context); return context.incoming.path === '/foo'; }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$exec": "{ logger.info(context); return context.incoming.path === '/foo' }" } } } EOF ``` ### $and | Type (application) | Type (cURL) | Description | |--|--|--| | `object[]` | `object[]` | Provides [operators](#operators) schemas | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $and: [ { $has: { $location: 'path', $match: 'foo/*' } }, { $has: { $location: 'method', $valueAnyOf: ['GET', 'POST'] } }, ], }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$and": [ {"\$has": {"\$location": "path", "\$match": "foo/*"}}, {"\$has": {"\$location": "method", "\$valueAnyOf": ["GET", "POST"]}} ] } } } EOF ``` ### $or | Type (application) | Type (cURL) | Description | |--|--|--| | `object[]` | `object[]` | Provides [operators](#operators) schemas | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $or: [ { $has: { $location: 'path', $match: 'foo/*' } }, { $has: { $location: 'method', $valueAnyOf: ['GET', 'POST'] } }, ], }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$or": [ {"\$has": {"\$location": "path", "\$match": "foo/*"}}, {"\$has": {"\$location": "method", "\$valueAnyOf": ["GET", "POST"]}} ] } } } EOF ``` ### $not | Type (application) | Type (cURL) | Description | |--|--|--| | `object` | `object` | Provides an [operators](#operators) schema | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $not: { $has: { $location: 'path', $match: 'foo/*' } }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$not": {"\$has": {"\$location": "path", "\$match": "foo/*"}} } } } EOF ``` ### $if | Property | Type (application) | Type (cURL) | Optional | Description | |--|--|--|--|--| | $condition | `object` | `object` | | Condition to check. Should contain one of `$and`, `$exec`, `$has`, `$or` or `$not` [operators](#operators) schema | | $then | `object` | `object` | * | Logical `then`. Should contain an [operators](#operators) schema | | $else | `object` | `object` | * | Logical `else`. Should contain an [operators](#operators) schema | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $if: { $condition: { $has: { $location: 'path', $match: 'foo/*' } }, $then: { $set: { $location: 'delay', $value: 5000 } }, $else: { $set: { $location: 'error', $value: 'ECONNABORTED' } }, }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$if": { "\$condition": {"\$has": {"\$location": "path", "\$match": "foo/*"}}, "\$then": {"\$set": {"\$location": "delay", "\$value": 5000}}, "\$else": {"\$set": {"\$location": "error", "\$value": "ECONNABORTED"}} } } } } EOF ``` ### $switch > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient` | Property | Type (application) | Type (cURL) | Optional | Description | |--|--|--|--|--| | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with | | $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 | | $default | `object` | `object` | * | Default behavior as an [operators](#operators) schema | | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) | | $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) | **Example using application** ```ts await server.client.createExpectation({ schema: { request: { $switch: { $location: 'method', $cases: { 'GET': { $set: { $location: 'delay', $value: 2000 } }, 'POST': { $set: { $location: 'delay', $value: 5000 } }, }, $default: { $set: { $location: 'error', $value: 'ECONNABORTED' } }, }, }, }, }); ``` **Example using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$switch": { "\$location": "method", "\$cases": { "GET": {"\$set": {"\$location": "delay", "\$value": 2000}}, "POST": {"\$set": {"\$location": "delay", "\$value": 5000}} }, "\$default": { "\$set": {"\$location": "error", "\$value": "ECONNABORTED"} } } } } } EOF ``` ## Typings 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) 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 **Examples** ```ts await client.createExpectation<{ incoming: { query: { foo: 'a' | 'b' | 'c'; bar?: string; }; }; }>(({ $ }) => ({ schema: { request: $.or([ $.has('incoming.query', '$path', 'foo', { $value: 'a' }), $.has('incoming.query', { $match: { foo: 'b' } }), ]), }, })); ``` ```ts await client.createExpectation<{ incoming: { query: { foo: 'a' | 'b' | 'c'; bar?: string; }; }; outgoing: { data: { foo: 'a' | 'b' | 'c'; bar?: { baz: 'a' | 'b' | 'c'; }; }; }; }>(({ $ }) => ({ schema: { response: $.and([ $.switch('incoming.query', '$exec', (payload) => payload.foo, { $cases: { 'a': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'a' }), 'b': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'b' }), }, }), $.switch('incoming.query', '$path', 'bar', { $cases: { 'something': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'c' }), }, }), ]), }, })); ``` ## Storage Storage is a temporary storage that provides an access to read/write [containers](#containers) | Property | Type | Description | |--|--|--| | find | `(key: string ∣ object) => Container ∣ null` | Finds a container in storage. Every `key` provided as `object` will hashed using [FNV1A-64](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) algorithm | | delete | `(key: string ∣ object) => Container ∣ null` | Deletes a container in storage. Every `key` provided as `object` will hashed using [FNV1A-64](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) algorithm | | register | `(configuration: Container) => Container` | Registers a container in storage (overrides if existent) | | provide | `(configuration: Container) => Container` | Finds or registers a container in storage | As a temporary storage it has a job to garbage an expired containers. Use `containers.expiredCleaningInterval` to setup an interval of clearance in [configuration](#configuration) > **!NOTE** See example of usage in [containers](#containers) section below ## Containers | Property | Type | Description | |--|--|--| | key | `string` | A key of container | | prefix | `string` | A prefix of container | | payload | `object` | An object with custom data | | ttl | `number` | Time to live of container in seconds **(default: 1h)** | | expiresAt | `number` | An expiration date/time as unix timestamp with milliseconds | | bind | `(key: string ∣ object) => Container` | Binds a container to one more key. Every `key` provided as `object` will hashed using [FNV1A-64](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) algorithm | | unbind | `(key: string ∣ object) => Container` | Unbinds a container from key. Every `key` provided as `object` will hashed using [FNV1A-64](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) algorithm | | assign | `(payload: object ∣ (payload: object) => object) => Container` | Uses as payload predicate to assign payload values to existent | | merge | `(payload: object ∣ (payload: object) => object) => Container` | Uses as payload predicate to deep merge of payload values with existent | **Example** ```ts await client.createExpectation<{ container: { counter: number; }; }>(({ $ }) => ({ schema: { request: $.set('container', { $exec: (container, { context }) => context.storage .provide({ key: 'foo', payload: { counter: 0 } }) .assign((payload) => ({ counter: payload.counter + 1 })) }), response: $.set('outgoing.data', { $exec: (payload, { context }) => ({ count: context.container!.payload.counter, }), }), }, })); ``` ## Cache > **!NOTE** Cache is usable **only** to store a payload of forwarded requests To work with cache the mock server uses [ioredis](https://www.npmjs.com/package/ioredis) package To configure it use `database.redis` configuration on the mock server start options **Example** ```ts const server = await MockServer.start({ host: 'localhost', port: 8080, databases: { redis: { host: 'localhost', port: 6379, }, }, }); ``` **How it works in steps?** 0. [Expectation schema](#schema) should have `forward` configuration specified 1. Preparing incoming request... 2. Preparing [request schema](#schema) in expectation... 3. Setting up cache configuration from [context](#context) or [forward.cache](#forwarding)... 4. If `cache.isEnabled` is equals `true` the mock server checks a cache using provided configuration 5. If `key` was not provided a key for cache will calculated with `path`, `method`, `data` and `query` property values using [FNV1A-64](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) algorithm 6. If cache was found then step `7` is skipping 7. Forwarding a request.... 8. Preparing [response schema](#schema) in expectation... 9. Setting up cache configuration from [context](#context)... 10. If `cache.isEnabled` is equals `true` the mock server will write a cache over provided `ttl` 11. Replying... **Example** ```ts await client.createExpectation(({ $ }) => ({ schema: { response: $.set('cache', '$path', 'isEnabled', { $exec: (payload, { context }) => context.outgoing.status < 400, }), forward: { baseUrl: 'https://example.com', cache: { ttl: 30 * 24 * 60 * 60, }, }, }, })); ``` ## State State is a unique storage of each request. It can be used to handle complex expectations 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 **Example** ```ts await client.createExpectation<{ state: { id?: number; }; incoming: { query: { foo: 'a' | 'b' | 'c'; }; }; outgoing: { data: { id: number; }; }; }>(({ $ }) => ({ schema: { request: $.and([ $.switch('incoming.query', '$exec', (payload) => payload.foo, { $cases: { 'a': $.set('state', '$path', 'id', { $value: 1 }), 'b': $.set('state', '$path', 'id', { $value: 2 }), }, }), ]), response: $.set('outgoing.data', { $exec: (payload, { state }) => ({ id: state.id ?? 0 }), }), }, })); ``` ## Seeds Seeds can help to generate content with the same values each request using [faker](https://www.npmjs.com/package/@faker-js/faker) By default a number of seed takes from `X-Use-Mock-Seed` in `incoming.headers` **Example** ```ts await client.createExpectation(({ $ }) => ({ schema: { request: $.and([ $.set('seed', { $exec: (seed) => seed ?? 123 }), ]), response: $.set('outgoing.data', { $exec: (payload, { faker }) => ({ id: faker.number.int({ max: 1000, min: 500 }), first_name: faker.person.firstName('male'), last_name: faker.person.lastName('male'), }), }), }, })); ``` ## XML 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: ```ts { ignoreAttributes: false, } ``` To define a `incoming.data` as XML in incoming request `incoming.headers` should have `Content-Type: application/xml`. The same with `outgoing.data` and `outgoing.headers` **Example of serialized XML** ```xml <tag type="default"> <nested type="nested">456</nested> 123 </tag> ``` **Example of parsed XML** ```json { "tag":{ "nested":{ "#text":456, "@_type":"nested" }, "#text":123, "@_type":"default" } } ``` To parse an XML manually the application lib provides utils: ```ts import { parsePayload, serializePayload } from '@n1k1t/mock-server'; const parsed = parsePayload('xml', '<tag>123</tag>'); // { tag: 123 } const serialized = serializePayload('xml', parsed); // '<tag>123</tag>' ``` # API 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) The `HTTP API` and `RemoteClient` have some usage restrictions like: - 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 - Plugins are not supported ## Ping `INPUT``GET /_system/ping` `OUTPUT` | Type | Description | |--|--| | `string` | A `pong` message | **Using cURL** ```bash curl -H "Content-type: application/json" --location "localhost:8080/_system/ping" ``` **Using application lib on server side** ```ts import { MockServer } from '@n1k1t/mock-server'; const server = await MockServer.start({ host: 'localhost', port: 8080 }); await server.client.ping(); ``` **Using application lib on remotely** ```ts import { RemoteClient } from '@n1k1t/mock-server'; const client = await RemoteClient.connect({ host: 'localhost', port: 8080 }); await client.ping(); ``` ## Create expectation `INPUT``POST /_system/expectations` | Property | Nested | Type | Optional | Description | |--|--|--|--|--| | schema | [Schema](#schema) | `object` | | An expectation schema | | name | | `string` | * | A preferred name for an expectation | `OUTPUT` | Property | Nested | Type | Optional | Description | |--|--|--|--|--| | id | | `string` | | An expectation ID | | name | | `string` | | An expectation name | | schema | [Schema](#schema) | `object` | | Provided schema | **Using cURL** ```bash curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "schema": { "request": { "\$has": { "\$location": "method", "\$value": "GET" } } } } EOF ``` **Using application lib on server side** ```ts import { MockServer } from '@n1k1t/mock-server'; const server = await MockServer.start({ host: 'localhost', port: 8080 }); const expectation = await server.client.createExpectation({ schema: { request: { $has: { $location: 'method', $value: 'GET', }, }, }, }); console.log('Mock expectation has created', expectation.id); ``` **Using application lib on remotely** ```ts import { RemoteClient } from '@n1k1t/mock-server'; const client = await RemoteClient.connect({ host: 'localhost', port: 8080 }); const expectation = await client.createExpectation({ schema: { request: { $has: { $location: 'method', $value: 'GET', }, }, }, }); console.log('Mock expectation has created', expectation.id); ``` ## Update expectation `INPUT``PUT /_system/expectations` | Property | Nested | Type | Optional | Description | |--|--|--|--|--| | id | | `string` | | ID of a registered expectation | | set | | `object` | | A payload to set | | | name | `string` | * | A preferred name for an expectation | | | schema | [Schema](#schema) | * | An expectation schema | `OUTPUT` | Property | Nested | Type | Optional | Description | |--|--|--|--|--| | id | | `string` | | An expectation ID | | name | | `string` | | An expectation name | | schema | [Schema](#schema) | `object` | | Provided schema | **Using cURL** ```bash curl -H "Content-type: application/json" -X PUT --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "id": "...", "set": {"name": "The expectation"} } EOF ``` **Using application lib on server side** ```ts import { MockServer } from '@n1k1t/mock-server'; const server = await MockServer.start({ host: 'localhost', port: 8080 }); const expectation = await server.client.updateExpectation({ id: '...', set: { name: 'The expectation' } }); console.log('Mock expectation has updated', expectation); ``` **Using application lib on remotely** ```ts import { RemoteClient } from '@n1k1t/mock-server'; const client = await RemoteClient.connect({ host: 'localhost', port: 8080 }); const expectation = await client.updateExpectation({ id: '...', set: { name: 'The expectation' } }); console.log('Mock expectation has updated', expectation); ``` ## Delete expectation `INPUT``DELETE /_system/expectations` | Property | Nested | Type | Optional | Description | |--|--|--|--|--| | ids | | `string[]` | * | An expectation IDs list to delete. Or **delete all expectations** if not provided | **Using cURL** ```bash curl -H "Content-type: application/json" -X DELETE --location "localhost:8080/_system/expectations" --data-binary @- << EOF { "ids": ["..."] } EOF ``` **Using application lib on server side** ```ts import { MockServer } from '@n1k1t/mock-server'; const server = await MockServer.start({ host: 'localhost', port: 8080 }); await server.client.deleteExpectations({ ids: ['...'], }); ``` **Using application lib on remotely** ```ts import { RemoteClient } from '@n1k1t/mock-server'; const client = await RemoteClient.connect({ host: 'localhost', port: 8080 }); await client.deleteExpectations({ ids: ['...'], }); ``` # Additional ## Configuration > **!NOTE** Configuration must be provided in the same script like mock server ```ts import { config } from '@n1k1t/mock-server'; config.merge({ logger: { level: 'D', // Logger level (default: D) }, history: { limit: 100, // Limit for history of requests (default: 100) }, containers: { expiredCleaningInterval: 60 * 60, // Expired containers cleaning interval in seconds (default: 1h) }, }); ``` ## Logger > **!NOTE** Configuration must be provided in the same script like mock server ```ts import { Logger } from '@n1k1t/mock-server'; // It defines your own logger methods Logger.useExternal({ debug: (...messages: string[]) => console.debug(...messages), info: (...messages: string[]) => console.log(...messages), warn: (...messages: string[]) => console.warn(...messages), error: (...messages: string[]) => console.error(...messages), fatal: (...messages: string[]) => console.error(...messages), }); // It defines a JSON serializers to mask some private data by keys on objects Logger.useSerializers({ cvv: () => '***', card: (payload: string) => payload.slice(0, 8) + 'xxxx', }); ``` ## Meta 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`. The `metaStorage.provide()` returns an instance of `meta` that contains basic data like: | Property | Type | Optional | Description | |--|--|--|--| | operationId | `string` | | UUID v4 | | requestId | `string` | * | `X-Request-Id` from `incoming.headers` | **Setup** ```ts import { Logger, metaStorage } from '@n1k1t/mock-server'; // Some external logger with meta context support const external = {...}; // It defines your own logger methods Logger.useExternal({ debug: (...messages: string[]) => external.debug(metaStorage.provide(), ...messages), info: (...messages: string[]) => external.log(metaStorage.provide(), ...messages), warn: (...messages: string[]) => external.warn(metaStorage.provide(), ...messages), error: (...messages: string[]) => external.error(metaStorage.provide(), ...messages), fatal: (...messages: string[]) => external.error(metaStorage.provide(), ...messages), }); ``` **Usage** ```ts await server.client.createExpectation({ schema: { request: { $exec: ({ context, logger }) => { // Here logger should have a meta context like { operationId: '...' } logger.info('Before') }, $exec: ({ context, logger, meta }) => { // It enriches meta context for further logs of request meta.merge({ foo: 'bar' }); }, $exec: ({ context, logger, meta }) => { // Now logger should have a meta context like { foo: 'bar', operationId: '...' } logger.info('After') }, }, }, }); ```