@n1k1t/mock-server 0.3.0 → 1.0.1

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 +590 -1006
  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,828 @@
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
+ - [Add Skills](#add-skills)
20
+ - [Simple Example](#simple-example)
21
+ - [Overview](#overview)
22
+ - [GUI](#gui)
23
+ - [Storage & Containers](#storage--containers)
24
+ - [Cache](#cache)
25
+ - [State & Seeds](#state--seeds)
26
+ - [Mock Server Utilities](#mock-server-utilities)
27
+ - [Expectation Operators ($)](#expectation-operators-)
28
+ - [Operator Locations](#operator-locations)
29
+ - [Operator Context](#operator-context)
30
+ - [Container Methods](#container-methods)
31
+ - [Usage](#usage)
32
+ - [Complex Expectations with $and](#complex-expectations-with-and)
33
+ - [Custom Logic with $exec](#custom-logic-with-exec)
34
+ - [Dynamic Response Manipulation](#dynamic-response-manipulation)
35
+ - [Binary Data and Files](#binary-data-and-files)
36
+ - [XML Support](#xml-support)
37
+ - [Simulating Network Errors](#simulating-network-errors)
38
+ - [Disabled By Default](#disabled-by-default)
39
+ - [Type Safety](#type-safety)
40
+ - [WebSocket Support](#websocket-support)
41
+ - [Request Forwarding](#request-forwarding)
42
+ - [Using Relative Paths with baseUrl](#using-relative-paths-with-baseurl)
43
+ - [Transforming Request Data Before Forwarding](#transforming-request-data-before-forwarding)
44
+ - [WebSocket Forwarding and Manipulation](#websocket-forwarding-and-manipulation)
45
+ - [Conditional Caching](#conditional-caching)
46
+ - [Custom Cache Key](#custom-cache-key)
47
+ - [Request State](#request-state)
48
+ - [Storage and Containers](#storage-and-containers)
49
+ - [Cross-Expectation Sync](#cross-expectation-sync)
50
+ - [Extensions](#extensions)
51
+ - [Redis Configuration](#redis-configuration)
52
+ - [Remote Expectations with RemoteClient](#remote-expectations-with-remoteclient)
53
+ - [Grouping with Providers](#grouping-with-providers)
54
+ - [Logger](#logger)
55
+ - [License](#license)
56
+
57
+ ## Features
58
+
59
+ - **Multi-protocol Support**: Full support for both **HTTP** and **WebSocket** protocols.
60
+ - **Flexible Matching**: Match requests by path, method, headers, and data using regex, minimatch, or custom functions.
61
+ - **Payload Manipulation**: Modify request or response payloads on the fly.
62
+ - **Request Forwarding**: Proxy requests to real services with optional caching.
63
+ - **Built-in GUI**: Monitor traffic and manage expectations through a web interface.
64
+ - **Typed Expectations**: First-class support for TypeScript to keep your mocks type-safe.
65
+ - **Advanced State Management**: Store data across requests using Containers or Request State.
66
+ - **XML Support**: Native parsing and serialization for XML payloads.
67
+
68
+ ## Installation
49
69
 
50
70
  ```bash
51
71
  npm i @n1k1t/mock-server
52
72
  ```
53
73
 
54
- ## How it works
74
+ ## Add Skills
55
75
 
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
76
+ If you are using the [skills](https://www.npmjs.com/package/skills) package, you can add `n1k1t/mock-server` to your project using the following command:
81
77
 
82
78
  ```bash
83
- npx mock -h localhost -p 8080
79
+ npx skills add n1k1t/mock-server
84
80
  ```
85
81
 
86
- ### JavaScript
82
+ This package includes the following skills for better integration with AI agents:
87
83
 
88
- ```js
89
- const { MockServer } = require('@n1k1t/mock-server');
90
- MockServer.start({ host: 'localhost', port: 8080 });
91
- ```
84
+ - **`mock-server-basic`**: Core entities management, including server initialization, operators (`$`), expectations, containers, forwarding, and cache.
85
+ - **`mock-server-extensions`**: Advanced features like Redis configuration, `RemoteClient` for remote management, `Provider` for grouping/routing, and `Logger` customization.
92
86
 
93
- ### TypeScript
87
+ ## Simple Example
94
88
 
95
89
  ```ts
96
90
  import { MockServer } from '@n1k1t/mock-server';
97
- MockServer.start({ host: 'localhost', port: 8080 });
98
- ```
99
91
 
100
- ## GUI
92
+ const server = await MockServer.start({ host: 'localhost', port: 8080 });
101
93
 
102
- The mock server provides built-in web panel to track everything that is going through. There are two tabs `Expectations` and `History`
94
+ // Create a type-safe expectation using the operator compiler ($)
95
+ await server.client.createExpectation(({ $ }) => ({
96
+ schema: {
97
+ request: $.has('incoming.path', { $value: '/api/hello' }),
98
+ response: $.set('outgoing.data', { $value: { message: 'world' } }),
99
+ },
100
+ }));
101
+ ```
103
102
 
104
- By default it can be found on `/_system/gui` of a host of mock server. Example: `localhost:8080/_system/gui`
103
+ ## Overview
105
104
 
106
- Also it provides convenient util to navigate through payload of expectations and requests payload
105
+ ### GUI
107
106
 
108
- ## Mock
107
+ 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
108
 
110
- Simple examples can be found in [expectation creation API](#create-expectation)
109
+ ### Storage & Containers
111
110
 
112
- # Expectations
111
+ 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
112
 
114
- ## Schema
113
+ ### Cache
115
114
 
116
- An expectation schema can contain some rules to handle `request`, `response` and `forward`
115
+ 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
116
 
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 |
117
+ ### State & Seeds
123
118
 
124
- **Example**
119
+ - **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).
120
+ - **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
121
 
126
122
  ```ts
127
- await server.client.createExpectation({
123
+ await server.client.createExpectation(({ $ }) => ({
128
124
  schema: {
129
- request: {
130
- $and: [],
131
- },
132
- response: {
133
- $or: [],
134
- },
135
- forward: {
136
- baseUrl: 'https://example.com',
137
- url: '/some/path',
138
- },
125
+ request: $.and([
126
+ $.has('incoming.path', { $value: '/api/faker' }),
127
+ // Manually set seed from query parameter or any other logic
128
+ $.set('seed', { $exec: (seed, { context }) => context.incoming.query.seed ?? context.seed ?? 12345 }),
129
+ ]),
130
+ response: $.set('outgoing.data', {
131
+ $exec: (payload, { faker }) => ({
132
+ id: faker.number.int(), // Will be consistent for the same seed
133
+ name: faker.person.fullName(),
134
+ }),
135
+ }),
139
136
  },
140
- });
137
+ }));
141
138
  ```
142
139
 
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({
140
+ ## Mock Server Utilities
141
+
142
+ ### Expectation Operators ($)
143
+
144
+ Expectations are built using operators that define how to match requests and transform responses.
145
+
146
+ - `$.has(location, { $path, ... })`: Checks if a value exists at the specified path. Supports `$value`, `$match` (minimatch), `$regExp`, and `$exec`.
147
+ - `$.set(location, { $value, ... })`: Sets a value at the specified path.
148
+ - `$.merge(location, { $value, ... })`: Merges an object into the value at the specified path.
149
+ - `$.remove(location, { $path, ... })`: Removes the value at the specified path.
150
+ - `$.and([...operators])`: Logical AND of multiple operators.
151
+ - `$.or([...operators])`: Logical OR of multiple operators.
152
+ - `$.not(operator)`: Logical NOT of an operator.
153
+ - `$.if({ $condition, $then, $else })`: Conditional execution of operators.
154
+ - `$.switch({ $cases, $default })`: Switch-case execution based on the value at the path.
155
+ - `$.exec(fn)`: Executes custom logic. In `match` mode, it must return a boolean. In `manipulate` mode, it can modify the context.
156
+
157
+ ### Operator Locations
158
+
159
+ Many operators (like `$.has`, `$.set`, `$.merge`) accept a `location` as their first argument. This defines where the operation should be performed:
160
+
161
+ - `'incoming.path'`: The URL path of the request.
162
+ - `'incoming.method'`: The HTTP method.
163
+ - `'incoming.query'`: The query parameters object.
164
+ - `'incoming.headers'`: The request headers object.
165
+ - `'incoming.data'`: The request body (parsed JSON or XML).
166
+ - `'outgoing.status'`: The response status code.
167
+ - `'outgoing.headers'`: The response headers object.
168
+ - `'outgoing.data'`: The response body object.
169
+ - `'container'`: Access to a [Container](#container-methods).
170
+ - `'state'`: Access to [Request State](#request-state).
171
+ - `'cache'`: Access to [Forward Cache](#conditional-caching) configuration.
172
+ - `'seed'`: Access to the [Seed](#state--seeds) value.
173
+ - `'error'`: For [Simulating Network Errors](#simulating-network-errors).
174
+
175
+ When targeting an object (like `headers` or `data`), you can use `$path` as the second argument to target a nested value:
176
+ `$.has('incoming.headers', '$path', 'content-type', { $value: 'application/json' })`
177
+
178
+ ### Operator Context
179
+
180
+ When using `$exec`, the second argument provides a context with useful utilities and data:
181
+
182
+ - **`context`**: The full request/response context.
183
+ - `incoming`: Input data (immutable in match mode).
184
+ - `path`: The request URL path.
185
+ - `method`: HTTP method (GET, POST, etc.).
186
+ - `query`: Object containing query parameters.
187
+ - `headers`: Object containing request headers.
188
+ - `data`: Parsed JSON/XML request body.
189
+ - `outgoing`: Output data (target for manipulation).
190
+ - `status`: HTTP status code.
191
+ - `headers`: Response headers.
192
+ - `data`: Response body object (serialized to JSON/XML).
193
+ - `dataRaw`: Buffer for binary data.
194
+ - `stream`: Observable for WebSocket messages.
195
+ - `storage`: Interface to manage [Containers](#container-methods).
196
+ - `container`: The currently bound container (if any).
197
+ - `cache`: Configuration for forwarding cache.
198
+ - `isEnabled`: Whether caching is enabled for this request.
199
+ - `prefix`: Redis key prefix.
200
+ - `key`: Custom cache key (string or object).
201
+ - `ttl`: Time to live in seconds.
202
+ - `hasRead`: (Internal) Whether cache was read.
203
+ - `hasWritten`: (Internal) Whether cache was written.
204
+ - **`state`**: The unique [Request State](#state--seeds). Persistent only for the duration of the current request.
205
+ - **`mode`**: The execution phase: `'match'` (deciding if expectation applies) or `'manipulate'` (preparing the response).
206
+ - **`logger`**: A scoped logger for debugging (`info`, `error`, `warn`).
207
+ - **`faker`**: Full `@faker-js/faker` instance for generating mock data.
208
+ - **`_`**: Lodash utility library.
209
+ - **`d`**: Dayjs instance for date/time manipulation.
210
+ - **`rx`**: RxJS exports for advanced WebSocket stream control.
211
+
212
+ ### Container Methods
213
+
214
+ Containers (accessible via `context.storage`) provide several methods for managing data:
215
+
216
+ - `storage.provide({ key, payload, ttl })`: Retrieves an existing container by key or creates a new one with the provided payload and TTL.
217
+ - `storage.register({ key, payload, ttl })`: Forcefully creates and registers a new container, overwriting any existing one with the same key.
218
+ - `storage.find(key)`: Finds an existing container by key. Returns `undefined` if not found.
219
+ - `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.
220
+ - `container.merge(payload | fn)`: Deeply merges the given payload into the current container state.
221
+ - `container.bind(key)`: Binds the container to an additional key.
222
+ - `container.unbind()`: Removes the container from the storage.
223
+
224
+ ## Usage
225
+
226
+ ### Complex Expectations with `$and`
227
+
228
+ You can group multiple conditions using logical operators like `$.and`, `$.or`, and `$.not` to create sophisticated matching rules.
229
+
230
+ ```ts
231
+ await server.client.createExpectation(({ $ }) => ({
220
232
  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
- },
233
+ request: $.and([
234
+ $.has('incoming.path', { $value: '/api/user' }),
235
+ $.has('incoming.method', { $value: 'POST' }),
236
+ // Checks if 'content-type' header equals 'application/json' using selection with $path = content-type
237
+ $.has('incoming.headers', '$path', 'content-type', { $value: 'application/json' }),
238
+ ]),
239
+ response: $.set('outgoing.data', { $value: { status: 'success' } }),
237
240
  },
238
- });
241
+ }));
239
242
  ```
240
243
 
241
- ### $has
242
-
243
- > **!NOTE** `$exec` operators [have restrictions](#api) when it defined over `HTTP API` or `RemoteClient`
244
+ ### Custom Logic with `$exec`
244
245
 
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**
246
+ For scenarios that require dynamic calculations or external utilities, use the `$exec` operator.
259
247
 
260
248
  ```ts
261
- await server.client.createExpectation({
249
+ await server.client.createExpectation(({ $ }) => ({
262
250
  schema: {
263
- request: {
264
- $has: {
265
- $location: 'path',
266
- $regExp: /^\/foo/,
267
- },
268
- },
251
+ request: $.has('incoming.path', {
252
+ // Custom matching logic: checks if path starts with '/api/data'
253
+ $exec: (path) => path.startsWith('/api/data')
254
+ }),
255
+ response: $.set('outgoing.data', {
256
+ // Use built-in utils like lodash (_), dayjs (d), or faker
257
+ $exec: (payload, { _, d, faker }) => ({
258
+ id: faker.string.uuid(),
259
+ timestamp: d().format(),
260
+ values: _.range(3).map(() => _.random(1, 100)),
261
+ }),
262
+ }),
269
263
  },
270
- });
271
- ```
272
-
273
- **Example using cURL**
264
+ }));
274
265
 
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" }
266
+ // Use $.exec for full control over the expectation execution
267
+ await server.client.createExpectation(({ $ }) => ({
268
+ schema: {
269
+ request: $.exec(({ context, logger, mode }) => {
270
+ // mode can be 'match' (on catching request) or 'manipulate' (on manipulation)
271
+ if (mode === 'match') {
272
+ logger.info('Checking request path', context.incoming.path);
273
+ return context.incoming.path === '/api/custom';
283
274
  }
284
- }
285
- }
286
- }
287
- EOF
275
+ }),
276
+ response: $.set('outgoing.data', { $value: { status: 'custom' } }),
277
+ },
278
+ }));
288
279
  ```
289
280
 
290
- ### $set
291
-
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) |
281
+ ### Dynamic Response Manipulation
301
282
 
302
- **Example using application**
283
+ You can dynamically adjust the response payload based on incoming request parameters (query, body, headers, etc.).
303
284
 
304
285
  ```ts
305
- await server.client.createExpectation({
286
+ await server.client.createExpectation(({ $ }) => ({
306
287
  schema: {
307
- request: {
308
- $set: {
309
- $location: 'incoming.data',
310
- $path: 'foo',
311
- $exec: (payload, { _ }) => _.clamp(payload, 0, 10),
312
- },
313
- },
288
+ request: $.has('incoming.path', { $value: '/api/profile' }),
289
+ response: $.set('outgoing.data', {
290
+ $exec: (payload, { context }) => ({
291
+ id: context.incoming.query.userId,
292
+ // Conditionally include data based on request headers
293
+ debug: context.incoming.headers['x-debug'] === 'true'
294
+ ? { timestamp: Date.now() }
295
+ : undefined
296
+ })
297
+ }),
314
298
  },
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
299
+ }));
334
300
  ```
335
301
 
336
- ### $merge
337
-
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) |
302
+ ### Binary Data and Files
347
303
 
348
- **Example using application**
304
+ You can return binary data (Buffers) as a response body. This is useful for mocking file downloads or image responses.
349
305
 
350
306
  ```ts
351
- await server.client.createExpectation({
307
+ import { readFile } from 'fs/promises';
308
+
309
+ await server.client.createExpectation(({ $ }) => ({
352
310
  schema: {
353
- request: {
354
- $merge: {
355
- $location: 'incoming.data',
356
- $value: { has_mocked: true },
357
- },
358
- },
311
+ request: $.has('incoming.path', { $value: '/api/download' }),
312
+ response: $.and([
313
+ $.set('outgoing.headers', '$path', 'content-type', { $value: 'application/pdf' }),
314
+ $.set('outgoing.dataRaw', {
315
+ $exec: async () => await readFile('./path/to/document.pdf')
316
+ }),
317
+ ]),
359
318
  },
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
319
+ }));
378
320
  ```
379
321
 
380
- ### $remove
322
+ ### XML Support
381
323
 
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**
324
+ 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
325
 
390
326
  ```ts
391
- await server.client.createExpectation({
327
+ await server.client.createExpectation(({ $ }) => ({
392
328
  schema: {
393
- request: {
394
- $remove: { $location: 'outgoing.data' },
395
- },
329
+ request: $.and([
330
+ $.has('incoming.path', { $matchAnyOf: ['/api/user*'] }),
331
+ $.exec(({ context }) => context.incoming.headers['content-type'] === 'application/xml'),
332
+ ]),
333
+ response: $.and([
334
+ $.set('outgoing.headers', '$path', 'content-type', { $value: 'application/xml' }),
335
+ $.set('outgoing.data', {
336
+ $value: {
337
+ user: {
338
+ info: {
339
+ '#text': 'John Doe',
340
+ '@_type': 'mocked',
341
+ '@_id': 123
342
+ }
343
+ },
344
+ },
345
+ }),
346
+ ]),
396
347
  },
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
348
+ }));
412
349
  ```
413
350
 
414
- ### $exec
415
-
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) |
351
+ ### Simulating Network Errors
421
352
 
422
- **Example using application**
353
+ You can simulate connection drops or network errors by setting the `error` property.
423
354
 
424
355
  ```ts
425
- await server.client.createExpectation({
356
+ await server.client.createExpectation(({ $ }) => ({
426
357
  schema: {
427
- request: {
428
- $exec: ({ context, logger }) => {
429
- logger.info(context);
430
- return context.incoming.path === '/foo';
431
- },
432
- },
358
+ request: $.has('incoming.path', { $value: '/api/error' }),
359
+ response: $.set('error', {
360
+ // Common error codes: ECONNRESET, ECONNABORTED, etc.
361
+ $value: 'ECONNRESET'
362
+ }),
433
363
  },
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
364
+ }));
449
365
  ```
450
366
 
451
- ### $and
367
+ ### Disabled by Default
452
368
 
453
- | Type (application) | Type (cURL) | Description |
454
- |--|--|--|
455
- | `object[]` | `object[]` | Provides [operators](#operators) schemas |
456
-
457
- **Example using application**
369
+ You can create expectations that are disabled by default by setting `isEnabled: false`. These can be manually enabled later via the [GUI](#gui).
458
370
 
459
371
  ```ts
460
- await server.client.createExpectation({
372
+ await server.client.createExpectation(({ $ }) => ({
373
+ isEnabled: false,
461
374
  schema: {
462
- request: {
463
- $and: [
464
- { $has: { $location: 'path', $match: 'foo/*' } },
465
- { $has: { $location: 'method', $valueAnyOf: ['GET', 'POST'] } },
466
- ],
467
- },
375
+ request: $.has('incoming.path', { $value: '/api/hidden' }),
376
+ response: $.set('outgoing.data', { $value: { message: 'Activate me in GUI' } }),
468
377
  },
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
378
+ }));
487
379
  ```
488
380
 
489
- ### $or
490
-
491
- | Type (application) | Type (cURL) | Description |
492
- |--|--|--|
493
- | `object[]` | `object[]` | Provides [operators](#operators) schemas |
381
+ ### Type Safety
494
382
 
495
- **Example using application**
383
+ You can provide generic types to `createExpectation` to ensure your schemas and `$exec` functions are fully typed.
496
384
 
497
385
  ```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
- }
386
+ interface MyContext {
387
+ incoming: {
388
+ query: {
389
+ userId: string;
390
+ expand?: boolean;
391
+ };
392
+ };
393
+ outgoing: {
394
+ data: {
395
+ id: string;
396
+ name: string;
397
+ };
398
+ };
523
399
  }
524
- EOF
525
- ```
526
-
527
- ### $not
528
-
529
- | Type (application) | Type (cURL) | Description |
530
- |--|--|--|
531
- | `object` | `object` | Provides an [operators](#operators) schema |
532
400
 
533
- **Example using application**
534
-
535
- ```ts
536
- await server.client.createExpectation({
401
+ await server.client.createExpectation<MyContext>(({ $ }) => ({
537
402
  schema: {
538
- request: {
539
- $not: { $has: { $location: 'path', $match: 'foo/*' } },
540
- },
403
+ request: $.has('incoming.query', {
404
+ $exec: (query) => query.userId === '123'
405
+ }),
406
+ response: $.set('outgoing.data', {
407
+ $exec: (payload, { faker }) => ({
408
+ id: '123',
409
+ name: faker.person.fullName()
410
+ })
411
+ }),
541
412
  },
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
413
+ }));
557
414
  ```
558
415
 
559
- ### $if
416
+ ### WebSocket Support
560
417
 
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**
418
+ The mock server provides first-class support for WebSockets, allowing you to intercept and mock streaming data using [RxJS](https://rxjs.dev/).
568
419
 
569
420
  ```ts
570
- await server.client.createExpectation({
421
+ await server.client.createExpectation(({ $, utils }) => ({
422
+ // 1. Specify 'ws' transport
423
+ transports: utils.transports(['ws']),
424
+
571
425
  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
- },
426
+ request: $.has('incoming.path', { $value: '/ws/updates' }),
427
+ response: $.and([
428
+ // 2. Set custom WS status code if needed
429
+ $.set('outgoing.status', { $value: 1000 }),
430
+ // 3. Define the outgoing stream
431
+ $.set('outgoing.stream', {
432
+ $exec: (stream, { rx }) => {
433
+ // Return an Observable of messages
434
+ return rx.from([
435
+ { event: 'connected' },
436
+ { event: 'data', value: 1 },
437
+ { event: 'data', value: 2 },
438
+ ]);
439
+ },
440
+ }),
441
+ ]),
579
442
  },
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
443
+ }));
599
444
  ```
600
445
 
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) |
446
+ ### Request Forwarding
612
447
 
613
- **Example using application**
448
+ You can forward requests to an external API and manipulate the response before it's returned to the client.
614
449
 
615
- ```ts
616
- await server.client.createExpectation({
450
+ ```ts
451
+ await server.client.createExpectation(({ $ }) => ({
617
452
  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
- },
453
+ request: $.has('incoming.path', { $value: '/api/proxy' }),
454
+ // Forward the request to an external service
455
+ forward: {
456
+ url: 'https://api.external-service.com/data',
629
457
  },
458
+ // Manipulate the response from the external service
459
+ response: $.set('outgoing.data', {
460
+ $exec: (data) => ({
461
+ ...data,
462
+ source: 'mock-server',
463
+ timestamp: new Date().toISOString(),
464
+ }),
465
+ }),
630
466
  },
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
467
+ }));
655
468
  ```
656
469
 
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)
470
+ ### Using Relative Paths with `baseUrl`
660
471
 
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**
472
+ When using `baseUrl`, the original request path is appended to the target URL.
664
473
 
665
474
  ```ts
666
- await client.createExpectation<{
667
- incoming: {
668
- query: {
669
- foo: 'a' | 'b' | 'c';
670
- bar?: string;
671
- };
672
- };
673
- }>(({ $ }) => ({
475
+ await server.client.createExpectation(({ $ }) => ({
674
476
  schema: {
675
- request: $.or([
676
- $.has('incoming.query', '$path', 'foo', { $value: 'a' }),
677
- $.has('incoming.query', { $match: { foo: 'b' } }),
477
+ request: $.and([
478
+ $.has('incoming.path', { $value: '/api/v1/*' }),
479
+ // Rewrite the path from v1 to v2 before forwarding
480
+ $.set('incoming.path', {
481
+ $exec: (path) => path.replace('/v1/', '/v2/'),
482
+ }),
678
483
  ]),
484
+ forward: {
485
+ // If request is '/api/v1/users', it forwards to 'https://legacy-api.com/api/v2/users'
486
+ baseUrl: 'https://legacy-api.com',
487
+ },
488
+ response: $.set('outgoing.headers', '$path', 'x-proxied-by', { $value: 'mock-server' }),
679
489
  },
680
490
  }));
681
491
  ```
682
492
 
493
+ ### Transforming Request Data Before Forwarding
494
+
495
+ You can also modify the request body or headers before they are sent to the target API.
496
+
683
497
  ```ts
684
- await client.createExpectation<{
498
+ interface SearchRequest {
685
499
  incoming: {
686
- query: {
687
- foo: 'a' | 'b' | 'c';
688
- bar?: string;
689
- };
690
- };
691
- outgoing: {
692
500
  data: {
693
- foo: 'a' | 'b' | 'c';
694
- bar?: {
695
- baz: 'a' | 'b' | 'c';
696
- };
501
+ query: string;
502
+ limit?: number;
697
503
  };
698
504
  };
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
- }),
505
+ }
708
506
 
709
- $.switch('incoming.query', '$path', 'bar', {
710
- $cases: {
711
- 'something': $.set('outgoing.data', '$path', 'bar.baz', { $value: 'c' }),
712
- },
507
+ await server.client.createExpectation<SearchRequest>(({ $ }) => ({
508
+ schema: {
509
+ request: $.and([
510
+ $.has('incoming.path', { $value: '/api/search' }),
511
+ // Inject API key and transform data before forwarding
512
+ $.merge('incoming.headers', { $value: { 'X-API-Key': 'secret-token' } }),
513
+ $.set('incoming.data', {
514
+ $exec: (data) => ({
515
+ ...data,
516
+ limit: data.limit ?? 20,
517
+ q: data.query, // Rename 'query' to 'q' for the external API
518
+ })
713
519
  }),
714
520
  ]),
521
+ forward: {
522
+ url: 'https://external-search-service.com/v1/query',
523
+ },
715
524
  },
716
525
  }));
717
526
  ```
718
527
 
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)
528
+ ### WebSocket Forwarding and Manipulation
731
529
 
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 |
747
-
748
-
749
- **Example**
530
+ You can forward WebSocket connections to a real server and manipulate the message stream on the fly.
750
531
 
751
532
  ```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
- }),
533
+ await server.client.createExpectation(({ $, utils }) => ({
534
+ transports: utils.transports(['ws']),
763
535
 
764
- response: $.set('outgoing.data', {
765
- $exec: (payload, { context }) => ({
766
- count: context.container!.payload.counter,
767
- }),
536
+ schema: {
537
+ request: $.has('incoming.path', { $value: '/ws/proxy' }),
538
+ forward: {
539
+ baseUrl: 'wss://real-server.com',
540
+ },
541
+ response: $.set('outgoing.stream', {
542
+ $exec: (stream, { rx }) => {
543
+ // 'stream' is the Observable from the real server
544
+ return stream?.pipe(
545
+ // Inject extra message at the beginning
546
+ rx.startWith({ event: 'proxy-init', timestamp: Date.now() }),
547
+ // Transform real messages
548
+ rx.map((message) => ({ ...message, intercepted: true }))
549
+ );
550
+ },
768
551
  }),
769
552
  },
770
553
  }));
771
554
  ```
772
555
 
773
- ## Cache
774
-
775
- > **!NOTE** Cache is usable **only** to store a payload of forwarded requests
776
-
777
- To work with cache the mock server uses [ioredis](https://www.npmjs.com/package/ioredis) package
556
+ ### Conditional Caching
778
557
 
779
- To configure it use `database.redis` configuration on the mock server start options
780
-
781
- **Example**
558
+ You can control when the mock server should cache a forwarded response. For example, only cache responses with a `200 OK` status.
782
559
 
783
560
  ```ts
784
- const server = await MockServer.start({
785
- host: 'localhost',
786
- port: 8080,
787
-
788
- databases: {
789
- redis: {
790
- host: 'localhost',
791
- port: 6379,
561
+ await server.client.createExpectation(({ $ }) => ({
562
+ schema: {
563
+ request: $.has('incoming.path', { $value: '/api/cacheable' }),
564
+ forward: {
565
+ baseUrl: 'https://api.service.com',
566
+ cache: {
567
+ ttl: 3600, // 1 hour
568
+ },
792
569
  },
570
+ response: $.set('cache', '$path', 'isEnabled', {
571
+ $exec: (value, { context }) => context.outgoing.status === 200,
572
+ }),
793
573
  },
794
- });
574
+ }));
795
575
  ```
796
576
 
797
- **How it works in steps?**
577
+ ### Custom Cache Key
798
578
 
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**
579
+ 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
580
 
814
581
  ```ts
815
- await client.createExpectation(({ $ }) => ({
582
+ await server.client.createExpectation(({ $ }) => ({
816
583
  schema: {
817
- response: $.set('cache', '$path', 'isEnabled', {
818
- $exec: (payload, { context }) => context.outgoing.status < 400,
819
- }),
820
-
584
+ request: $.and([
585
+ $.has('incoming.path', { $value: '/api/cache-custom' }),
586
+ $.set('cache', '$path', 'key', {
587
+ // Use a specific header as the cache key
588
+ $exec: (key, { context }) => context.incoming.headers['x-user-id'],
589
+ }),
590
+ ]),
821
591
  forward: {
822
- baseUrl: 'https://example.com',
823
-
592
+ baseUrl: 'https://api.service.com',
824
593
  cache: {
825
- ttl: 30 * 24 * 60 * 60,
594
+ isEnabled: true,
595
+ ttl: 600,
826
596
  },
827
597
  },
828
598
  },
829
599
  }));
830
- ```
831
-
832
- ## State
833
-
834
- State is a unique storage of each request. It can be used to handle complex expectations
835
600
 
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
- }>(({ $ }) => ({
601
+ // You can also provide an object as a key; it will be automatically hashed
602
+ await server.client.createExpectation(({ $ }) => ({
856
603
  schema: {
857
604
  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
- },
605
+ $.has('incoming.path', { $value: '/api/cache-object' }),
606
+ $.set('cache', '$path', 'key', {
607
+ // Cache depends on specific properties of the incoming data
608
+ $exec: (key, { context }) => ({
609
+ id: context.incoming.data.id,
610
+ type: context.incoming.data.type,
611
+ }),
863
612
  }),
864
613
  ]),
865
- response: $.set('outgoing.data', {
866
- $exec: (payload, { state }) => ({ id: state.id ?? 0 }),
867
- }),
614
+ forward: {
615
+ baseUrl: 'https://api.service.com',
616
+ cache: { isEnabled: true },
617
+ },
868
618
  },
869
619
  }));
870
620
  ```
871
621
 
872
- ## Seeds
873
-
874
- Seeds can help to generate content with the same values each request using [faker](https://www.npmjs.com/package/@faker-js/faker)
622
+ ### Request State
875
623
 
876
- By default a number of seed takes from `X-Use-Mock-Seed` in `incoming.headers`
877
-
878
- **Example**
624
+ 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
625
 
880
626
  ```ts
881
- await client.createExpectation(({ $ }) => ({
627
+ interface MyState {
628
+ state: {
629
+ internalId: number;
630
+ };
631
+ }
632
+
633
+ await server.client.createExpectation<MyState>(({ $ }) => ({
882
634
  schema: {
883
635
  request: $.and([
884
- $.set('seed', { $exec: (seed) => seed ?? 123 }),
636
+ $.has('incoming.path', { $value: '/api/stateful' }),
637
+ $.set('state', {
638
+ // Compute and store data in state for later use
639
+ $exec: (state) => ({ internalId: state.internalId ?? Math.random() }),
640
+ }),
885
641
  ]),
886
642
  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'),
643
+ // Use the stored state in response building
644
+ $exec: (payload, { state }) => ({
645
+ id: state.internalId,
891
646
  }),
892
647
  }),
893
648
  },
894
649
  }));
895
650
  ```
896
651
 
897
- ## XML
652
+ ### Storage and Containers
898
653
 
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:
654
+ 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
655
 
901
656
  ```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
- }
657
+ interface CounterContainer {
658
+ container: {
659
+ count: number;
660
+ };
932
661
  }
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
662
 
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
-
962
- **Using cURL**
963
-
964
- ```bash
965
- curl -H "Content-type: application/json" --location "localhost:8080/_system/ping"
663
+ await server.client.createExpectation<CounterContainer>(({ $ }) => ({
664
+ schema: {
665
+ request: $.set('container', {
666
+ $exec: (container, { context }) => context.storage
667
+ // provide() finds an existing container by key or creates a new one with the given payload
668
+ .provide({ key: 'my-counter', payload: { count: 0 } })
669
+ .assign((payload) => ({ count: payload.count + 1 })),
670
+ }),
671
+ response: $.set('outgoing.data', {
672
+ $exec: (payload, { context }) => ({
673
+ // Access the incremented value from the container
674
+ currentCount: context.container!.payload.count,
675
+ }),
676
+ }),
677
+ },
678
+ }));
966
679
  ```
967
680
 
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
- ```
681
+ ### Cross-Expectation Sync
976
682
 
977
- **Using application lib on remotely**
683
+ You can use Containers to synchronize data between different endpoints. For example, one endpoint increments a counter, and another retrieves its current value.
978
684
 
979
685
  ```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
- }
686
+ interface SyncContainer {
687
+ container: {
688
+ count: number;
689
+ };
1016
690
  }
1017
- EOF
1018
- ```
1019
691
 
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({
692
+ // Expectation 1: Increments the counter
693
+ await server.client.createExpectation<SyncContainer>(({ $ }) => ({
694
+ name: 'Incrementer',
1027
695
  schema: {
1028
- request: {
1029
- $has: {
1030
- $location: 'method',
1031
- $value: 'GET',
1032
- },
1033
- },
696
+ request: $.and([
697
+ $.has('incoming.path', { $value: '/api/increment' }),
698
+ $.set('container', {
699
+ $exec: (container, { context }) => context.storage
700
+ .provide({ key: 'shared-counter', payload: { count: 0 } })
701
+ .assign((payload) => ({ count: payload.count + 1 })),
702
+ }),
703
+ ]),
704
+ response: $.set('outgoing.data', { $value: { status: 'incremented' } }),
1034
705
  },
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';
706
+ }));
1044
707
 
1045
- const client = await RemoteClient.connect({ host: 'localhost', port: 8080 });
1046
- const expectation = await client.createExpectation({
708
+ // Expectation 2: Retrieves the current value
709
+ await server.client.createExpectation<SyncContainer>(({ $ }) => ({
710
+ name: 'Getter',
1047
711
  schema: {
1048
- request: {
1049
- $has: {
1050
- $location: 'method',
1051
- $value: 'GET',
1052
- },
1053
- },
712
+ request: $.and([
713
+ $.has('incoming.path', { $value: '/api/count' }),
714
+ $.set('container', {
715
+ $exec: (container, { context }) => context.storage
716
+ .provide({ key: 'shared-counter', payload: { count: 0 } }),
717
+ }),
718
+ ]),
719
+ response: $.set('outgoing.data', {
720
+ $exec: (payload, { context }) => ({
721
+ total: context.container!.payload.count,
722
+ }),
723
+ }),
1054
724
  },
1055
- });
1056
-
1057
- console.log('Mock expectation has created', expectation.id);
725
+ }));
1058
726
  ```
1059
727
 
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 |
1070
-
1071
- `OUTPUT`
728
+ ## Extensions
1072
729
 
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 |
730
+ ### Redis Configuration
1078
731
 
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**
732
+ The mock server uses Redis for caching forwarded responses and persistent storage. You can configure the connection details during server startup.
1091
733
 
1092
734
  ```ts
1093
735
  import { MockServer } from '@n1k1t/mock-server';
1094
736
 
1095
- const server = await MockServer.start({ host: 'localhost', port: 8080 });
1096
- const expectation = await server.client.updateExpectation({
1097
- id: '...',
1098
- set: { name: 'The expectation' }
737
+ const server = await MockServer.start({
738
+ host: 'localhost',
739
+ port: 8080,
740
+ databases: {
741
+ redis: {
742
+ host: 'localhost',
743
+ port: 6379,
744
+ // Optional: password, keyPrefix, etc.
745
+ keyPrefix: 'my-mock-server:',
746
+ },
747
+ },
1099
748
  });
1100
-
1101
- console.log('Mock expectation has updated', expectation);
1102
749
  ```
1103
750
 
1104
- **Using application lib on remotely**
751
+ ### Remote Expectations with `RemoteClient`
752
+
753
+ The `RemoteClient` allows you to manage expectations on a running mock server instance from a remote application or a separate test suite.
1105
754
 
1106
755
  ```ts
1107
756
  import { RemoteClient } from '@n1k1t/mock-server';
1108
757
 
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
758
+ // 1. Connect to a running mock server
759
+ const client = await RemoteClient.connect({ baseUrl: 'http://localhost:8080' });
1119
760
 
1120
- `INPUT` `DELETE /_system/expectations`
761
+ // 2. Create expectations remotely
762
+ await client.createExpectation(({ $, utils }) => ({
763
+ // Specify allowed transports (http, ws)
764
+ transports: utils.transports(['http']),
1121
765
 
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
766
+ schema: {
767
+ request: $.has('path', { $value: '/api/remote-mock' }),
768
+ response: $.set('outgoing.data', { $value: { success: true, source: 'remote-client' } }),
769
+ },
770
+ }));
1134
771
  ```
1135
772
 
1136
- **Using application lib on server side**
773
+ ### Grouping with Providers
1137
774
 
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**
775
+ 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
776
 
1149
777
  ```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
- ```
778
+ import { MockServer, Provider } from '@n1k1t/mock-server';
1157
779
 
1158
- # Additional
780
+ // 1. Define providers for different modules
781
+ const authProvider = Provider.build({ group: 'auth' });
782
+ const userProvider = Provider.build({ group: 'users' });
1159
783
 
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';
784
+ // 2. Setup server
785
+ const server = await MockServer.start({ port: 13000 });
1166
786
 
1167
- config.merge({
1168
- logger: {
1169
- level: 'D', // Logger level (default: D)
787
+ // 3. Add expectations to specific providers
788
+ await authProvider.client.createExpectation(({ $ }) => ({
789
+ schema: {
790
+ request: $.has('path', { $value: '/api/auth/login' }),
791
+ response: $.set('outgoing.data', { $value: { token: 'secret-token' } }),
1170
792
  },
793
+ }));
1171
794
 
1172
- history: {
1173
- limit: 100, // Limit for history of requests (default: 100)
795
+ await userProvider.client.createExpectation(({ $ }) => ({
796
+ schema: {
797
+ request: $.has('path', { $value: '/api/users/me' }),
798
+ response: $.set('outgoing.data', { $value: { id: 1, name: 'John' } }),
1174
799
  },
800
+ }));
1175
801
 
1176
- containers: {
1177
- expiredCleaningInterval: 60 * 60, // Expired containers cleaning interval in seconds (default: 1h)
1178
- },
1179
- });
802
+ // 4. Setup routing
803
+ server.router
804
+ .register('/api/auth/**', { provider: authProvider })
805
+ .register('/api/users/**', { provider: userProvider });
1180
806
  ```
1181
807
 
1182
- ## Logger
808
+ ### Logger
1183
809
 
1184
- > **!NOTE** Configuration must be provided in the same script like mock server
810
+ 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
811
 
1186
812
  ```ts
1187
813
  import { Logger } from '@n1k1t/mock-server';
1188
814
 
1189
- // It defines your own logger methods
1190
815
  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',
816
+ info: (title, ...messages) => {
817
+ // Send to your logging service
818
+ console.log(`[${title}]`, ...messages);
819
+ },
820
+ error: (title, ...messages) => {
821
+ // Handle errors specifically
822
+ },
1202
823
  });
1203
824
  ```
1204
825
 
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**
826
+ Serializers allow you to mask sensitive data (like passwords or credit card numbers) before they are logged.
1217
827
 
1218
828
  ```ts
1219
- import { Logger, metaStorage } from '@n1k1t/mock-server';
1220
-
1221
- // Some external logger with meta context support
1222
- const external = {...};
829
+ import { Logger } from '@n1k1t/mock-server';
1223
830
 
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),
831
+ Logger.useSerializers({
832
+ password: () => '***',
833
+ token: (val: string) => `${val.slice(0, 4)}...`,
1231
834
  });
1232
835
  ```
1233
836
 
1234
- **Usage**
837
+ ## License
1235
838
 
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
- ```
839
+ MIT