@n1k1t/mock-server 0.3.0 → 1.0.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.
Files changed (248) hide show
  1. package/README.md +583 -1013
  2. package/README.old.md +1255 -0
  3. package/lib/package.json +15 -6
  4. package/lib/src/client/helpers/expectations.d.ts +15 -45
  5. package/lib/src/client/helpers/expectations.d.ts.map +1 -1
  6. package/lib/src/client/helpers/expectations.js.map +1 -1
  7. package/lib/src/client/methods/expectations-group.update.method.js +1 -1
  8. package/lib/src/client/methods/expectations-group.update.method.js.map +1 -1
  9. package/lib/src/client/methods/expectations.create.method.js +1 -1
  10. package/lib/src/client/methods/expectations.create.method.js.map +1 -1
  11. package/lib/src/client/methods/expectations.delete.method.js +1 -1
  12. package/lib/src/client/methods/expectations.delete.method.js.map +1 -1
  13. package/lib/src/client/methods/expectations.update.method.js +1 -1
  14. package/lib/src/client/methods/expectations.update.method.js.map +1 -1
  15. package/lib/src/client/methods/ping.method.js +1 -1
  16. package/lib/src/client/methods/ping.method.js.map +1 -1
  17. package/lib/src/client/methods/providers.create.method.js +1 -1
  18. package/lib/src/client/methods/providers.create.method.js.map +1 -1
  19. package/lib/src/client/methods/providers.delete.method.js +1 -1
  20. package/lib/src/client/methods/providers.delete.method.js.map +1 -1
  21. package/lib/src/client/models/client.d.ts +4 -4
  22. package/lib/src/client/models/client.d.ts.map +1 -1
  23. package/lib/src/client/models/client.js +6 -3
  24. package/lib/src/client/models/client.js.map +1 -1
  25. package/lib/src/client/models/types.d.ts +3 -0
  26. package/lib/src/client/models/types.d.ts.map +1 -1
  27. package/lib/src/client/models/types.js.map +1 -1
  28. package/lib/src/client/types.d.ts +1 -1
  29. package/lib/src/client/types.d.ts.map +1 -1
  30. package/lib/src/client/utils.d.ts +1 -1
  31. package/lib/src/client/utils.d.ts.map +1 -1
  32. package/lib/src/client/utils.js.map +1 -1
  33. package/lib/src/config/index.d.ts +1 -1
  34. package/lib/src/config/index.js +1 -1
  35. package/lib/src/config/index.js.map +1 -1
  36. package/lib/src/expectations/__utils__/index.d.ts.map +1 -1
  37. package/lib/src/expectations/__utils__/index.js +0 -1
  38. package/lib/src/expectations/__utils__/index.js.map +1 -1
  39. package/lib/src/expectations/models/expectation.d.ts +1 -1
  40. package/lib/src/expectations/models/expectation.d.ts.map +1 -1
  41. package/lib/src/expectations/models/expectation.js.map +1 -1
  42. package/lib/src/expectations/models/storage.d.ts +5 -5
  43. package/lib/src/expectations/models/storage.d.ts.map +1 -1
  44. package/lib/src/expectations/models/storage.js.map +1 -1
  45. package/lib/src/expectations/types.d.ts +18 -15
  46. package/lib/src/expectations/types.d.ts.map +1 -1
  47. package/lib/src/expectations/types.js +0 -1
  48. package/lib/src/expectations/types.js.map +1 -1
  49. package/lib/src/expectations/utils/location.d.ts.map +1 -1
  50. package/lib/src/expectations/utils/location.js +0 -6
  51. package/lib/src/expectations/utils/location.js.map +1 -1
  52. package/lib/src/server/endpoints/cache.backup.endpoint.js +1 -1
  53. package/lib/src/server/endpoints/cache.backup.endpoint.js.map +1 -1
  54. package/lib/src/server/endpoints/cache.delete.endpoint.js +1 -1
  55. package/lib/src/server/endpoints/cache.delete.endpoint.js.map +1 -1
  56. package/lib/src/server/endpoints/cache.restore.endpoint.d.ts.map +1 -1
  57. package/lib/src/server/endpoints/cache.restore.endpoint.js +8 -3
  58. package/lib/src/server/endpoints/cache.restore.endpoint.js.map +1 -1
  59. package/lib/src/server/endpoints/cache.restore.stream.endpoint.js +1 -1
  60. package/lib/src/server/endpoints/cache.restore.stream.endpoint.js.map +1 -1
  61. package/lib/src/server/endpoints/cache.usage.get.endpoint.js +1 -1
  62. package/lib/src/server/endpoints/cache.usage.get.endpoint.js.map +1 -1
  63. package/lib/src/server/endpoints/config.get.endpoint.d.ts +1 -1
  64. package/lib/src/server/endpoints/expectations.get-by-id.endpoint.js +1 -1
  65. package/lib/src/server/endpoints/expectations.get-by-id.endpoint.js.map +1 -1
  66. package/lib/src/server/endpoints/expectations.update.endpoint.d.ts +1 -1
  67. package/lib/src/server/endpoints/expectations.update.endpoint.d.ts.map +1 -1
  68. package/lib/src/server/endpoints/gui.endpoint.js +2 -2
  69. package/lib/src/server/endpoints/gui.endpoint.js.map +1 -1
  70. package/lib/src/server/endpoints/history.compact.get-list.endpoint.d.ts +2 -2
  71. package/lib/src/server/endpoints/history.get-by-id.endpoint.d.ts +1 -1
  72. package/lib/src/server/endpoints/history.get-by-id.endpoint.js +1 -1
  73. package/lib/src/server/endpoints/history.get-by-id.endpoint.js.map +1 -1
  74. package/lib/src/server/index.d.ts +2 -5
  75. package/lib/src/server/index.d.ts.map +1 -1
  76. package/lib/src/server/index.js +10 -9
  77. package/lib/src/server/index.js.map +1 -1
  78. package/lib/src/server/models/context/index.d.ts +6 -6
  79. package/lib/src/server/models/context/index.d.ts.map +1 -1
  80. package/lib/src/server/models/context/index.js +0 -4
  81. package/lib/src/server/models/context/index.js.map +1 -1
  82. package/lib/src/server/models/context/snapshot.d.ts +2 -3
  83. package/lib/src/server/models/context/snapshot.d.ts.map +1 -1
  84. package/lib/src/server/models/context/snapshot.js +6 -6
  85. package/lib/src/server/models/context/snapshot.js.map +1 -1
  86. package/lib/src/server/models/context/types.d.ts +10 -9
  87. package/lib/src/server/models/context/types.d.ts.map +1 -1
  88. package/lib/src/server/models/context/utils.d.ts.map +1 -1
  89. package/lib/src/server/models/context/utils.js +2 -6
  90. package/lib/src/server/models/context/utils.js.map +1 -1
  91. package/lib/src/server/models/endpoint.d.ts +2 -2
  92. package/lib/src/server/models/endpoint.d.ts.map +1 -1
  93. package/lib/src/server/models/executor/index.d.ts +2 -2
  94. package/lib/src/server/models/executor/index.d.ts.map +1 -1
  95. package/lib/src/server/models/executor/index.js +38 -6
  96. package/lib/src/server/models/executor/index.js.map +1 -1
  97. package/lib/src/server/models/history/model.d.ts +4 -6
  98. package/lib/src/server/models/history/model.d.ts.map +1 -1
  99. package/lib/src/server/models/history/model.js +2 -7
  100. package/lib/src/server/models/history/model.js.map +1 -1
  101. package/lib/src/server/models/index.d.ts +2 -0
  102. package/lib/src/server/models/index.d.ts.map +1 -1
  103. package/lib/src/server/models/index.js +2 -0
  104. package/lib/src/server/models/index.js.map +1 -1
  105. package/lib/src/server/models/message.d.ts +16 -0
  106. package/lib/src/server/models/message.d.ts.map +1 -0
  107. package/lib/src/server/models/message.js +53 -0
  108. package/lib/src/server/models/message.js.map +1 -0
  109. package/lib/src/server/models/providers/model.d.ts +2 -2
  110. package/lib/src/server/models/providers/model.d.ts.map +1 -1
  111. package/lib/src/server/models/providers/storage.js +1 -1
  112. package/lib/src/server/models/providers/storage.js.map +1 -1
  113. package/lib/src/server/models/providers/system.d.ts.map +1 -1
  114. package/lib/src/server/models/providers/system.js +0 -1
  115. package/lib/src/server/models/providers/system.js.map +1 -1
  116. package/lib/src/server/models/router.js +1 -1
  117. package/lib/src/server/models/router.js.map +1 -1
  118. package/lib/src/server/models/transports/storage.js +1 -1
  119. package/lib/src/server/models/transports/storage.js.map +1 -1
  120. package/lib/src/server/models/websocket/errors/index.d.ts +2 -0
  121. package/lib/src/server/models/websocket/errors/index.d.ts.map +1 -0
  122. package/lib/src/server/models/websocket/errors/index.js +18 -0
  123. package/lib/src/server/models/websocket/errors/index.js.map +1 -0
  124. package/lib/src/server/models/websocket/errors/websocket-connection.error.d.ts +9 -0
  125. package/lib/src/server/models/websocket/errors/websocket-connection.error.d.ts.map +1 -0
  126. package/lib/src/server/models/websocket/errors/websocket-connection.error.js +32 -0
  127. package/lib/src/server/models/websocket/errors/websocket-connection.error.js.map +1 -0
  128. package/lib/src/server/models/websocket/factory.d.ts +9 -0
  129. package/lib/src/server/models/websocket/factory.d.ts.map +1 -0
  130. package/lib/src/server/models/websocket/factory.js +23 -0
  131. package/lib/src/server/models/websocket/factory.js.map +1 -0
  132. package/lib/src/server/models/websocket/index.d.ts +5 -0
  133. package/lib/src/server/models/websocket/index.d.ts.map +1 -0
  134. package/lib/src/server/models/websocket/index.js +21 -0
  135. package/lib/src/server/models/websocket/index.js.map +1 -0
  136. package/lib/src/server/models/websocket/model.d.ts +29 -0
  137. package/lib/src/server/models/websocket/model.d.ts.map +1 -0
  138. package/lib/src/server/models/websocket/model.js +164 -0
  139. package/lib/src/server/models/websocket/model.js.map +1 -0
  140. package/lib/src/server/models/websocket/types.d.ts +11 -0
  141. package/lib/src/server/models/websocket/types.d.ts.map +1 -0
  142. package/lib/src/server/models/websocket/types.js +3 -0
  143. package/lib/src/server/models/websocket/types.js.map +1 -0
  144. package/lib/src/server/transports/http.transport/context.d.ts +1 -1
  145. package/lib/src/server/transports/http.transport/context.d.ts.map +1 -1
  146. package/lib/src/server/transports/http.transport/context.js +10 -3
  147. package/lib/src/server/transports/http.transport/context.js.map +1 -1
  148. package/lib/src/server/transports/http.transport/executor.d.ts +3 -13
  149. package/lib/src/server/transports/http.transport/executor.d.ts.map +1 -1
  150. package/lib/src/server/transports/http.transport/executor.js +7 -6
  151. package/lib/src/server/transports/http.transport/executor.js.map +1 -1
  152. package/lib/src/server/transports/http.transport/index.d.ts +1 -1
  153. package/lib/src/server/transports/http.transport/index.d.ts.map +1 -1
  154. package/lib/src/server/transports/http.transport/index.js +2 -3
  155. package/lib/src/server/transports/http.transport/index.js.map +1 -1
  156. package/lib/src/server/transports/index.d.ts +1 -1
  157. package/lib/src/server/transports/index.d.ts.map +1 -1
  158. package/lib/src/server/transports/index.js +1 -1
  159. package/lib/src/server/transports/index.js.map +1 -1
  160. package/lib/src/server/transports/{internal → system}/http.transport/context.d.ts +6 -6
  161. package/lib/src/server/transports/system/http.transport/context.d.ts.map +1 -0
  162. package/lib/src/server/transports/{internal → system}/http.transport/context.js +7 -7
  163. package/lib/src/server/transports/system/http.transport/context.js.map +1 -0
  164. package/lib/src/server/transports/system/http.transport/executor.d.ts +21 -0
  165. package/lib/src/server/transports/system/http.transport/executor.d.ts.map +1 -0
  166. package/lib/src/server/transports/{internal → system}/http.transport/executor.js +5 -5
  167. package/lib/src/server/transports/system/http.transport/executor.js.map +1 -0
  168. package/lib/src/server/transports/{internal → system}/http.transport/index.d.ts +5 -5
  169. package/lib/src/server/transports/system/http.transport/index.d.ts.map +1 -0
  170. package/lib/src/server/transports/{internal → system}/http.transport/index.js +5 -5
  171. package/lib/src/server/transports/system/http.transport/index.js.map +1 -0
  172. package/lib/src/server/transports/system/http.transport/reply.d.ts +10 -0
  173. package/lib/src/server/transports/system/http.transport/reply.d.ts.map +1 -0
  174. package/lib/src/server/transports/{internal → system}/http.transport/reply.js +4 -4
  175. package/lib/src/server/transports/system/http.transport/reply.js.map +1 -0
  176. package/lib/src/server/transports/system/index.d.ts.map +1 -0
  177. package/lib/src/server/transports/system/index.js.map +1 -0
  178. package/lib/src/server/transports/{internal → system}/io.transport/context.d.ts +6 -6
  179. package/lib/src/server/transports/system/io.transport/context.d.ts.map +1 -0
  180. package/lib/src/server/transports/{internal → system}/io.transport/context.js +6 -6
  181. package/lib/src/server/transports/system/io.transport/context.js.map +1 -0
  182. package/lib/src/server/transports/{internal → system}/io.transport/executor.d.ts +3 -3
  183. package/lib/src/server/transports/system/io.transport/executor.d.ts.map +1 -0
  184. package/lib/src/server/transports/{internal → system}/io.transport/executor.js +3 -3
  185. package/lib/src/server/transports/system/io.transport/executor.js.map +1 -0
  186. package/lib/src/server/transports/system/io.transport/index.d.ts +14 -0
  187. package/lib/src/server/transports/system/io.transport/index.d.ts.map +1 -0
  188. package/lib/src/server/transports/{internal → system}/io.transport/index.js +5 -5
  189. package/lib/src/server/transports/system/io.transport/index.js.map +1 -0
  190. package/lib/src/server/transports/system/io.transport/reply.d.ts +10 -0
  191. package/lib/src/server/transports/system/io.transport/reply.d.ts.map +1 -0
  192. package/lib/src/server/transports/{internal → system}/io.transport/reply.js +4 -4
  193. package/lib/src/server/transports/system/io.transport/reply.js.map +1 -0
  194. package/lib/src/server/transports/system/utils.d.ts.map +1 -0
  195. package/lib/src/server/transports/system/utils.js.map +1 -0
  196. package/lib/src/server/transports/ws.transport/context.d.ts +13 -12
  197. package/lib/src/server/transports/ws.transport/context.d.ts.map +1 -1
  198. package/lib/src/server/transports/ws.transport/context.js +20 -28
  199. package/lib/src/server/transports/ws.transport/context.js.map +1 -1
  200. package/lib/src/server/transports/ws.transport/executor.d.ts +7 -5
  201. package/lib/src/server/transports/ws.transport/executor.d.ts.map +1 -1
  202. package/lib/src/server/transports/ws.transport/executor.js +77 -25
  203. package/lib/src/server/transports/ws.transport/executor.js.map +1 -1
  204. package/lib/src/server/transports/ws.transport/index.d.ts +3 -3
  205. package/lib/src/server/transports/ws.transport/index.d.ts.map +1 -1
  206. package/lib/src/server/transports/ws.transport/index.js +29 -54
  207. package/lib/src/server/transports/ws.transport/index.js.map +1 -1
  208. package/lib/src/server/types/index.d.ts +2 -4
  209. package/lib/src/server/types/index.d.ts.map +1 -1
  210. package/lib/src/utils/common.d.ts +6 -3
  211. package/lib/src/utils/common.d.ts.map +1 -1
  212. package/lib/src/utils/common.js +22 -11
  213. package/lib/src/utils/common.js.map +1 -1
  214. package/package.json +15 -6
  215. package/public/assets/{index-DoUtVarD.js → index-DoLXR0S_.js} +6 -1
  216. package/public/index.html +1 -1
  217. package/lib/bin/index.d.ts +0 -3
  218. package/lib/bin/index.d.ts.map +0 -1
  219. package/lib/bin/index.js +0 -32
  220. package/lib/bin/index.js.map +0 -1
  221. package/lib/src/server/transports/internal/http.transport/context.d.ts.map +0 -1
  222. package/lib/src/server/transports/internal/http.transport/context.js.map +0 -1
  223. package/lib/src/server/transports/internal/http.transport/executor.d.ts +0 -21
  224. package/lib/src/server/transports/internal/http.transport/executor.d.ts.map +0 -1
  225. package/lib/src/server/transports/internal/http.transport/executor.js.map +0 -1
  226. package/lib/src/server/transports/internal/http.transport/index.d.ts.map +0 -1
  227. package/lib/src/server/transports/internal/http.transport/index.js.map +0 -1
  228. package/lib/src/server/transports/internal/http.transport/reply.d.ts +0 -10
  229. package/lib/src/server/transports/internal/http.transport/reply.d.ts.map +0 -1
  230. package/lib/src/server/transports/internal/http.transport/reply.js.map +0 -1
  231. package/lib/src/server/transports/internal/index.d.ts.map +0 -1
  232. package/lib/src/server/transports/internal/index.js.map +0 -1
  233. package/lib/src/server/transports/internal/io.transport/context.d.ts.map +0 -1
  234. package/lib/src/server/transports/internal/io.transport/context.js.map +0 -1
  235. package/lib/src/server/transports/internal/io.transport/executor.d.ts.map +0 -1
  236. package/lib/src/server/transports/internal/io.transport/executor.js.map +0 -1
  237. package/lib/src/server/transports/internal/io.transport/index.d.ts +0 -14
  238. package/lib/src/server/transports/internal/io.transport/index.d.ts.map +0 -1
  239. package/lib/src/server/transports/internal/io.transport/index.js.map +0 -1
  240. package/lib/src/server/transports/internal/io.transport/reply.d.ts +0 -10
  241. package/lib/src/server/transports/internal/io.transport/reply.d.ts.map +0 -1
  242. package/lib/src/server/transports/internal/io.transport/reply.js.map +0 -1
  243. package/lib/src/server/transports/internal/utils.d.ts.map +0 -1
  244. package/lib/src/server/transports/internal/utils.js.map +0 -1
  245. /package/lib/src/server/transports/{internal → system}/index.d.ts +0 -0
  246. /package/lib/src/server/transports/{internal → system}/index.js +0 -0
  247. /package/lib/src/server/transports/{internal → system}/utils.d.ts +0 -0
  248. /package/lib/src/server/transports/{internal → system}/utils.js +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align='center'>
2
2
  <h1>Mock server</h1>
3
- <p>Mock, match, modify and manipulate a HTTP request/response payload using flexible expectations with types</p>
3
+ <p>The ultimate toolkit to <b>intercept, transform, and simulate</b> HTTP/WS traffic with type-safe expectations</p>
4
4
 
5
5
  <img src="https://raw.githubusercontent.com/n1k1t/mock-server/refs/heads/master/images/preview.png?raw=true" />
6
6
 
@@ -12,1244 +12,814 @@
12
12
  ![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)
13
13
  </div>
14
14
 
15
- # Navigation
16
-
17
- - [Basics](#basics)
18
- - [How it works](#how-it-works)
19
- - [Install](#install)
20
- - [Start](#start)
21
- - [GUI](#gui)
22
- - [Mock](#mock)
23
- - [Expectations](#expectations)
24
- - [Schema](#schema)
25
- - [Forwarding](#forwarding)
26
- - [Context](#context)
27
- - [Utils](#utils)
28
- - [Operators](#operators)
29
- - [Typings](#typings)
30
- - [Storage](#storage)
31
- - [Containers](#containers)
32
- - [Cache](#cache)
33
- - [State](#state)
34
- - [Seeds](#seeds)
35
- - [XML](#xml)
36
- - [API](#api)
37
- - [Ping](#ping)
38
- - [Create expectation](#create-expectation)
39
- - [Update expectation](#update-expectation)
40
- - [Delete expectation](#delete-expectation)
41
- - [Additional](#additional)
42
- - [Configuration](#configuration)
43
- - [Logger](#logger)
44
- - [Meta](#meta)
45
-
46
- # Basics
47
-
48
- ## Install
15
+ This mock server provides complete control over your network layer. It allows you to simulate missing APIs, modify real backend responses on the fly, and proxy traffic with advanced caching. Designed for flexibility, it handles complex scenarios with ease while maintaining strict type safety across all your expectations.
16
+
17
+ - [Features](#features)
18
+ - [Installation](#installation)
19
+ - [Simple Example](#simple-example)
20
+ - [Overview](#overview)
21
+ - [GUI](#gui)
22
+ - [Storage & Containers](#storage--containers)
23
+ - [Cache](#cache)
24
+ - [State & Seeds](#state--seeds)
25
+ - [Mock Server Utilities](#mock-server-utilities)
26
+ - [Expectation Operators ($)](#expectation-operators-)
27
+ - [Operator Locations](#operator-locations)
28
+ - [Operator Context](#operator-context)
29
+ - [Container Methods](#container-methods)
30
+ - [Usage](#usage)
31
+ - [Complex Expectations with $and](#complex-expectations-with-and)
32
+ - [Custom Logic with $exec](#custom-logic-with-exec)
33
+ - [Dynamic Response Manipulation](#dynamic-response-manipulation)
34
+ - [Binary Data and Files](#binary-data-and-files)
35
+ - [XML Support](#xml-support)
36
+ - [Simulating Network Errors](#simulating-network-errors)
37
+ - [Disabled By Default](#disabled-by-default)
38
+ - [Type Safety](#type-safety)
39
+ - [WebSocket Support](#websocket-support)
40
+ - [Request Forwarding](#request-forwarding)
41
+ - [Using Relative Paths with baseUrl](#using-relative-paths-with-baseurl)
42
+ - [Transforming Request Data Before Forwarding](#transforming-request-data-before-forwarding)
43
+ - [WebSocket Forwarding and Manipulation](#websocket-forwarding-and-manipulation)
44
+ - [Conditional Caching](#conditional-caching)
45
+ - [Custom Cache Key](#custom-cache-key)
46
+ - [Request State](#request-state)
47
+ - [Storage and Containers](#storage-and-containers)
48
+ - [Cross-Expectation Sync](#cross-expectation-sync)
49
+ - [Extensions](#extensions)
50
+ - [Redis Configuration](#redis-configuration)
51
+ - [Remote Expectations with RemoteClient](#remote-expectations-with-remoteclient)
52
+ - [Grouping with Providers](#grouping-with-providers)
53
+ - [Logger](#logger)
54
+ - [License](#license)
55
+
56
+ ## Features
57
+
58
+ - **Multi-protocol Support**: Full support for both **HTTP** and **WebSocket** protocols.
59
+ - **Flexible Matching**: Match requests by path, method, headers, and data using regex, minimatch, or custom functions.
60
+ - **Payload Manipulation**: Modify request or response payloads on the fly.
61
+ - **Request Forwarding**: Proxy requests to real services with optional caching.
62
+ - **Built-in GUI**: Monitor traffic and manage expectations through a web interface.
63
+ - **Typed Expectations**: First-class support for TypeScript to keep your mocks type-safe.
64
+ - **Advanced State Management**: Store data across requests using Containers or Request State.
65
+ - **XML Support**: Native parsing and serialization for XML payloads.
66
+
67
+ ## Installation
49
68
 
50
69
  ```bash
51
70
  npm i @n1k1t/mock-server
52
71
  ```
53
72
 
54
- ## How it works
55
-
56
- ![screenshot](https://raw.githubusercontent.com/n1k1t/mock-server/refs/heads/master/images/strategy.png?raw=true)
57
-
58
- 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
59
-
60
- **In case of mocking without request forwarding:**
61
-
62
- 1. Start mock server (for example on `localhost:8080`)
63
- 2. Register expectation using CLI (cURL) or application lib
64
- 3. Make request to `localhost:8080/...`
65
- 1. The mock server matches a request payload with registered expectations
66
- 2. Build a response using an expectation configuration
67
-
68
- **In case of mocking with request forwarding:**
69
-
70
- 0. Lets imagine that you have a service that hosts on `localhost:8081`
71
- 1. Start mock server (for example on `localhost:8080`)
72
- 2. Register expectation using CLI (cURL) or application lib
73
- 3. Make request to `localhost:8080/...`
74
- 1. The mock server matches a request payload with registered expectations
75
- 2. Next is forwarding a request payload to `localhost:8081/...`
76
- 3. Using response fetched from `localhost:8081/...` the mock server builds a response
77
-
78
- ## Start
79
-
80
- ### CLI
81
-
82
- ```bash
83
- npx mock -h localhost -p 8080
84
- ```
85
-
86
- ### JavaScript
87
-
88
- ```js
89
- const { MockServer } = require('@n1k1t/mock-server');
90
- MockServer.start({ host: 'localhost', port: 8080 });
91
- ```
92
-
93
- ### TypeScript
73
+ ## Simple Example
94
74
 
95
75
  ```ts
96
76
  import { MockServer } from '@n1k1t/mock-server';
97
- MockServer.start({ host: 'localhost', port: 8080 });
98
- ```
99
77
 
100
- ## GUI
78
+ const server = await MockServer.start({ host: 'localhost', port: 8080 });
101
79
 
102
- The mock server provides built-in web panel to track everything that is going through. There are two tabs `Expectations` and `History`
80
+ // Create a type-safe expectation using the operator compiler ($)
81
+ await server.client.createExpectation(({ $ }) => ({
82
+ schema: {
83
+ request: $.has('incoming.path', { $value: '/api/hello' }),
84
+ response: $.set('outgoing.data', { $value: { message: 'world' } }),
85
+ },
86
+ }));
87
+ ```
103
88
 
104
- By default it can be found on `/_system/gui` of a host of mock server. Example: `localhost:8080/_system/gui`
89
+ ## Overview
105
90
 
106
- Also it provides convenient util to navigate through payload of expectations and requests payload
91
+ ### GUI
107
92
 
108
- ## Mock
93
+ The mock server provides a built-in web panel to track everything that is going through. By default, it can be found on `/_system/gui`. Example: `localhost:8080/_system/gui`.
109
94
 
110
- Simple examples can be found in [expectation creation API](#create-expectation)
95
+ ### Storage & Containers
111
96
 
112
- # Expectations
97
+ Storage provides access to read/write **Containers**—temporary cells used to sync expectations or store data between requests. Containers have a configurable TTL (Time to Live) and can be merged or assigned new payloads.
113
98
 
114
- ## Schema
99
+ ### Cache
115
100
 
116
- An expectation schema can contain some rules to handle `request`, `response` and `forward`
101
+ Cache is used to store payloads of forwarded requests, powered by [ioredis](https://www.npmjs.com/package/ioredis). It allows for long-term persistence of mocked responses from real backends.
117
102
 
118
- | Property | Nested | Type | Optional | Description |
119
- |--|--|--|--|--|
120
- | request | [Operators](#operators) | `object` | * | Describes a way to catch by request and how to manipulate it |
121
- | response | [Operators](#operators) | `object` | * | Describes how to manipulate response. Also can be used to catch response in case of forwarding |
122
- | forward | [Forwarding](#forwarding) | `object` | * | Describes configuration to forward a request to another host |
103
+ ### State & Seeds
123
104
 
124
- **Example**
105
+ - **State**: A unique storage for each individual request, typically extracted from headers. By default, it's extracted from the `X-Use-Mock-State` header (JSON in Base64).
106
+ - **Seeds**: Help generate consistent random data using [faker](https://www.npmjs.com/package/@faker-js/faker). By default, the seed value is extracted from the `X-Use-Mock-Seed` header. You can also manually set the seed within an expectation using the `seed` location:
125
107
 
126
108
  ```ts
127
- await server.client.createExpectation({
109
+ await server.client.createExpectation(({ $ }) => ({
128
110
  schema: {
129
- request: {
130
- $and: [],
131
- },
132
- response: {
133
- $or: [],
134
- },
135
- forward: {
136
- baseUrl: 'https://example.com',
137
- url: '/some/path',
138
- },
111
+ request: $.and([
112
+ $.has('incoming.path', { $value: '/api/faker' }),
113
+ // Manually set seed from query parameter or any other logic
114
+ $.set('seed', { $exec: (seed, { context }) => context.incoming.query.seed ?? context.seed ?? 12345 }),
115
+ ]),
116
+ response: $.set('outgoing.data', {
117
+ $exec: (payload, { faker }) => ({
118
+ id: faker.number.int(), // Will be consistent for the same seed
119
+ name: faker.person.fullName(),
120
+ }),
121
+ }),
139
122
  },
140
- });
123
+ }));
141
124
  ```
142
125
 
143
- ## Forwarding
144
-
145
- | Property | Nested | Type | Optional | Description |
146
- |--|--|--|--|--|
147
- | url | | `string` | * | Absolute URL to target |
148
- | baseUrl | | `string` | * | Base URL to target. The path will be provided from request |
149
- | options | | `string` | * | Forwarding options |
150
- | | 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 |
151
- | cache | | `object` | * | [Cache](#cache) configuration for a payload of forwarded requests |
152
- | | storage | `redis` | * | Storage to read/write a cache |
153
- | | key | `string` | * | Key to get read/write access of cached payload |
154
- | | prefix | `string` | * | Prefix of the `key` of cache |
155
- | | ttl | `number` | * | Time to live of cache in seconds |
156
-
157
- ## Context
158
-
159
- | Property | Nested | `$location` | Type | Optional | Description |
160
- |--|--|--|--|--|--|
161
- | storage | [Storage](#storage) | | `object` | | A storage of `container` entities |
162
- | 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 |
163
- | state | | `state` | `object` | | An [object](#state) with custom data |
164
- | seed | | `seed` | `string` | * | Incoming request [seed](#seeds) |
165
- | cache | | `cache` | `object` | | [Cache](#cache) configuration |
166
- | | isEnabled | | `boolean` | | Toggle of cache usage |
167
- | | 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 |
168
- | | prefix | | `string` | * | Prefix of the `key` of cache |
169
- | | ttl | | `number` | * | Time to live of cache in seconds |
170
- | incoming | | | `object` | | Payload with data of incoming request |
171
- | | path | `path` | `string` | | Incoming request path |
172
- | | method | `method` | `string` | | Incoming request method in **uppercase** |
173
- | | headers | `incoming.headers` | `object` | | Incoming request headers with keys in **lowercase** |
174
- | | dataRaw | `incoming.dataRaw` | `string` | | Incoming request source data |
175
- | | data | `incoming.data` | `object` | * | Incoming request parsed data |
176
- | | query | `incoming.query` | `object` | * | Incoming request query search parameters |
177
- | | delay | `delay` | `number` | * | Delay that can be applied with [operators](#operators) |
178
- | | error | `error` | `string` | * | Error that can be applied with [operators](#operators) |
179
- | outgoing | | | `object` | | Payload with data of response |
180
- | | status | `outgoing.status` | `number` | | Response status code |
181
- | | headers | `outgoing.headers` | `number` | | Response headers |
182
- | | dataRaw | `outgoing.dataRaw` | `string` | | Response source data |
183
- | | data | `outgoing.data` | `any` | * | Response data |
184
-
185
- ## Utils
186
-
187
- Additional utils in `$exec` operator
188
-
189
- | Property | Description |
190
- |--|--|
191
- | `context` | A request [context](#context) |
192
- | `logger` | [Logger](#logger) of mock server |
193
- | `mode` | A mode of expectation execution. Has `match` on catching request or `manipulate` on manipulation over [context](#context) |
194
- | `meta` | A [meta](#meta) of a request |
195
- | `_` | [Lodash](https://www.npmjs.com/package/lodash) |
196
- | `d` | [DayJS](https://www.npmjs.com/package/dayjs) |
197
- | `faker` | [Faker](https://www.npmjs.com/package/@faker-js/faker). Uses [seed](#seeds) if it was provided |
198
-
199
- ## Operators
200
-
201
- > **!NOTE** Each schema that using operators can have only one nested operator. To use more than one operator use `$and` or `$or` operators
202
-
203
- | Operator | Optional | Description |
204
- |--|--|--|
205
- | [$has](#has) | * | Catches a request/response or checks a payload in [context](#context) |
206
- | [$set](#set) | * | Sets payload in [context](#context) |
207
- | [$merge](#merge) | * | Merges object payload in [context](#context) with provided `$value` |
208
- | [$remove](#remove) | * | Removes payload in [context](#context) |
209
- | [$exec](#exec) | * | Function to catch a request/response or check/manipulate payload in [context](#context) |
210
- | [$and](#and) | * | Logical `and` |
211
- | [$or](#or) | * | Logical `or` |
212
- | [$not](#not) | * | Logical `not` |
213
- | [$if](#if) | * | Logical `if` |
214
- | [$switch](#switch) | * | Logical `switch/case` |
215
-
216
- **Example**
217
-
218
- ```ts
219
- await server.client.createExpectation({
126
+ ## Mock Server Utilities
127
+
128
+ ### Expectation Operators ($)
129
+
130
+ Expectations are built using operators that define how to match requests and transform responses.
131
+
132
+ - `$.has(location, { $path, ... })`: Checks if a value exists at the specified path. Supports `$value`, `$match` (minimatch), `$regExp`, and `$exec`.
133
+ - `$.set(location, { $value, ... })`: Sets a value at the specified path.
134
+ - `$.merge(location, { $value, ... })`: Merges an object into the value at the specified path.
135
+ - `$.remove(location, { $path, ... })`: Removes the value at the specified path.
136
+ - `$.and([...operators])`: Logical AND of multiple operators.
137
+ - `$.or([...operators])`: Logical OR of multiple operators.
138
+ - `$.not(operator)`: Logical NOT of an operator.
139
+ - `$.if({ $condition, $then, $else })`: Conditional execution of operators.
140
+ - `$.switch({ $cases, $default })`: Switch-case execution based on the value at the path.
141
+ - `$.exec(fn)`: Executes custom logic. In `match` mode, it must return a boolean. In `manipulate` mode, it can modify the context.
142
+
143
+ ### Operator Locations
144
+
145
+ Many operators (like `$.has`, `$.set`, `$.merge`) accept a `location` as their first argument. This defines where the operation should be performed:
146
+
147
+ - `'incoming.path'`: The URL path of the request.
148
+ - `'incoming.method'`: The HTTP method.
149
+ - `'incoming.query'`: The query parameters object.
150
+ - `'incoming.headers'`: The request headers object.
151
+ - `'incoming.data'`: The request body (parsed JSON or XML).
152
+ - `'outgoing.status'`: The response status code.
153
+ - `'outgoing.headers'`: The response headers object.
154
+ - `'outgoing.data'`: The response body object.
155
+ - `'container'`: Access to a [Container](#container-methods).
156
+ - `'state'`: Access to [Request State](#request-state).
157
+ - `'cache'`: Access to [Forward Cache](#conditional-caching) configuration.
158
+ - `'seed'`: Access to the [Seed](#state--seeds) value.
159
+ - `'error'`: For [Simulating Network Errors](#simulating-network-errors).
160
+
161
+ When targeting an object (like `headers` or `data`), you can use `$path` as the second argument to target a nested value:
162
+ `$.has('incoming.headers', '$path', 'content-type', { $value: 'application/json' })`
163
+
164
+ ### Operator Context
165
+
166
+ When using `$exec`, the second argument provides a context with useful utilities and data:
167
+
168
+ - **`context`**: The full request/response context.
169
+ - `incoming`: Input data (immutable in match mode).
170
+ - `path`: The request URL path.
171
+ - `method`: HTTP method (GET, POST, etc.).
172
+ - `query`: Object containing query parameters.
173
+ - `headers`: Object containing request headers.
174
+ - `data`: Parsed JSON/XML request body.
175
+ - `outgoing`: Output data (target for manipulation).
176
+ - `status`: HTTP status code.
177
+ - `headers`: Response headers.
178
+ - `data`: Response body object (serialized to JSON/XML).
179
+ - `dataRaw`: Buffer for binary data.
180
+ - `stream`: Observable for WebSocket messages.
181
+ - `storage`: Interface to manage [Containers](#container-methods).
182
+ - `container`: The currently bound container (if any).
183
+ - `cache`: Configuration for forwarding cache.
184
+ - `isEnabled`: Whether caching is enabled for this request.
185
+ - `prefix`: Redis key prefix.
186
+ - `key`: Custom cache key (string or object).
187
+ - `ttl`: Time to live in seconds.
188
+ - `hasRead`: (Internal) Whether cache was read.
189
+ - `hasWritten`: (Internal) Whether cache was written.
190
+ - **`state`**: The unique [Request State](#state--seeds). Persistent only for the duration of the current request.
191
+ - **`mode`**: The execution phase: `'match'` (deciding if expectation applies) or `'manipulate'` (preparing the response).
192
+ - **`logger`**: A scoped logger for debugging (`info`, `error`, `warn`).
193
+ - **`faker`**: Full `@faker-js/faker` instance for generating mock data.
194
+ - **`_`**: Lodash utility library.
195
+ - **`d`**: Dayjs instance for date/time manipulation.
196
+ - **`rx`**: RxJS exports for advanced WebSocket stream control.
197
+
198
+ ### Container Methods
199
+
200
+ Containers (accessible via `context.storage`) provide several methods for managing data:
201
+
202
+ - `storage.provide({ key, payload, ttl })`: Retrieves an existing container by key or creates a new one with the provided payload and TTL.
203
+ - `storage.register({ key, payload, ttl })`: Forcefully creates and registers a new container, overwriting any existing one with the same key.
204
+ - `storage.find(key)`: Finds an existing container by key. Returns `undefined` if not found.
205
+ - `container.assign(payload | fn)`: Shallows merges the given payload into the current container state. If a function is provided, it receives the current payload and returns the new one.
206
+ - `container.merge(payload | fn)`: Deeply merges the given payload into the current container state.
207
+ - `container.bind(key)`: Binds the container to an additional key.
208
+ - `container.unbind()`: Removes the container from the storage.
209
+
210
+ ## Usage
211
+
212
+ ### Complex Expectations with `$and`
213
+
214
+ You can group multiple conditions using logical operators like `$.and`, `$.or`, and `$.not` to create sophisticated matching rules.
215
+
216
+ ```ts
217
+ await server.client.createExpectation(({ $ }) => ({
220
218
  schema: {
221
- request: {
222
- $and: [
223
- {
224
- $has: {
225
- $location: 'path',
226
- $value: '/foo',
227
- },
228
- },
229
- {
230
- $has: {
231
- $location: 'method',
232
- $value: 'GET',
233
- },
234
- },
235
- ],
236
- },
219
+ request: $.and([
220
+ $.has('incoming.path', { $value: '/api/user' }),
221
+ $.has('incoming.method', { $value: 'POST' }),
222
+ // Checks if 'content-type' header equals 'application/json' using selection with $path = content-type
223
+ $.has('incoming.headers', '$path', 'content-type', { $value: 'application/json' }),
224
+ ]),
225
+ response: $.set('outgoing.data', { $value: { status: 'success' } }),
237
226
  },
238
- });
227
+ }));
239
228
  ```
240
229
 
241
- ### $has
230
+ ### Custom Logic with `$exec`
242
231
 
243
- > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
244
-
245
- | Property | Type (application) | Type (cURL) | Optional | Description |
246
- |--|--|--|--|--|
247
- | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
248
- | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
249
- | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
250
- | $value | `any` | `any` | * | Checks by value equality in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
251
- | $valueAnyOf | `any[]` | `any[]` | * | Checks by any of value equality in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
252
- | $regExp | `RegExp` | `{ source: string, flags?: string }` | * | Checks by regular expression in context using `$location` (and `$path`, `$jsonPath` if it was specified) |
253
- | $regExpAnyOf | `RegExp[]` | `{ source: string, flags?: string }[]` | * | Checks by any of regular expression in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
254
- | $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) |
255
- | $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) |
256
- | $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) |
257
-
258
- **Example using application**
232
+ For scenarios that require dynamic calculations or external utilities, use the `$exec` operator.
259
233
 
260
234
  ```ts
261
- await server.client.createExpectation({
235
+ await server.client.createExpectation(({ $ }) => ({
262
236
  schema: {
263
- request: {
264
- $has: {
265
- $location: 'path',
266
- $regExp: /^\/foo/,
267
- },
268
- },
237
+ request: $.has('incoming.path', {
238
+ // Custom matching logic: checks if path starts with '/api/data'
239
+ $exec: (path) => path.startsWith('/api/data')
240
+ }),
241
+ response: $.set('outgoing.data', {
242
+ // Use built-in utils like lodash (_), dayjs (d), or faker
243
+ $exec: (payload, { _, d, faker }) => ({
244
+ id: faker.string.uuid(),
245
+ timestamp: d().format(),
246
+ values: _.range(3).map(() => _.random(1, 100)),
247
+ }),
248
+ }),
269
249
  },
270
- });
271
- ```
272
-
273
- **Example using cURL**
250
+ }));
274
251
 
275
- ```bash
276
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
277
- {
278
- "schema": {
279
- "request": {
280
- "\$has": {
281
- "\$location": "method",
282
- "\$regExp": { "source": "^\/foo" }
252
+ // Use $.exec for full control over the expectation execution
253
+ await server.client.createExpectation(({ $ }) => ({
254
+ schema: {
255
+ request: $.exec(({ context, logger, mode }) => {
256
+ // mode can be 'match' (on catching request) or 'manipulate' (on manipulation)
257
+ if (mode === 'match') {
258
+ logger.info('Checking request path', context.incoming.path);
259
+ return context.incoming.path === '/api/custom';
283
260
  }
284
- }
285
- }
286
- }
287
- EOF
261
+ }),
262
+ response: $.set('outgoing.data', { $value: { status: 'custom' } }),
263
+ },
264
+ }));
288
265
  ```
289
266
 
290
- ### $set
267
+ ### Dynamic Response Manipulation
291
268
 
292
- > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
293
-
294
- | Property | Type (application) | Type (cURL) | Optional | Description |
295
- |--|--|--|--|--|
296
- | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
297
- | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
298
- | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
299
- | $value | `any` | `any` | * | Sets value to [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
300
- | $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) |
301
-
302
- **Example using application**
269
+ You can dynamically adjust the response payload based on incoming request parameters (query, body, headers, etc.).
303
270
 
304
271
  ```ts
305
- await server.client.createExpectation({
272
+ await server.client.createExpectation(({ $ }) => ({
306
273
  schema: {
307
- request: {
308
- $set: {
309
- $location: 'incoming.data',
310
- $path: 'foo',
311
- $exec: (payload, { _ }) => _.clamp(payload, 0, 10),
312
- },
313
- },
274
+ request: $.has('incoming.path', { $value: '/api/profile' }),
275
+ response: $.set('outgoing.data', {
276
+ $exec: (payload, { context }) => ({
277
+ id: context.incoming.query.userId,
278
+ // Conditionally include data based on request headers
279
+ debug: context.incoming.headers['x-debug'] === 'true'
280
+ ? { timestamp: Date.now() }
281
+ : undefined
282
+ })
283
+ }),
314
284
  },
315
- });
316
- ```
317
-
318
- **Example using cURL**
319
-
320
- ```bash
321
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
322
- {
323
- "schema": {
324
- "request": {
325
- "\$set": {
326
- "\$location": "incoming.data",
327
- "\$path": "foo",
328
- "\$exec": "_.clamp(payload, 0, 10)"
329
- }
330
- }
331
- }
332
- }
333
- EOF
285
+ }));
334
286
  ```
335
287
 
336
- ### $merge
288
+ ### Binary Data and Files
337
289
 
338
- > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
339
-
340
- | Property | Type (application) | Type (cURL) | Optional | Description |
341
- |--|--|--|--|--|
342
- | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
343
- | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
344
- | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
345
- | $value | `object` | `object` | * | Merges value in [context](#context) using `$location` (and `$path`, `$jsonPath` if it was specified) |
346
- | $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) |
347
-
348
- **Example using application**
290
+ You can return binary data (Buffers) as a response body. This is useful for mocking file downloads or image responses.
349
291
 
350
292
  ```ts
351
- await server.client.createExpectation({
293
+ import { readFile } from 'fs/promises';
294
+
295
+ await server.client.createExpectation(({ $ }) => ({
352
296
  schema: {
353
- request: {
354
- $merge: {
355
- $location: 'incoming.data',
356
- $value: { has_mocked: true },
357
- },
358
- },
297
+ request: $.has('incoming.path', { $value: '/api/download' }),
298
+ response: $.and([
299
+ $.set('outgoing.headers', '$path', 'content-type', { $value: 'application/pdf' }),
300
+ $.set('outgoing.dataRaw', {
301
+ $exec: async () => await readFile('./path/to/document.pdf')
302
+ }),
303
+ ]),
359
304
  },
360
- });
361
- ```
362
-
363
- **Example using cURL**
364
-
365
- ```bash
366
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
367
- {
368
- "schema": {
369
- "request": {
370
- "\$merge": {
371
- "\$location": "incoming.data",
372
- "\$value": {"has_mocked": true}
373
- }
374
- }
375
- }
376
- }
377
- EOF
305
+ }));
378
306
  ```
379
307
 
380
- ### $remove
308
+ ### XML Support
381
309
 
382
- | Property | Type (application) | Type (cURL) | Optional | Description |
383
- |--|--|--|--|--|
384
- | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
385
- | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
386
- | $jsonPath | `string` | `string` | * | Specifies a path to payload using [JSON path](https://www.npmjs.com/package/jsonpath-plus) |
387
-
388
- **Example using application**
310
+ The mock server provides native support for XML payloads via [fast-xml-parser](https://www.npmjs.com/package/fast-xml-parser) with `ignoreAttributes: false`. It automatically parses incoming XML and can serialize objects back to XML in the response.
389
311
 
390
312
  ```ts
391
- await server.client.createExpectation({
313
+ await server.client.createExpectation(({ $ }) => ({
392
314
  schema: {
393
- request: {
394
- $remove: { $location: 'outgoing.data' },
395
- },
315
+ request: $.and([
316
+ $.has('incoming.path', { $matchAnyOf: ['/api/user*'] }),
317
+ $.exec(({ context }) => context.incoming.headers['content-type'] === 'application/xml'),
318
+ ]),
319
+ response: $.and([
320
+ $.set('outgoing.headers', '$path', 'content-type', { $value: 'application/xml' }),
321
+ $.set('outgoing.data', {
322
+ $value: {
323
+ user: {
324
+ info: {
325
+ '#text': 'John Doe',
326
+ '@_type': 'mocked',
327
+ '@_id': 123
328
+ }
329
+ },
330
+ },
331
+ }),
332
+ ]),
396
333
  },
397
- });
398
- ```
399
-
400
- **Example using cURL**
401
-
402
- ```bash
403
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
404
- {
405
- "schema": {
406
- "request": {
407
- "\$remove": {"\$location": "outgoing.data"}
408
- }
409
- }
410
- }
411
- EOF
334
+ }));
412
335
  ```
413
336
 
414
- ### $exec
337
+ ### Simulating Network Errors
415
338
 
416
- > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
417
-
418
- | Type (application) | Type (cURL) | Description |
419
- |--|--|--|
420
- | `(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) |
421
-
422
- **Example using application**
339
+ You can simulate connection drops or network errors by setting the `error` property.
423
340
 
424
341
  ```ts
425
- await server.client.createExpectation({
342
+ await server.client.createExpectation(({ $ }) => ({
426
343
  schema: {
427
- request: {
428
- $exec: ({ context, logger }) => {
429
- logger.info(context);
430
- return context.incoming.path === '/foo';
431
- },
432
- },
344
+ request: $.has('incoming.path', { $value: '/api/error' }),
345
+ response: $.set('error', {
346
+ // Common error codes: ECONNRESET, ECONNABORTED, etc.
347
+ $value: 'ECONNRESET'
348
+ }),
433
349
  },
434
- });
435
- ```
436
-
437
- **Example using cURL**
438
-
439
- ```bash
440
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
441
- {
442
- "schema": {
443
- "request": {
444
- "\$exec": "{ logger.info(context); return context.incoming.path === '/foo' }"
445
- }
446
- }
447
- }
448
- EOF
350
+ }));
449
351
  ```
450
352
 
451
- ### $and
452
-
453
- | Type (application) | Type (cURL) | Description |
454
- |--|--|--|
455
- | `object[]` | `object[]` | Provides [operators](#operators) schemas |
353
+ ### Disabled by Default
456
354
 
457
- **Example using application**
355
+ You can create expectations that are disabled by default by setting `isEnabled: false`. These can be manually enabled later via the [GUI](#gui).
458
356
 
459
357
  ```ts
460
- await server.client.createExpectation({
358
+ await server.client.createExpectation(({ $ }) => ({
359
+ isEnabled: false,
461
360
  schema: {
462
- request: {
463
- $and: [
464
- { $has: { $location: 'path', $match: 'foo/*' } },
465
- { $has: { $location: 'method', $valueAnyOf: ['GET', 'POST'] } },
466
- ],
467
- },
361
+ request: $.has('incoming.path', { $value: '/api/hidden' }),
362
+ response: $.set('outgoing.data', { $value: { message: 'Activate me in GUI' } }),
468
363
  },
469
- });
470
- ```
471
-
472
- **Example using cURL**
473
-
474
- ```bash
475
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
476
- {
477
- "schema": {
478
- "request": {
479
- "\$and": [
480
- {"\$has": {"\$location": "path", "\$match": "foo/*"}},
481
- {"\$has": {"\$location": "method", "\$valueAnyOf": ["GET", "POST"]}}
482
- ]
483
- }
484
- }
485
- }
486
- EOF
364
+ }));
487
365
  ```
488
366
 
489
- ### $or
490
-
491
- | Type (application) | Type (cURL) | Description |
492
- |--|--|--|
493
- | `object[]` | `object[]` | Provides [operators](#operators) schemas |
367
+ ### Type Safety
494
368
 
495
- **Example using application**
369
+ You can provide generic types to `createExpectation` to ensure your schemas and `$exec` functions are fully typed.
496
370
 
497
371
  ```ts
498
- await server.client.createExpectation({
499
- schema: {
500
- request: {
501
- $or: [
502
- { $has: { $location: 'path', $match: 'foo/*' } },
503
- { $has: { $location: 'method', $valueAnyOf: ['GET', 'POST'] } },
504
- ],
505
- },
506
- },
507
- });
508
- ```
509
-
510
- **Example using cURL**
511
-
512
- ```bash
513
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
514
- {
515
- "schema": {
516
- "request": {
517
- "\$or": [
518
- {"\$has": {"\$location": "path", "\$match": "foo/*"}},
519
- {"\$has": {"\$location": "method", "\$valueAnyOf": ["GET", "POST"]}}
520
- ]
521
- }
522
- }
372
+ interface MyContext {
373
+ incoming: {
374
+ query: {
375
+ userId: string;
376
+ expand?: boolean;
377
+ };
378
+ };
379
+ outgoing: {
380
+ data: {
381
+ id: string;
382
+ name: string;
383
+ };
384
+ };
523
385
  }
524
- EOF
525
- ```
526
-
527
- ### $not
528
386
 
529
- | Type (application) | Type (cURL) | Description |
530
- |--|--|--|
531
- | `object` | `object` | Provides an [operators](#operators) schema |
532
-
533
- **Example using application**
534
-
535
- ```ts
536
- await server.client.createExpectation({
387
+ await server.client.createExpectation<MyContext>(({ $ }) => ({
537
388
  schema: {
538
- request: {
539
- $not: { $has: { $location: 'path', $match: 'foo/*' } },
540
- },
389
+ request: $.has('incoming.query', {
390
+ $exec: (query) => query.userId === '123'
391
+ }),
392
+ response: $.set('outgoing.data', {
393
+ $exec: (payload, { faker }) => ({
394
+ id: '123',
395
+ name: faker.person.fullName()
396
+ })
397
+ }),
541
398
  },
542
- });
543
- ```
544
-
545
- **Example using cURL**
546
-
547
- ```bash
548
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
549
- {
550
- "schema": {
551
- "request": {
552
- "\$not": {"\$has": {"\$location": "path", "\$match": "foo/*"}}
553
- }
554
- }
555
- }
556
- EOF
399
+ }));
557
400
  ```
558
401
 
559
- ### $if
402
+ ### WebSocket Support
560
403
 
561
- | Property | Type (application) | Type (cURL) | Optional | Description |
562
- |--|--|--|--|--|
563
- | $condition | `object` | `object` | | Condition to check. Should contain one of `$and`, `$exec`, `$has`, `$or` or `$not` [operators](#operators) schema |
564
- | $then | `object` | `object` | * | Logical `then`. Should contain an [operators](#operators) schema |
565
- | $else | `object` | `object` | * | Logical `else`. Should contain an [operators](#operators) schema |
566
-
567
- **Example using application**
404
+ The mock server provides first-class support for WebSockets, allowing you to intercept and mock streaming data using [RxJS](https://rxjs.dev/).
568
405
 
569
406
  ```ts
570
- await server.client.createExpectation({
407
+ await server.client.createExpectation(({ $, utils }) => ({
408
+ // 1. Specify 'ws' transport
409
+ transports: utils.transports(['ws']),
410
+
571
411
  schema: {
572
- request: {
573
- $if: {
574
- $condition: { $has: { $location: 'path', $match: 'foo/*' } },
575
- $then: { $set: { $location: 'delay', $value: 5000 } },
576
- $else: { $set: { $location: 'error', $value: 'ECONNABORTED' } },
577
- },
578
- },
412
+ request: $.has('incoming.path', { $value: '/ws/updates' }),
413
+ response: $.and([
414
+ // 2. Set custom WS status code if needed
415
+ $.set('outgoing.status', { $value: 1000 }),
416
+ // 3. Define the outgoing stream
417
+ $.set('outgoing.stream', {
418
+ $exec: (stream, { rx }) => {
419
+ // Return an Observable of messages
420
+ return rx.from([
421
+ { event: 'connected' },
422
+ { event: 'data', value: 1 },
423
+ { event: 'data', value: 2 },
424
+ ]);
425
+ },
426
+ }),
427
+ ]),
579
428
  },
580
- });
581
- ```
582
-
583
- **Example using cURL**
584
-
585
- ```bash
586
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
587
- {
588
- "schema": {
589
- "request": {
590
- "\$if": {
591
- "\$condition": {"\$has": {"\$location": "path", "\$match": "foo/*"}},
592
- "\$then": {"\$set": {"\$location": "delay", "\$value": 5000}},
593
- "\$else": {"\$set": {"\$location": "error", "\$value": "ECONNABORTED"}}
594
- }
595
- }
596
- }
597
- }
598
- EOF
429
+ }));
599
430
  ```
600
431
 
601
- ### $switch
602
-
603
- > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
604
-
605
- | Property | Type (application) | Type (cURL) | Optional | Description |
606
- |--|--|--|--|--|
607
- | $location | `string` [enum](#context) | `string` [enum](#context) | | Location that describes what [context](#context) entity is selecting for operator to work with |
608
- | $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 |
609
- | $default | `object` | `object` | * | Default behavior as an [operators](#operators) schema |
610
- | $path | `string` | `string` | * | Specifies a path to payload using [lodash get](https://lodash.com/docs/4.17.15#get) |
611
- | $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) |
432
+ ### Request Forwarding
612
433
 
613
- **Example using application**
434
+ You can forward requests to an external API and manipulate the response before it's returned to the client.
614
435
 
615
- ```ts
616
- await server.client.createExpectation({
436
+ ```ts
437
+ await server.client.createExpectation(({ $ }) => ({
617
438
  schema: {
618
- request: {
619
- $switch: {
620
- $location: 'method',
621
- $cases: {
622
- 'GET': { $set: { $location: 'delay', $value: 2000 } },
623
- 'POST': { $set: { $location: 'delay', $value: 5000 } },
624
- },
625
- $default: {
626
- $set: { $location: 'error', $value: 'ECONNABORTED' }
627
- },
628
- },
439
+ request: $.has('incoming.path', { $value: '/api/proxy' }),
440
+ // Forward the request to an external service
441
+ forward: {
442
+ url: 'https://api.external-service.com/data',
629
443
  },
444
+ // Manipulate the response from the external service
445
+ response: $.set('outgoing.data', {
446
+ $exec: (data) => ({
447
+ ...data,
448
+ source: 'mock-server',
449
+ timestamp: new Date().toISOString(),
450
+ }),
451
+ }),
630
452
  },
631
- });
632
- ```
633
-
634
- **Example using cURL**
635
-
636
- ```bash
637
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
638
- {
639
- "schema": {
640
- "request": {
641
- "\$switch": {
642
- "\$location": "method",
643
- "\$cases": {
644
- "GET": {"\$set": {"\$location": "delay", "\$value": 2000}},
645
- "POST": {"\$set": {"\$location": "delay", "\$value": 5000}}
646
- },
647
- "\$default": {
648
- "\$set": {"\$location": "error", "\$value": "ECONNABORTED"}
649
- }
650
- }
651
- }
652
- }
653
- }
654
- EOF
453
+ }));
655
454
  ```
656
455
 
657
- ## Typings
658
-
659
- 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)
456
+ ### Using Relative Paths with `baseUrl`
660
457
 
661
- 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
662
-
663
- **Examples**
458
+ When using `baseUrl`, the original request path is appended to the target URL.
664
459
 
665
460
  ```ts
666
- await client.createExpectation<{
667
- incoming: {
668
- query: {
669
- foo: 'a' | 'b' | 'c';
670
- bar?: string;
671
- };
672
- };
673
- }>(({ $ }) => ({
461
+ await server.client.createExpectation(({ $ }) => ({
674
462
  schema: {
675
- request: $.or([
676
- $.has('incoming.query', '$path', 'foo', { $value: 'a' }),
677
- $.has('incoming.query', { $match: { foo: 'b' } }),
463
+ request: $.and([
464
+ $.has('incoming.path', { $value: '/api/v1/*' }),
465
+ // Rewrite the path from v1 to v2 before forwarding
466
+ $.set('incoming.path', {
467
+ $exec: (path) => path.replace('/v1/', '/v2/'),
468
+ }),
678
469
  ]),
470
+ forward: {
471
+ // If request is '/api/v1/users', it forwards to 'https://legacy-api.com/api/v2/users'
472
+ baseUrl: 'https://legacy-api.com',
473
+ },
474
+ response: $.set('outgoing.headers', '$path', 'x-proxied-by', { $value: 'mock-server' }),
679
475
  },
680
476
  }));
681
477
  ```
682
478
 
479
+ ### Transforming Request Data Before Forwarding
480
+
481
+ You can also modify the request body or headers before they are sent to the target API.
482
+
683
483
  ```ts
684
- await client.createExpectation<{
484
+ interface SearchRequest {
685
485
  incoming: {
686
- query: {
687
- foo: 'a' | 'b' | 'c';
688
- bar?: string;
689
- };
690
- };
691
- outgoing: {
692
486
  data: {
693
- foo: 'a' | 'b' | 'c';
694
- bar?: {
695
- baz: 'a' | 'b' | 'c';
696
- };
487
+ query: string;
488
+ limit?: number;
697
489
  };
698
490
  };
699
- }>(({ $ }) => ({
700
- schema: {
701
- response: $.and([
702
- $.switch('incoming.query', '$exec', (payload) => payload.foo, {
703
- $cases: {
704
- 'a': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'a' }),
705
- 'b': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'b' }),
706
- },
707
- }),
491
+ }
708
492
 
709
- $.switch('incoming.query', '$path', 'bar', {
710
- $cases: {
711
- 'something': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'c' }),
712
- },
493
+ await server.client.createExpectation<SearchRequest>(({ $ }) => ({
494
+ schema: {
495
+ request: $.and([
496
+ $.has('incoming.path', { $value: '/api/search' }),
497
+ // Inject API key and transform data before forwarding
498
+ $.merge('incoming.headers', { $value: { 'X-API-Key': 'secret-token' } }),
499
+ $.set('incoming.data', {
500
+ $exec: (data) => ({
501
+ ...data,
502
+ limit: data.limit ?? 20,
503
+ q: data.query, // Rename 'query' to 'q' for the external API
504
+ })
713
505
  }),
714
506
  ]),
507
+ forward: {
508
+ url: 'https://external-search-service.com/v1/query',
509
+ },
715
510
  },
716
511
  }));
717
512
  ```
718
513
 
719
- ## Storage
720
-
721
- Storage is a temporary storage that provides an access to read/write [containers](#containers)
722
-
723
- | Property | Type | Description |
724
- |--|--|--|
725
- | 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 |
726
- | 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 |
727
- | register | `(configuration: Container) => Container` | Registers a container in storage (overrides if existent) |
728
- | provide | `(configuration: Container) => Container` | Finds or registers a container in storage |
729
-
730
- 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)
731
-
732
- > **!NOTE** See example of usage in [containers](#containers) section below
733
-
734
- ## Containers
735
-
736
- | Property | Type | Description |
737
- |--|--|--|
738
- | key | `string` | A key of container |
739
- | prefix | `string` | A prefix of container |
740
- | payload | `object` | An object with custom data |
741
- | ttl | `number` | Time to live of container in seconds **(default: 1h)** |
742
- | expiresAt | `number` | An expiration date/time as unix timestamp with milliseconds |
743
- | 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 |
744
- | 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 |
745
- | assign | `(payload: object ∣ (payload: object) => object) => Container` | Uses as payload predicate to assign payload values to existent |
746
- | merge | `(payload: object ∣ (payload: object) => object) => Container` | Uses as payload predicate to deep merge of payload values with existent |
514
+ ### WebSocket Forwarding and Manipulation
747
515
 
748
-
749
- **Example**
516
+ You can forward WebSocket connections to a real server and manipulate the message stream on the fly.
750
517
 
751
518
  ```ts
752
- await client.createExpectation<{
753
- container: {
754
- counter: number;
755
- };
756
- }>(({ $ }) => ({
757
- schema: {
758
- request: $.set('container', {
759
- $exec: (container, { context }) => context.storage
760
- .provide({ key: 'foo', payload: { counter: 0 } })
761
- .assign((payload) => ({ counter: payload.counter + 1 }))
762
- }),
519
+ await server.client.createExpectation(({ $, utils }) => ({
520
+ transports: utils.transports(['ws']),
763
521
 
764
- response: $.set('outgoing.data', {
765
- $exec: (payload, { context }) => ({
766
- count: context.container!.payload.counter,
767
- }),
522
+ schema: {
523
+ request: $.has('incoming.path', { $value: '/ws/proxy' }),
524
+ forward: {
525
+ baseUrl: 'wss://real-server.com',
526
+ },
527
+ response: $.set('outgoing.stream', {
528
+ $exec: (stream, { rx }) => {
529
+ // 'stream' is the Observable from the real server
530
+ return stream?.pipe(
531
+ // Inject extra message at the beginning
532
+ rx.startWith({ event: 'proxy-init', timestamp: Date.now() }),
533
+ // Transform real messages
534
+ rx.map((message) => ({ ...message, intercepted: true }))
535
+ );
536
+ },
768
537
  }),
769
538
  },
770
539
  }));
771
540
  ```
772
541
 
773
- ## Cache
774
-
775
- > **!NOTE** Cache is usable **only** to store a payload of forwarded requests
542
+ ### Conditional Caching
776
543
 
777
- To work with cache the mock server uses [ioredis](https://www.npmjs.com/package/ioredis) package
778
-
779
- To configure it use `database.redis` configuration on the mock server start options
780
-
781
- **Example**
544
+ You can control when the mock server should cache a forwarded response. For example, only cache responses with a `200 OK` status.
782
545
 
783
546
  ```ts
784
- const server = await MockServer.start({
785
- host: 'localhost',
786
- port: 8080,
787
-
788
- databases: {
789
- redis: {
790
- host: 'localhost',
791
- port: 6379,
547
+ await server.client.createExpectation(({ $ }) => ({
548
+ schema: {
549
+ request: $.has('incoming.path', { $value: '/api/cacheable' }),
550
+ forward: {
551
+ baseUrl: 'https://api.service.com',
552
+ cache: {
553
+ ttl: 3600, // 1 hour
554
+ },
792
555
  },
556
+ response: $.set('cache', '$path', 'isEnabled', {
557
+ $exec: (value, { context }) => context.outgoing.status === 200,
558
+ }),
793
559
  },
794
- });
560
+ }));
795
561
  ```
796
562
 
797
- **How it works in steps?**
563
+ ### Custom Cache Key
798
564
 
799
- 0. [Expectation schema](#schema) should have `forward` configuration specified
800
- 1. Preparing incoming request...
801
- 2. Preparing [request schema](#schema) in expectation...
802
- 3. Setting up cache configuration from [context](#context) or [forward.cache](#forwarding)...
803
- 4. If `cache.isEnabled` is equals `true` the mock server checks a cache using provided configuration
804
- 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
805
- 6. If cache was found then step `7` is skipping
806
- 7. Forwarding a request....
807
- 8. Preparing [response schema](#schema) in expectation...
808
- 9. Setting up cache configuration from [context](#context)...
809
- 10. If `cache.isEnabled` is equals `true` the mock server will write a cache over provided `ttl`
810
- 11. Replying...
811
-
812
- **Example**
565
+ By default, the cache key is a hash generated from the `incoming` object (path, method, data, query). You can override this with a custom key.
813
566
 
814
567
  ```ts
815
- await client.createExpectation(({ $ }) => ({
568
+ await server.client.createExpectation(({ $ }) => ({
816
569
  schema: {
817
- response: $.set('cache', '$path', 'isEnabled', {
818
- $exec: (payload, { context }) => context.outgoing.status < 400,
819
- }),
820
-
570
+ request: $.and([
571
+ $.has('incoming.path', { $value: '/api/cache-custom' }),
572
+ $.set('cache', '$path', 'key', {
573
+ // Use a specific header as the cache key
574
+ $exec: (key, { context }) => context.incoming.headers['x-user-id'],
575
+ }),
576
+ ]),
821
577
  forward: {
822
- baseUrl: 'https://example.com',
823
-
578
+ baseUrl: 'https://api.service.com',
824
579
  cache: {
825
- ttl: 30 * 24 * 60 * 60,
580
+ isEnabled: true,
581
+ ttl: 600,
826
582
  },
827
583
  },
828
584
  },
829
585
  }));
830
- ```
831
-
832
- ## State
833
-
834
- State is a unique storage of each request. It can be used to handle complex expectations
835
586
 
836
- 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
837
-
838
- **Example**
839
-
840
- ```ts
841
- await client.createExpectation<{
842
- state: {
843
- id?: number;
844
- };
845
- incoming: {
846
- query: {
847
- foo: 'a' | 'b' | 'c';
848
- };
849
- };
850
- outgoing: {
851
- data: {
852
- id: number;
853
- };
854
- };
855
- }>(({ $ }) => ({
587
+ // You can also provide an object as a key; it will be automatically hashed
588
+ await server.client.createExpectation(({ $ }) => ({
856
589
  schema: {
857
590
  request: $.and([
858
- $.switch('incoming.query', '$exec', (payload) => payload.foo, {
859
- $cases: {
860
- 'a': $.set('state', '$path', 'id', { $value: 1 }),
861
- 'b': $.set('state', '$path', 'id', { $value: 2 }),
862
- },
591
+ $.has('incoming.path', { $value: '/api/cache-object' }),
592
+ $.set('cache', '$path', 'key', {
593
+ // Cache depends on specific properties of the incoming data
594
+ $exec: (key, { context }) => ({
595
+ id: context.incoming.data.id,
596
+ type: context.incoming.data.type,
597
+ }),
863
598
  }),
864
599
  ]),
865
- response: $.set('outgoing.data', {
866
- $exec: (payload, { state }) => ({ id: state.id ?? 0 }),
867
- }),
600
+ forward: {
601
+ baseUrl: 'https://api.service.com',
602
+ cache: { isEnabled: true },
603
+ },
868
604
  },
869
605
  }));
870
606
  ```
871
607
 
872
- ## Seeds
608
+ ### Request State
873
609
 
874
- Seeds can help to generate content with the same values each request using [faker](https://www.npmjs.com/package/@faker-js/faker)
875
-
876
- By default a number of seed takes from `X-Use-Mock-Seed` in `incoming.headers`
877
-
878
- **Example**
610
+ State is a unique storage for each request. It can be used to pass data between operators or expectations. By default, it's extracted from the `X-Use-Mock-State` header (JSON in Base64).
879
611
 
880
612
  ```ts
881
- await client.createExpectation(({ $ }) => ({
613
+ interface MyState {
614
+ state: {
615
+ internalId: number;
616
+ };
617
+ }
618
+
619
+ await server.client.createExpectation<MyState>(({ $ }) => ({
882
620
  schema: {
883
621
  request: $.and([
884
- $.set('seed', { $exec: (seed) => seed ?? 123 }),
622
+ $.has('incoming.path', { $value: '/api/stateful' }),
623
+ $.set('state', {
624
+ // Compute and store data in state for later use
625
+ $exec: (state) => ({ internalId: state.internalId ?? Math.random() }),
626
+ }),
885
627
  ]),
886
628
  response: $.set('outgoing.data', {
887
- $exec: (payload, { faker }) => ({
888
- id: faker.number.int({ max: 1000, min: 500 }),
889
- first_name: faker.person.firstName('male'),
890
- last_name: faker.person.lastName('male'),
629
+ // Use the stored state in response building
630
+ $exec: (payload, { state }) => ({
631
+ id: state.internalId,
891
632
  }),
892
633
  }),
893
634
  },
894
635
  }));
895
636
  ```
896
637
 
897
- ## XML
638
+ ### Storage and Containers
898
639
 
899
- 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:
640
+ Storage provides access to **Containers**—temporary cells used to store data between requests or sync multiple expectations. Unlike `state`, which is request-scoped, Containers are persistent until they expire (TTL).
900
641
 
901
642
  ```ts
902
- {
903
- ignoreAttributes: false,
904
- }
905
- ```
906
-
907
- To define a `incoming.data` as XML in incoming request `incoming.headers` should have `Content-Type: application/xml`.
908
-
909
- The same with `outgoing.data` and `outgoing.headers`
910
-
911
- **Example of serialized XML**
912
-
913
- ```xml
914
- <tag type="default">
915
- <nested type="nested">456</nested>
916
- 123
917
- </tag>
918
- ```
919
-
920
- **Example of parsed XML**
921
-
922
- ```json
923
- {
924
- "tag":{
925
- "nested":{
926
- "#text":456,
927
- "@_type":"nested"
928
- },
929
- "#text":123,
930
- "@_type":"default"
931
- }
643
+ interface CounterContainer {
644
+ container: {
645
+ count: number;
646
+ };
932
647
  }
933
- ```
934
-
935
- To parse an XML manually the application lib provides utils:
936
-
937
- ```ts
938
- import { parsePayload, serializePayload } from '@n1k1t/mock-server';
939
-
940
- const parsed = parsePayload('xml', '<tag>123</tag>'); // { tag: 123 }
941
- const serialized = serializePayload('xml', parsed); // '<tag>123</tag>'
942
- ```
943
-
944
- # API
945
-
946
- 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)
947
-
948
- The `HTTP API` and `RemoteClient` have some usage restrictions like:
949
- - 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
950
- - Plugins are not supported
951
-
952
- ## Ping
953
-
954
- `INPUT` → `GET /_system/ping`
955
-
956
- `OUTPUT`
957
-
958
- | Type | Description |
959
- |--|--|
960
- | `string` | A `pong` message |
961
648
 
962
- **Using cURL**
963
-
964
- ```bash
965
- curl -H "Content-type: application/json" --location "localhost:8080/_system/ping"
649
+ await server.client.createExpectation<CounterContainer>(({ $ }) => ({
650
+ schema: {
651
+ request: $.set('container', {
652
+ $exec: (container, { context }) => context.storage
653
+ // provide() finds an existing container by key or creates a new one with the given payload
654
+ .provide({ key: 'my-counter', payload: { count: 0 } })
655
+ .assign((payload) => ({ count: payload.count + 1 })),
656
+ }),
657
+ response: $.set('outgoing.data', {
658
+ $exec: (payload, { context }) => ({
659
+ // Access the incremented value from the container
660
+ currentCount: context.container!.payload.count,
661
+ }),
662
+ }),
663
+ },
664
+ }));
966
665
  ```
967
666
 
968
- **Using application lib on server side**
969
-
970
- ```ts
971
- import { MockServer } from '@n1k1t/mock-server';
972
-
973
- const server = await MockServer.start({ host: 'localhost', port: 8080 });
974
- await server.client.ping();
975
- ```
667
+ ### Cross-Expectation Sync
976
668
 
977
- **Using application lib on remotely**
669
+ You can use Containers to synchronize data between different endpoints. For example, one endpoint increments a counter, and another retrieves its current value.
978
670
 
979
671
  ```ts
980
- import { RemoteClient } from '@n1k1t/mock-server';
981
-
982
- const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
983
- await client.ping();
984
- ```
985
-
986
- ## Create expectation
987
-
988
- `INPUT` → `POST /_system/expectations`
989
-
990
- | Property | Nested | Type | Optional | Description |
991
- |--|--|--|--|--|
992
- | schema | [Schema](#schema) | `object` | | An expectation schema |
993
- | name | | `string` | * | A preferred name for an expectation |
994
-
995
- `OUTPUT`
996
-
997
- | Property | Nested | Type | Optional | Description |
998
- |--|--|--|--|--|
999
- | id | | `string` | | An expectation ID |
1000
- | name | | `string` | | An expectation name |
1001
- | schema | [Schema](#schema) | `object` | | Provided schema |
1002
-
1003
- **Using cURL**
1004
-
1005
- ```bash
1006
- curl -H "Content-type: application/json" -X POST --location "localhost:8080/_system/expectations" --data-binary @- << EOF
1007
- {
1008
- "schema": {
1009
- "request": {
1010
- "\$has": {
1011
- "\$location": "method",
1012
- "\$value": "GET"
1013
- }
1014
- }
1015
- }
672
+ interface SyncContainer {
673
+ container: {
674
+ count: number;
675
+ };
1016
676
  }
1017
- EOF
1018
- ```
1019
677
 
1020
- **Using application lib on server side**
1021
-
1022
- ```ts
1023
- import { MockServer } from '@n1k1t/mock-server';
1024
-
1025
- const server = await MockServer.start({ host: 'localhost', port: 8080 });
1026
- const expectation = await server.client.createExpectation({
678
+ // Expectation 1: Increments the counter
679
+ await server.client.createExpectation<SyncContainer>(({ $ }) => ({
680
+ name: 'Incrementer',
1027
681
  schema: {
1028
- request: {
1029
- $has: {
1030
- $location: 'method',
1031
- $value: 'GET',
1032
- },
1033
- },
682
+ request: $.and([
683
+ $.has('incoming.path', { $value: '/api/increment' }),
684
+ $.set('container', {
685
+ $exec: (container, { context }) => context.storage
686
+ .provide({ key: 'shared-counter', payload: { count: 0 } })
687
+ .assign((payload) => ({ count: payload.count + 1 })),
688
+ }),
689
+ ]),
690
+ response: $.set('outgoing.data', { $value: { status: 'incremented' } }),
1034
691
  },
1035
- });
1036
-
1037
- console.log('Mock expectation has created', expectation.id);
1038
- ```
1039
-
1040
- **Using application lib on remotely**
1041
-
1042
- ```ts
1043
- import { RemoteClient } from '@n1k1t/mock-server';
692
+ }));
1044
693
 
1045
- const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
1046
- const expectation = await client.createExpectation({
694
+ // Expectation 2: Retrieves the current value
695
+ await server.client.createExpectation<SyncContainer>(({ $ }) => ({
696
+ name: 'Getter',
1047
697
  schema: {
1048
- request: {
1049
- $has: {
1050
- $location: 'method',
1051
- $value: 'GET',
1052
- },
1053
- },
698
+ request: $.and([
699
+ $.has('incoming.path', { $value: '/api/count' }),
700
+ $.set('container', {
701
+ $exec: (container, { context }) => context.storage
702
+ .provide({ key: 'shared-counter', payload: { count: 0 } }),
703
+ }),
704
+ ]),
705
+ response: $.set('outgoing.data', {
706
+ $exec: (payload, { context }) => ({
707
+ total: context.container!.payload.count,
708
+ }),
709
+ }),
1054
710
  },
1055
- });
1056
-
1057
- console.log('Mock expectation has created', expectation.id);
711
+ }));
1058
712
  ```
1059
713
 
1060
- ## Update expectation
1061
-
1062
- `INPUT` → `PUT /_system/expectations`
1063
-
1064
- | Property | Nested | Type | Optional | Description |
1065
- |--|--|--|--|--|
1066
- | id | | `string` | | ID of a registered expectation |
1067
- | set | | `object` | | A payload to set |
1068
- | | name | `string` | * | A preferred name for an expectation |
1069
- | | schema | [Schema](#schema) | * | An expectation schema |
714
+ ## Extensions
1070
715
 
1071
- `OUTPUT`
716
+ ### Redis Configuration
1072
717
 
1073
- | Property | Nested | Type | Optional | Description |
1074
- |--|--|--|--|--|
1075
- | id | | `string` | | An expectation ID |
1076
- | name | | `string` | | An expectation name |
1077
- | schema | [Schema](#schema) | `object` | | Provided schema |
1078
-
1079
- **Using cURL**
1080
-
1081
- ```bash
1082
- curl -H "Content-type: application/json" -X PUT --location "localhost:8080/_system/expectations" --data-binary @- << EOF
1083
- {
1084
- "id": "...",
1085
- "set": {"name": "The expectation"}
1086
- }
1087
- EOF
1088
- ```
1089
-
1090
- **Using application lib on server side**
718
+ The mock server uses Redis for caching forwarded responses and persistent storage. You can configure the connection details during server startup.
1091
719
 
1092
720
  ```ts
1093
721
  import { MockServer } from '@n1k1t/mock-server';
1094
722
 
1095
- const server = await MockServer.start({ host: 'localhost', port: 8080 });
1096
- const expectation = await server.client.updateExpectation({
1097
- id: '...',
1098
- set: { name: 'The expectation' }
723
+ const server = await MockServer.start({
724
+ host: 'localhost',
725
+ port: 8080,
726
+ databases: {
727
+ redis: {
728
+ host: 'localhost',
729
+ port: 6379,
730
+ // Optional: password, keyPrefix, etc.
731
+ keyPrefix: 'my-mock-server:',
732
+ },
733
+ },
1099
734
  });
1100
-
1101
- console.log('Mock expectation has updated', expectation);
1102
735
  ```
1103
736
 
1104
- **Using application lib on remotely**
737
+ ### Remote Expectations with `RemoteClient`
738
+
739
+ The `RemoteClient` allows you to manage expectations on a running mock server instance from a remote application or a separate test suite.
1105
740
 
1106
741
  ```ts
1107
742
  import { RemoteClient } from '@n1k1t/mock-server';
1108
743
 
1109
- const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
1110
- const expectation = await client.updateExpectation({
1111
- id: '...',
1112
- set: { name: 'The expectation' }
1113
- });
1114
-
1115
- console.log('Mock expectation has updated', expectation);
1116
- ```
1117
-
1118
- ## Delete expectation
744
+ // 1. Connect to a running mock server
745
+ const client = await RemoteClient.connect({ baseUrl: 'http://localhost:8080' });
1119
746
 
1120
- `INPUT` `DELETE /_system/expectations`
747
+ // 2. Create expectations remotely
748
+ await client.createExpectation(({ $, utils }) => ({
749
+ // Specify allowed transports (http, ws)
750
+ transports: utils.transports(['http']),
1121
751
 
1122
- | Property | Nested | Type | Optional | Description |
1123
- |--|--|--|--|--|
1124
- | ids | | `string[]` | * | An expectation IDs list to delete. Or **delete all expectations** if not provided |
1125
-
1126
- **Using cURL**
1127
-
1128
- ```bash
1129
- curl -H "Content-type: application/json" -X DELETE --location "localhost:8080/_system/expectations" --data-binary @- << EOF
1130
- {
1131
- "ids": ["..."]
1132
- }
1133
- EOF
752
+ schema: {
753
+ request: $.has('path', { $value: '/api/remote-mock' }),
754
+ response: $.set('outgoing.data', { $value: { success: true, source: 'remote-client' } }),
755
+ },
756
+ }));
1134
757
  ```
1135
758
 
1136
- **Using application lib on server side**
759
+ ### Grouping with Providers
1137
760
 
1138
- ```ts
1139
- import { MockServer } from '@n1k1t/mock-server';
1140
-
1141
- const server = await MockServer.start({ host: 'localhost', port: 8080 });
1142
- await server.client.deleteExpectations({
1143
- ids: ['...'],
1144
- });
1145
- ```
1146
-
1147
- **Using application lib on remotely**
761
+ Providers allow grouping expectations and isolating them from each other. They are also used to route requests to specific sets of expectations based on path patterns.
1148
762
 
1149
763
  ```ts
1150
- import { RemoteClient } from '@n1k1t/mock-server';
1151
-
1152
- const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
1153
- await client.deleteExpectations({
1154
- ids: ['...'],
1155
- });
1156
- ```
764
+ import { MockServer, Provider } from '@n1k1t/mock-server';
1157
765
 
1158
- # Additional
766
+ // 1. Define providers for different modules
767
+ const authProvider = Provider.build({ group: 'auth' });
768
+ const userProvider = Provider.build({ group: 'users' });
1159
769
 
1160
- ## Configuration
1161
-
1162
- > **!NOTE** Configuration must be provided in the same script like mock server
1163
-
1164
- ```ts
1165
- import { config } from '@n1k1t/mock-server';
770
+ // 2. Setup server
771
+ const server = await MockServer.start({ port: 13000 });
1166
772
 
1167
- config.merge({
1168
- logger: {
1169
- level: 'D', // Logger level (default: D)
773
+ // 3. Add expectations to specific providers
774
+ await authProvider.client.createExpectation(({ $ }) => ({
775
+ schema: {
776
+ request: $.has('path', { $value: '/api/auth/login' }),
777
+ response: $.set('outgoing.data', { $value: { token: 'secret-token' } }),
1170
778
  },
779
+ }));
1171
780
 
1172
- history: {
1173
- limit: 100, // Limit for history of requests (default: 100)
781
+ await userProvider.client.createExpectation(({ $ }) => ({
782
+ schema: {
783
+ request: $.has('path', { $value: '/api/users/me' }),
784
+ response: $.set('outgoing.data', { $value: { id: 1, name: 'John' } }),
1174
785
  },
786
+ }));
1175
787
 
1176
- containers: {
1177
- expiredCleaningInterval: 60 * 60, // Expired containers cleaning interval in seconds (default: 1h)
1178
- },
1179
- });
788
+ // 4. Setup routing
789
+ server.router
790
+ .register('/api/auth/**', { provider: authProvider })
791
+ .register('/api/users/**', { provider: userProvider });
1180
792
  ```
1181
793
 
1182
- ## Logger
794
+ ### Logger
1183
795
 
1184
- > **!NOTE** Configuration must be provided in the same script like mock server
796
+ You can customize how the mock server logs information by using serializers or by redirecting logs to an external logger (e.g., Sentry, Winston).
1185
797
 
1186
798
  ```ts
1187
799
  import { Logger } from '@n1k1t/mock-server';
1188
800
 
1189
- // It defines your own logger methods
1190
801
  Logger.useExternal({
1191
- debug: (...messages: string[]) => console.debug(...messages),
1192
- info: (...messages: string[]) => console.log(...messages),
1193
- warn: (...messages: string[]) => console.warn(...messages),
1194
- error: (...messages: string[]) => console.error(...messages),
1195
- fatal: (...messages: string[]) => console.error(...messages),
1196
- });
1197
-
1198
- // It defines a JSON serializers to mask some private data by keys on objects
1199
- Logger.useSerializers({
1200
- cvv: () => '***',
1201
- card: (payload: string) => payload.slice(0, 8) + 'xxxx',
802
+ info: (title, ...messages) => {
803
+ // Send to your logging service
804
+ console.log(`[${title}]`, ...messages);
805
+ },
806
+ error: (title, ...messages) => {
807
+ // Handle errors specifically
808
+ },
1202
809
  });
1203
810
  ```
1204
811
 
1205
- ## Meta
1206
-
1207
- 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`.
1208
-
1209
- The `metaStorage.provide()` returns an instance of `meta` that contains basic data like:
1210
-
1211
- | Property | Type | Optional | Description |
1212
- |--|--|--|--|
1213
- | operationId | `string` | | UUID v4 |
1214
- | requestId | `string` | * | `X-Request-Id` from `incoming.headers` |
1215
-
1216
- **Setup**
812
+ Serializers allow you to mask sensitive data (like passwords or credit card numbers) before they are logged.
1217
813
 
1218
814
  ```ts
1219
- import { Logger, metaStorage } from '@n1k1t/mock-server';
1220
-
1221
- // Some external logger with meta context support
1222
- const external = {...};
815
+ import { Logger } from '@n1k1t/mock-server';
1223
816
 
1224
- // It defines your own logger methods
1225
- Logger.useExternal({
1226
- debug: (...messages: string[]) => external.debug(metaStorage.provide(), ...messages),
1227
- info: (...messages: string[]) => external.log(metaStorage.provide(), ...messages),
1228
- warn: (...messages: string[]) => external.warn(metaStorage.provide(), ...messages),
1229
- error: (...messages: string[]) => external.error(metaStorage.provide(), ...messages),
1230
- fatal: (...messages: string[]) => external.error(metaStorage.provide(), ...messages),
817
+ Logger.useSerializers({
818
+ password: () => '***',
819
+ token: (val: string) => `${val.slice(0, 4)}...`,
1231
820
  });
1232
821
  ```
1233
822
 
1234
- **Usage**
823
+ ## License
1235
824
 
1236
- ```ts
1237
- await server.client.createExpectation({
1238
- schema: {
1239
- request: {
1240
- $exec: ({ context, logger }) => {
1241
- // Here logger should have a meta context like { operationId: '...' }
1242
- logger.info('Before')
1243
- },
1244
- $exec: ({ context, logger, meta }) => {
1245
- // It enriches meta context for further logs of request
1246
- meta.merge({ foo: 'bar' });
1247
- },
1248
- $exec: ({ context, logger, meta }) => {
1249
- // Now logger should have a meta context like { foo: 'bar', operationId: '...' }
1250
- logger.info('After')
1251
- },
1252
- },
1253
- },
1254
- });
1255
- ```
825
+ MIT