@mojaloop/sdk-scheme-adapter 11.18.8

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 (277) hide show
  1. package/.env.example +140 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc.json +30 -0
  4. package/.nvmrc +1 -0
  5. package/.versionrc +15 -0
  6. package/CHANGELOG.md +118 -0
  7. package/InboundServer/api.yaml +3594 -0
  8. package/InboundServer/api_template.yaml +69 -0
  9. package/InboundServer/handlers.js +940 -0
  10. package/InboundServer/index.js +205 -0
  11. package/InboundServer/middlewares.js +426 -0
  12. package/OAuthTestServer/index.js +66 -0
  13. package/OAuthTestServer/model.js +70 -0
  14. package/OutboundServer/api.yaml +2732 -0
  15. package/OutboundServer/api_interfaces/index.d.ts +117 -0
  16. package/OutboundServer/api_interfaces/openapi.d.ts +1475 -0
  17. package/OutboundServer/api_template/components/parameters/bulkQuoteId.yaml +9 -0
  18. package/OutboundServer/api_template/components/parameters/bulkTransferId.yaml +9 -0
  19. package/OutboundServer/api_template/components/parameters/requestToPayTransactionId.yaml +9 -0
  20. package/OutboundServer/api_template/components/parameters/transferId.yaml +9 -0
  21. package/OutboundServer/api_template/components/responses/accountsCreationCompleted.yaml +5 -0
  22. package/OutboundServer/api_template/components/responses/accountsCreationError.yaml +5 -0
  23. package/OutboundServer/api_template/components/responses/accountsCreationTimeout.yaml +5 -0
  24. package/OutboundServer/api_template/components/responses/authorizationPostSuccess.yaml +5 -0
  25. package/OutboundServer/api_template/components/responses/authorizationsServerError.yaml +5 -0
  26. package/OutboundServer/api_template/components/responses/bulkQuoteBadRequest.yaml +5 -0
  27. package/OutboundServer/api_template/components/responses/bulkQuoteServerError.yaml +5 -0
  28. package/OutboundServer/api_template/components/responses/bulkQuoteSuccess.yaml +5 -0
  29. package/OutboundServer/api_template/components/responses/bulkQuoteTimeout.yaml +5 -0
  30. package/OutboundServer/api_template/components/responses/bulkTransferBadRequest.yaml +5 -0
  31. package/OutboundServer/api_template/components/responses/bulkTransferServerError.yaml +5 -0
  32. package/OutboundServer/api_template/components/responses/bulkTransferSuccess.yaml +5 -0
  33. package/OutboundServer/api_template/components/responses/bulkTransferTimeout.yaml +5 -0
  34. package/OutboundServer/api_template/components/responses/partiesByIdError404.yaml +9 -0
  35. package/OutboundServer/api_template/components/responses/partiesByIdSuccess.yaml +5 -0
  36. package/OutboundServer/api_template/components/responses/quotesPostSuccess.yaml +5 -0
  37. package/OutboundServer/api_template/components/responses/quotesServerError.yaml +5 -0
  38. package/OutboundServer/api_template/components/responses/requestToPaySuccess.yaml +5 -0
  39. package/OutboundServer/api_template/components/responses/requestToPayTransferBadRequest.yaml +5 -0
  40. package/OutboundServer/api_template/components/responses/requestToPayTransferSuccess.yaml +5 -0
  41. package/OutboundServer/api_template/components/responses/simpleTransfersPostSuccess.yaml +5 -0
  42. package/OutboundServer/api_template/components/responses/simpleTransfersServerError.yaml +5 -0
  43. package/OutboundServer/api_template/components/responses/transferBadRequest.yaml +5 -0
  44. package/OutboundServer/api_template/components/responses/transferServerError.yaml +5 -0
  45. package/OutboundServer/api_template/components/responses/transferSuccess.yaml +5 -0
  46. package/OutboundServer/api_template/components/responses/transferTimeout.yaml +5 -0
  47. package/OutboundServer/api_template/components/schemas/accountCreationStatus.yaml +18 -0
  48. package/OutboundServer/api_template/components/schemas/accountsCreationState.yaml +4 -0
  49. package/OutboundServer/api_template/components/schemas/accountsRequest.yaml +20 -0
  50. package/OutboundServer/api_template/components/schemas/accountsResponse.yaml +15 -0
  51. package/OutboundServer/api_template/components/schemas/async2SyncCurrentState.yaml +5 -0
  52. package/OutboundServer/api_template/components/schemas/authorizationsPostRequest.yaml +15 -0
  53. package/OutboundServer/api_template/components/schemas/authorizationsPostResponse.yaml +19 -0
  54. package/OutboundServer/api_template/components/schemas/bulkQuoteErrorResponse.yaml +8 -0
  55. package/OutboundServer/api_template/components/schemas/bulkQuoteRequest.yaml +26 -0
  56. package/OutboundServer/api_template/components/schemas/bulkQuoteResponse.yaml +21 -0
  57. package/OutboundServer/api_template/components/schemas/bulkQuoteStatus.yaml +4 -0
  58. package/OutboundServer/api_template/components/schemas/bulkQuoteStatusResponse.yaml +17 -0
  59. package/OutboundServer/api_template/components/schemas/bulkTransferErrorResponse.yaml +8 -0
  60. package/OutboundServer/api_template/components/schemas/bulkTransferRequest.yaml +26 -0
  61. package/OutboundServer/api_template/components/schemas/bulkTransferResponse.yaml +16 -0
  62. package/OutboundServer/api_template/components/schemas/bulkTransferStatus.yaml +4 -0
  63. package/OutboundServer/api_template/components/schemas/bulkTransferStatusResponse.yaml +17 -0
  64. package/OutboundServer/api_template/components/schemas/errorAccountsResponse.yaml +8 -0
  65. package/OutboundServer/api_template/components/schemas/errorAuthorizationsResponse.yaml +3 -0
  66. package/OutboundServer/api_template/components/schemas/errorQuotesResponse.yaml +9 -0
  67. package/OutboundServer/api_template/components/schemas/errorResponse.yaml +8 -0
  68. package/OutboundServer/api_template/components/schemas/errorSimpleTransfersResponse.yaml +3 -0
  69. package/OutboundServer/api_template/components/schemas/errorTransferResponse.yaml +8 -0
  70. package/OutboundServer/api_template/components/schemas/extensionListEmptiable.yaml +6 -0
  71. package/OutboundServer/api_template/components/schemas/individualQuote.yaml +32 -0
  72. package/OutboundServer/api_template/components/schemas/individualQuoteResult.yaml +28 -0
  73. package/OutboundServer/api_template/components/schemas/individualTransfer.yaml +32 -0
  74. package/OutboundServer/api_template/components/schemas/individualTransferFulfilment.yaml +13 -0
  75. package/OutboundServer/api_template/components/schemas/individualTransferResult.yaml +41 -0
  76. package/OutboundServer/api_template/components/schemas/mojaloopError.yaml +5 -0
  77. package/OutboundServer/api_template/components/schemas/mojaloopTransactionRequestState.yaml +2 -0
  78. package/OutboundServer/api_template/components/schemas/partiesByIdResponse.yaml +13 -0
  79. package/OutboundServer/api_template/components/schemas/quote.yaml +3 -0
  80. package/OutboundServer/api_template/components/schemas/quoteError.yaml +16 -0
  81. package/OutboundServer/api_template/components/schemas/quotesPostRequest.yaml +13 -0
  82. package/OutboundServer/api_template/components/schemas/quotesPostResponse.yaml +48 -0
  83. package/OutboundServer/api_template/components/schemas/requestToPayRequest.yaml +39 -0
  84. package/OutboundServer/api_template/components/schemas/requestToPayResponse.yaml +41 -0
  85. package/OutboundServer/api_template/components/schemas/requestToPayTransferRequest.yaml +42 -0
  86. package/OutboundServer/api_template/components/schemas/requestToPayTransferResponse.yaml +58 -0
  87. package/OutboundServer/api_template/components/schemas/simpleTransferServerError.yaml +5 -0
  88. package/OutboundServer/api_template/components/schemas/simpleTransfersPostRequest.yaml +12 -0
  89. package/OutboundServer/api_template/components/schemas/simpleTransfersPostResponse.yaml +11 -0
  90. package/OutboundServer/api_template/components/schemas/transactionType.yaml +4 -0
  91. package/OutboundServer/api_template/components/schemas/transferContinuationAcceptOTP.yaml +9 -0
  92. package/OutboundServer/api_template/components/schemas/transferContinuationAcceptParty.yaml +8 -0
  93. package/OutboundServer/api_template/components/schemas/transferContinuationAcceptQuote.yaml +9 -0
  94. package/OutboundServer/api_template/components/schemas/transferError.yaml +16 -0
  95. package/OutboundServer/api_template/components/schemas/transferFulfilment.yaml +3 -0
  96. package/OutboundServer/api_template/components/schemas/transferParty.yaml +40 -0
  97. package/OutboundServer/api_template/components/schemas/transferRequest.yaml +37 -0
  98. package/OutboundServer/api_template/components/schemas/transferResponse.yaml +58 -0
  99. package/OutboundServer/api_template/components/schemas/transferStatus.yaml +6 -0
  100. package/OutboundServer/api_template/components/schemas/transferStatusResponse.yaml +13 -0
  101. package/OutboundServer/api_template/health.yaml +12 -0
  102. package/OutboundServer/api_template/openapi.yaml +55 -0
  103. package/OutboundServer/api_template/paths/accounts.yaml +26 -0
  104. package/OutboundServer/api_template/paths/authorizations.yaml +19 -0
  105. package/OutboundServer/api_template/paths/bulkQuotes.yaml +23 -0
  106. package/OutboundServer/api_template/paths/bulkQuotes_bulkQuoteId.yaml +24 -0
  107. package/OutboundServer/api_template/paths/bulkTransfers.yaml +23 -0
  108. package/OutboundServer/api_template/paths/bulkTransfers_bulkTransferId.yaml +24 -0
  109. package/OutboundServer/api_template/paths/parties_Type_ID.yaml +20 -0
  110. package/OutboundServer/api_template/paths/parties_Type_ID_SubId.yaml +22 -0
  111. package/OutboundServer/api_template/paths/quotes.yaml +20 -0
  112. package/OutboundServer/api_template/paths/requestToPay.yaml +22 -0
  113. package/OutboundServer/api_template/paths/requestToPayTransfer.yaml +57 -0
  114. package/OutboundServer/api_template/paths/requestToPayTransfer_requestToPayTransactionId.yaml +34 -0
  115. package/OutboundServer/api_template/paths/simpleTransfers.yaml +19 -0
  116. package/OutboundServer/api_template/paths/transfers.yaml +55 -0
  117. package/OutboundServer/api_template/paths/transfers_transferId.yaml +58 -0
  118. package/OutboundServer/handlers.js +622 -0
  119. package/OutboundServer/index.js +137 -0
  120. package/OutboundServer/middlewares.js +67 -0
  121. package/TestServer/api.yaml +62 -0
  122. package/TestServer/handlers.js +63 -0
  123. package/TestServer/index.js +215 -0
  124. package/audit-resolve.json +65 -0
  125. package/babel.config.js +3 -0
  126. package/config.js +158 -0
  127. package/index.d.ts +1 -0
  128. package/index.js +149 -0
  129. package/jest.config.js +15 -0
  130. package/lib/api/index.js +12 -0
  131. package/lib/cache.js +352 -0
  132. package/lib/check.js +25 -0
  133. package/lib/model/AccountsModel.js +396 -0
  134. package/lib/model/Async2SyncModel.js +283 -0
  135. package/lib/model/AuthorizationsModel.js +86 -0
  136. package/lib/model/InboundTransfersModel.js +730 -0
  137. package/lib/model/OutboundBulkQuotesModel.js +485 -0
  138. package/lib/model/OutboundBulkTransfersModel.js +479 -0
  139. package/lib/model/OutboundRequestToPayModel.js +517 -0
  140. package/lib/model/OutboundRequestToPayTransferModel.js +893 -0
  141. package/lib/model/OutboundTransfersModel.js +823 -0
  142. package/lib/model/PartiesModel.js +70 -0
  143. package/lib/model/ProxyModel/MatchRules/Expression.js +48 -0
  144. package/lib/model/ProxyModel/MatchRules/Headers.js +65 -0
  145. package/lib/model/ProxyModel/MatchRules/MatchRule.js +27 -0
  146. package/lib/model/ProxyModel/MatchRules/Path.js +36 -0
  147. package/lib/model/ProxyModel/MatchRules/Query.js +65 -0
  148. package/lib/model/ProxyModel/MatchRules/index.js +19 -0
  149. package/lib/model/ProxyModel/Route.js +82 -0
  150. package/lib/model/ProxyModel/configSchema.json +118 -0
  151. package/lib/model/ProxyModel/index.js +138 -0
  152. package/lib/model/QuotesModel.js +94 -0
  153. package/lib/model/TransfersModel.js +81 -0
  154. package/lib/model/common/BackendError.js +26 -0
  155. package/lib/model/common/PersistentStateMachine.js +93 -0
  156. package/lib/model/common/index.js +18 -0
  157. package/lib/model/index.js +43 -0
  158. package/lib/model/lib/deferredJob.js +113 -0
  159. package/lib/model/lib/index.js +9 -0
  160. package/lib/model/lib/requests/backendRequests.js +227 -0
  161. package/lib/model/lib/requests/common.js +76 -0
  162. package/lib/model/lib/requests/index.js +19 -0
  163. package/lib/model/lib/shared.js +468 -0
  164. package/lib/randomphrase/index.js +21 -0
  165. package/lib/randomphrase/words.json +3397 -0
  166. package/lib/router.js +28 -0
  167. package/lib/validate.js +205 -0
  168. package/package.json +102 -0
  169. package/test/__mocks__/@mojaloop/sdk-standard-components.js +152 -0
  170. package/test/__mocks__/javascript-state-machine.js +21 -0
  171. package/test/__mocks__/redis.js +49 -0
  172. package/test/__mocks__/uuidv4.js +16 -0
  173. package/test/config/integration.env +136 -0
  174. package/test/integration/lib/Outbound/authorizations.test.js +58 -0
  175. package/test/integration/lib/Outbound/data/authorizationsPostRequest.json +43 -0
  176. package/test/integration/lib/Outbound/data/quotesPostRequest.json +52 -0
  177. package/test/integration/lib/Outbound/data/transfersPostRequest.json +24 -0
  178. package/test/integration/lib/Outbound/parties.test.js +28 -0
  179. package/test/integration/lib/Outbound/quotes.test.js +58 -0
  180. package/test/integration/lib/Outbound/simpleTransfers.test.js +67 -0
  181. package/test/integration/lib/cache.test.js +80 -0
  182. package/test/integration/testEnv.js +7 -0
  183. package/test/unit/InboundServer.test.js +443 -0
  184. package/test/unit/TestServer.test.js +394 -0
  185. package/test/unit/api/accounts/accounts.test.js +128 -0
  186. package/test/unit/api/accounts/data/postAccountsBody.json +7 -0
  187. package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +25 -0
  188. package/test/unit/api/accounts/data/postAccountsErrorTimeoutResponse.json +19 -0
  189. package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +17 -0
  190. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +21 -0
  191. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +21 -0
  192. package/test/unit/api/accounts/utils.js +65 -0
  193. package/test/unit/api/proxy/data/proxyConfig.yaml +82 -0
  194. package/test/unit/api/proxy/data/requestBody.json +22 -0
  195. package/test/unit/api/proxy/data/requestHeaders.json +5 -0
  196. package/test/unit/api/proxy/data/requestQuery.json +6 -0
  197. package/test/unit/api/proxy/data/responseBody.json +21 -0
  198. package/test/unit/api/proxy/data/responseHeaders.json +5 -0
  199. package/test/unit/api/proxy/proxy.test.js +220 -0
  200. package/test/unit/api/proxy/utils.js +79 -0
  201. package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +21 -0
  202. package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +17 -0
  203. package/test/unit/api/transfers/data/postQuotesBody.json +52 -0
  204. package/test/unit/api/transfers/data/postTransfersBadBody.json +17 -0
  205. package/test/unit/api/transfers/data/postTransfersBody.json +24 -0
  206. package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +53 -0
  207. package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +47 -0
  208. package/test/unit/api/transfers/data/postTransfersSimpleBody.json +26 -0
  209. package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +101 -0
  210. package/test/unit/api/transfers/data/putPartiesBody.json +20 -0
  211. package/test/unit/api/transfers/data/putQuotesBody.json +37 -0
  212. package/test/unit/api/transfers/data/putTransfersBody.json +17 -0
  213. package/test/unit/api/transfers/transfers.test.js +191 -0
  214. package/test/unit/api/transfers/utils.js +183 -0
  215. package/test/unit/api/utils.js +75 -0
  216. package/test/unit/config.test.js +119 -0
  217. package/test/unit/data/commonHttpHeaders.json +6 -0
  218. package/test/unit/data/defaultConfig.json +58 -0
  219. package/test/unit/data/postQuotesBody.json +52 -0
  220. package/test/unit/data/putParticipantsBody.json +12 -0
  221. package/test/unit/data/putPartiesBody.json +20 -0
  222. package/test/unit/data/testFile.json +29 -0
  223. package/test/unit/data/testFile.yaml +14 -0
  224. package/test/unit/inboundApi/data/mockArguments.json +117 -0
  225. package/test/unit/inboundApi/data/mockTransactionRequest.json +42 -0
  226. package/test/unit/inboundApi/handlers.test.js +799 -0
  227. package/test/unit/index.test.js +55 -0
  228. package/test/unit/lib/cache.test.js +146 -0
  229. package/test/unit/lib/model/AccountsModel.test.js +121 -0
  230. package/test/unit/lib/model/AuthorizationsModel.test.js +460 -0
  231. package/test/unit/lib/model/InboundTransfersModel.test.js +628 -0
  232. package/test/unit/lib/model/OutboundBulkQuotesModel.test.js +249 -0
  233. package/test/unit/lib/model/OutboundBulkTransfersModel.test.js +244 -0
  234. package/test/unit/lib/model/OutboundRequestToPayModel.test.js +166 -0
  235. package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +245 -0
  236. package/test/unit/lib/model/OutboundTransfersModel.test.js +836 -0
  237. package/test/unit/lib/model/PartiesModel.test.js +468 -0
  238. package/test/unit/lib/model/QuotesModel.test.js +470 -0
  239. package/test/unit/lib/model/TransfersModel.test.js +474 -0
  240. package/test/unit/lib/model/common/PersistentStateMachine.test.js +179 -0
  241. package/test/unit/lib/model/data/authorizationsResponse.json +13 -0
  242. package/test/unit/lib/model/data/bulkQuoteRequest.json +27 -0
  243. package/test/unit/lib/model/data/bulkQuoteResponse.json +35 -0
  244. package/test/unit/lib/model/data/bulkTransferFulfil.json +13 -0
  245. package/test/unit/lib/model/data/bulkTransferRequest.json +29 -0
  246. package/test/unit/lib/model/data/defaultConfig.json +47 -0
  247. package/test/unit/lib/model/data/getBulkTransfersBackendResponse.json +42 -0
  248. package/test/unit/lib/model/data/getBulkTransfersMojaloopResponse.json +22 -0
  249. package/test/unit/lib/model/data/getTransfersBackendResponse.json +34 -0
  250. package/test/unit/lib/model/data/getTransfersMojaloopResponse.json +17 -0
  251. package/test/unit/lib/model/data/mockArguments.json +131 -0
  252. package/test/unit/lib/model/data/mockTxnRequestsArguments.json +63 -0
  253. package/test/unit/lib/model/data/notificationToPayee.json +10 -0
  254. package/test/unit/lib/model/data/payeeParty.json +16 -0
  255. package/test/unit/lib/model/data/putAuthorizationsResponse.json +10 -0
  256. package/test/unit/lib/model/data/putQuotesResponse.json +33 -0
  257. package/test/unit/lib/model/data/putTransfersResponse.json +5 -0
  258. package/test/unit/lib/model/data/quoteResponse.json +31 -0
  259. package/test/unit/lib/model/data/requestToPayRequest.json +20 -0
  260. package/test/unit/lib/model/data/requestToPayTransferRequest.json +27 -0
  261. package/test/unit/lib/model/data/transactionRequestResponse.json +18 -0
  262. package/test/unit/lib/model/data/transferFulfil.json +8 -0
  263. package/test/unit/lib/model/data/transferRequest.json +26 -0
  264. package/test/unit/lib/model/mockedLibRequests.js +74 -0
  265. package/test/unit/mockLogger.js +39 -0
  266. package/test/unit/outboundApi/data/bulkQuoteRequest.json +28 -0
  267. package/test/unit/outboundApi/data/bulkTransferRequest.json +28 -0
  268. package/test/unit/outboundApi/data/mockBulkQuoteError.json +45 -0
  269. package/test/unit/outboundApi/data/mockBulkTransferError.json +48 -0
  270. package/test/unit/outboundApi/data/mockError.json +41 -0
  271. package/test/unit/outboundApi/data/mockGetPartiesError.json +4 -0
  272. package/test/unit/outboundApi/data/mockRequestToPayError.json +32 -0
  273. package/test/unit/outboundApi/data/mockRequestToPayTransferError.json +39 -0
  274. package/test/unit/outboundApi/data/requestToPay.json +21 -0
  275. package/test/unit/outboundApi/data/requestToPayTransferRequest.json +20 -0
  276. package/test/unit/outboundApi/data/transferRequest.json +21 -0
  277. package/test/unit/outboundApi/handlers.test.js +986 -0
@@ -0,0 +1,166 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2019 - All rights reserved. *
3
+ * *
4
+ * This file is made available under the terms of the license agreement *
5
+ * specified in the corresponding source code repository. *
6
+ * *
7
+ * ORIGINAL AUTHOR: *
8
+ * Murthy Kakarlamudi - murthy@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ 'use strict';
12
+
13
+ // we use a mock standard components lib to intercept and mock certain funcs
14
+ jest.mock('@mojaloop/sdk-standard-components');
15
+ jest.mock('redis');
16
+
17
+ const Cache = require('../../../../lib/cache');
18
+ const Model = require('../../../../lib/model').OutboundRequestToPayModel;
19
+ const PartiesModel = require('../../../../lib/model').PartiesModel;
20
+
21
+ const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components');
22
+ const StateMachine = require('javascript-state-machine');
23
+
24
+ const defaultConfig = require('./data/defaultConfig');
25
+ const requestToPayRequest = require('./data/requestToPayRequest');
26
+ const payeeParty = require('./data/payeeParty');
27
+ const transactionRequestResponseTemplate = require('./data/transactionRequestResponse');
28
+
29
+ const genPartyId = (party) => {
30
+ const { partyIdType, partyIdentifier, partySubIdOrType } = party.party.partyIdInfo;
31
+ return PartiesModel.channelName({
32
+ type: partyIdType,
33
+ id: partyIdentifier,
34
+ subId: partySubIdOrType
35
+ });
36
+ };
37
+
38
+ // util function to simulate a party resolution subscription message on a cache client
39
+ const emitPartyCacheMessage = (cache, party) => cache.publish(genPartyId(party), JSON.stringify(party));
40
+
41
+ // util function to simulate a quote response subscription message on a cache client
42
+ const emitTransactionRequestResponseCacheMessage = (cache, transactionRequestId, transactionRequestResponse) => cache.publish(`txnreq_${transactionRequestId}`, JSON.stringify(transactionRequestResponse));
43
+
44
+ describe('outboundModel', () => {
45
+ let transactionRequestResponse;
46
+ let config;
47
+ let logger;
48
+ let cache;
49
+
50
+ /**
51
+ *
52
+ * @param {Object} opts
53
+ * @param {Number} opts.expirySeconds
54
+ * @param {Object} opts.delays
55
+ * @param {Number} delays.requestQuotes
56
+ * @param {Number} delays.prepareTransfer
57
+ * @param {Object} opts.rejects
58
+ * @param {boolean} rejects.quoteResponse
59
+ * @param {boolean} rejects.transferFulfils
60
+ */
61
+
62
+ beforeAll(async () => {
63
+ logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' });
64
+ transactionRequestResponse = JSON.parse(JSON.stringify(transactionRequestResponseTemplate));
65
+ });
66
+
67
+ beforeEach(async () => {
68
+ config = JSON.parse(JSON.stringify(defaultConfig));
69
+ MojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve());
70
+ MojaloopRequests.__getParties = jest.fn(() => Promise.resolve());
71
+ MojaloopRequests.__postTransactionRequests = jest.fn(() => Promise.resolve());
72
+
73
+ cache = new Cache({
74
+ host: 'dummycachehost',
75
+ port: 1234,
76
+ logger,
77
+ });
78
+ await cache.connect();
79
+ });
80
+
81
+ afterEach(async () => {
82
+ await cache.disconnect();
83
+ });
84
+
85
+ test('initializes to starting state', async () => {
86
+ const model = new Model({
87
+ cache,
88
+ logger,
89
+ ...config,
90
+ });
91
+
92
+ await model.initialize(JSON.parse(JSON.stringify(requestToPayRequest)));
93
+ expect(StateMachine.__instance.state).toBe('start');
94
+ });
95
+
96
+
97
+ test('executes all two stages without halting when AUTO_ACCEPT_PARTY is true', async () => {
98
+ config.autoAcceptParty = true;
99
+
100
+ MojaloopRequests.__getParties = jest.fn(() => {
101
+ emitPartyCacheMessage(cache, payeeParty);
102
+ return Promise.resolve();
103
+ });
104
+
105
+ MojaloopRequests.__postTransactionRequests = jest.fn((postTransactionRequestsBody) => {
106
+ // simulate a callback with the quote response
107
+ emitTransactionRequestResponseCacheMessage(cache, postTransactionRequestsBody.transactionRequestId, transactionRequestResponse);
108
+ return Promise.resolve();
109
+ });
110
+
111
+ const model = new Model({
112
+ cache,
113
+ logger,
114
+ ...config,
115
+ });
116
+
117
+ await model.initialize(JSON.parse(JSON.stringify(requestToPayRequest)));
118
+
119
+ expect(StateMachine.__instance.state).toBe('start');
120
+
121
+ // start the model running
122
+ const result = await model.run();
123
+
124
+ expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
125
+ expect(MojaloopRequests.__postTransactionRequests).toHaveBeenCalledTimes(1);
126
+
127
+ // check we stopped at payeeResolved state
128
+ expect(result.currentState).toBe('COMPLETED');
129
+ expect(result.requestToPayState).toBe('RECEIVED');
130
+ expect(StateMachine.__instance.state).toBe('succeeded');
131
+ });
132
+
133
+ test('resolves payee and halts when AUTO_ACCEPT_PARTY is false', async () => {
134
+ config.autoAcceptParty = false;
135
+
136
+ MojaloopRequests.__getParties = jest.fn(() => {
137
+ emitPartyCacheMessage(cache, payeeParty);
138
+ return Promise.resolve();
139
+ });
140
+
141
+ const model = new Model({
142
+ cache,
143
+ logger,
144
+ ...config,
145
+ });
146
+
147
+ await model.initialize(JSON.parse(JSON.stringify(requestToPayRequest)));
148
+
149
+ expect(StateMachine.__instance.state).toBe('start');
150
+
151
+ // start the model running
152
+ const resultPromise = model.run();
153
+
154
+ // now we started the model running we simulate a callback with the resolved party
155
+ emitPartyCacheMessage(cache, payeeParty);
156
+
157
+ // wait for the model to reach a terminal state
158
+ const result = await resultPromise;
159
+
160
+ // check we stopped at payeeResolved state
161
+ expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
162
+ expect(StateMachine.__instance.state).toBe('payeeResolved');
163
+ });
164
+
165
+
166
+ });
@@ -0,0 +1,245 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2019 - All rights reserved. *
3
+ * *
4
+ * This file is made available under the terms of the license agreement *
5
+ * specified in the corresponding source code repository. *
6
+ * *
7
+ * ORIGINAL AUTHOR: *
8
+ * Murthy Kakarlamudi - murthy@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ 'use strict';
12
+
13
+ // we use a mock standard components lib to intercept and mock certain funcs
14
+ jest.mock('@mojaloop/sdk-standard-components');
15
+ jest.mock('redis');
16
+
17
+ const Cache = require('../../../../lib/cache');
18
+ const Model = require('../../../../lib/model').OutboundRequestToPayTransferModel;
19
+
20
+ const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components');
21
+ const StateMachine = require('javascript-state-machine');
22
+
23
+ const defaultConfig = require('./data/defaultConfig');
24
+ const requestToPayTransferRequest = require('./data/requestToPayTransferRequest');
25
+ const quoteResponseTemplate = require('./data/quoteResponse');
26
+ const authorizationsResponse = require('./data/authorizationsResponse');
27
+ const transferFulfil = require('./data/transferFulfil');
28
+
29
+ // util function to simulate a quote response subscription message on a cache client
30
+ const emitQuoteResponseCacheMessage = (cache, quoteId, quoteResponse) => cache.publish(`qt_${quoteId}`, JSON.stringify(quoteResponse));
31
+
32
+ // util function to simulate a authorizations response subscription message on a cache client
33
+ const emitAuthorizationsResponseCacheMessage = (cache, authorizationsResponse) => cache.publish(`otp_${requestToPayTransferRequest.requestToPayTransactionId}`, JSON.stringify(authorizationsResponse));
34
+
35
+
36
+ // util function to simulate a transfer fulfilment subscription message on a cache client
37
+ const emitTransferFulfilCacheMessage = (cache, transferId, fulfil) => cache.publish(`tf_${transferId}`, JSON.stringify(fulfil));
38
+
39
+ describe('outboundRequestToPayTransferModel', () => {
40
+ let quoteResponse;
41
+ let config;
42
+ let logger;
43
+ let cache;
44
+
45
+ /**
46
+ *
47
+ * @param {Object} opts
48
+ * @param {Number} opts.expirySeconds
49
+ * @param {Object} opts.delays
50
+ * @param {Number} delays.requestQuotes
51
+ * @param {Number} delays.prepareTransfer
52
+ * @param {Object} opts.rejects
53
+ * @param {boolean} rejects.quoteResponse
54
+ * @param {boolean} rejects.transferFulfils
55
+ */
56
+
57
+
58
+ beforeAll(async () => {
59
+ logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' });
60
+ quoteResponse = JSON.parse(JSON.stringify(quoteResponseTemplate));
61
+ });
62
+
63
+ beforeEach(async () => {
64
+ config = JSON.parse(JSON.stringify(defaultConfig));
65
+ MojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve());
66
+ MojaloopRequests.__getParties = jest.fn(() => Promise.resolve());
67
+ MojaloopRequests.__getAuthorizations = jest.fn(() => Promise.resolve());
68
+ MojaloopRequests.__postQuotes = jest.fn(() => Promise.resolve());
69
+ MojaloopRequests.__putQuotes = jest.fn(() => Promise.resolve());
70
+ MojaloopRequests.__putQuotesError = jest.fn(() => Promise.resolve());
71
+ MojaloopRequests.__postTransfers = jest.fn(() => Promise.resolve());
72
+
73
+ cache = new Cache({
74
+ host: 'dummycachehost',
75
+ port: 1234,
76
+ logger,
77
+ });
78
+ await cache.connect();
79
+ });
80
+
81
+ afterEach(async () => {
82
+ await cache.disconnect();
83
+ });
84
+
85
+ test('initializes to starting state', async () => {
86
+ const model = new Model({
87
+ cache,
88
+ logger,
89
+ ...config,
90
+ });
91
+
92
+ await model.initialize(JSON.parse(JSON.stringify(requestToPayTransferRequest)));
93
+ expect(StateMachine.__instance.state).toBe('start');
94
+ });
95
+
96
+
97
+ test('executes all three transfer stages without halting when AUTO_ACCEPT_QUOTES and AUTO_ACCEPT_PARTY are true', async () => {
98
+ config.autoAcceptR2PDeviceOTP = true;
99
+ config.autoAcceptR2PDeviceQuotes = true;
100
+ config.autoAcceptQuotes = true;
101
+
102
+ MojaloopRequests.__getAuthorizations = jest.fn(() => {
103
+ emitAuthorizationsResponseCacheMessage(cache, authorizationsResponse);
104
+ return Promise.resolve();
105
+ });
106
+
107
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
108
+ // ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
109
+ // including extension list
110
+ const extensionList = postQuotesBody.extensionList.extension;
111
+ expect(extensionList).toBeTruthy();
112
+ expect(extensionList.length).toBe(2);
113
+ expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
114
+ expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
115
+
116
+ // simulate a callback with the quote response
117
+ emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
118
+ return Promise.resolve();
119
+ });
120
+
121
+ MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
122
+ //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
123
+ // set as the destination FSPID, picked up from the header's value `fspiop-source`
124
+ expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
125
+
126
+ const extensionList = postTransfersBody.extensionList.extension;
127
+ expect(extensionList).toBeTruthy();
128
+ expect(extensionList.length).toBe(2);
129
+ expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
130
+ expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
131
+
132
+ expect(destFspId).toBe(quoteResponse.headers['fspiop-source']);
133
+ expect(quoteResponse.headers['fspiop-source']).not.toBe(model.data.to.fspId);
134
+
135
+ // simulate a callback with the transfer fulfilment
136
+ emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
137
+ return Promise.resolve();
138
+ });
139
+
140
+ const model = new Model({
141
+ cache,
142
+ logger,
143
+ ...config,
144
+ });
145
+
146
+ await model.initialize(JSON.parse(JSON.stringify(requestToPayTransferRequest)));
147
+
148
+ expect(StateMachine.__instance.state).toBe('start');
149
+
150
+ // start the model running
151
+ const result = await model.run();
152
+
153
+ expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
154
+ expect(MojaloopRequests.__getAuthorizations).toHaveBeenCalledTimes(1);
155
+ expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
156
+
157
+ // check we stopped at payeeResolved state
158
+ expect(result.currentState).toBe('COMPLETED');
159
+ expect(StateMachine.__instance.state).toBe('succeeded');
160
+ });
161
+
162
+ // test('halts and resumes after quotes and otp stages when AUTO_ACCEPT_QUOTES is false and AUTO_ACCEPT_OTP is false', async () => {
163
+
164
+ // config.autoAcceptR2PDeviceOTP = false;
165
+ // config.autoAcceptR2PDeviceQuotes = false;
166
+
167
+ // let model = new Model({
168
+ // cache,
169
+ // logger,
170
+ // ...config,
171
+ // });
172
+
173
+ // await model.initialize(JSON.parse(JSON.stringify(requestToPayTransferRequest)));
174
+
175
+ // expect(StateMachine.__instance.state).toBe('start');
176
+
177
+ // // start the model running
178
+ // let resultPromise = model.run();
179
+
180
+ // // now we started the model running we simulate a callback with the quote response
181
+ // cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
182
+
183
+ // // wait for the model to reach a terminal state
184
+ // let result = await resultPromise;
185
+
186
+ // // check we stopped at quoteReceived state
187
+ // expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
188
+ // expect(StateMachine.__instance.state).toBe('quoteReceived');
189
+
190
+ // const requestToPayTransactionId = requestToPayTransferRequest.requestToPayTransactionId;
191
+
192
+ // // load a new model from the saved state
193
+ // model = new Model({
194
+ // cache,
195
+ // logger,
196
+ // ...config,
197
+ // });
198
+
199
+ // await model.load(requestToPayTransactionId);
200
+
201
+ // // check the model loaded to the correct state
202
+ // expect(StateMachine.__instance.state).toBe('quoteReceived');
203
+
204
+ // // now run the model again. this should trigger transition to quote request
205
+ // resultPromise = model.run();
206
+
207
+ // // now we started the model running we simulate a callback with the otp response
208
+ // cache.publish(`otp_${requestToPayTransactionId}`, JSON.stringify(authorizationsResponse));
209
+
210
+ // // wait for the model to reach a terminal state
211
+ // result = await resultPromise;
212
+
213
+ // // check we stopped at quoteReceived state
214
+ // expect(result.currentState).toBe('WAITING_FOR_OTP_ACCEPTANCE');
215
+ // expect(StateMachine.__instance.state).toBe('otpReceived');
216
+
217
+ // // load a new model from the saved state
218
+ // model = new Model({
219
+ // cache,
220
+ // logger,
221
+ // ...config,
222
+ // });
223
+
224
+ // await model.load(requestToPayTransactionId);
225
+
226
+ // // check the model loaded to the correct state
227
+ // expect(StateMachine.__instance.state).toBe('otpReceived');
228
+
229
+ // // now run the model again. this should trigger transition to quote request
230
+ // resultPromise = model.run();
231
+
232
+ // // now we started the model running we simulate a callback with the transfer fulfilment
233
+ // cache.publish(`tf_${model.data.transferId}`, JSON.stringify(transferFulfil));
234
+
235
+ // // wait for the model to reach a terminal state
236
+ // result = await resultPromise;
237
+
238
+ // // check we stopped at quoteReceived state
239
+ // expect(result.currentState).toBe('COMPLETED');
240
+ // expect(StateMachine.__instance.state).toBe('succeeded');
241
+
242
+ // });
243
+
244
+
245
+ });
@@ -0,0 +1,836 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2019 - All rights reserved. *
3
+ * *
4
+ * This file is made available under the terms of the license agreement *
5
+ * specified in the corresponding source code repository. *
6
+ * *
7
+ * ORIGINAL AUTHOR: *
8
+ * James Bush - james.bush@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ 'use strict';
12
+
13
+ // we use a mock standard components lib to intercept and mock certain funcs
14
+ jest.mock('@mojaloop/sdk-standard-components');
15
+ jest.mock('redis');
16
+
17
+ const Cache = require('../../../../lib/cache');
18
+ const Model = require('../../../../lib/model').OutboundTransfersModel;
19
+ const PartiesModel = require('../../../../lib/model').PartiesModel;
20
+
21
+ const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components');
22
+ const StateMachine = require('javascript-state-machine');
23
+
24
+ const defaultConfig = require('./data/defaultConfig');
25
+ const transferRequest = require('./data/transferRequest');
26
+ const payeeParty = require('./data/payeeParty');
27
+ const quoteResponseTemplate = require('./data/quoteResponse');
28
+ const transferFulfil = require('./data/transferFulfil');
29
+
30
+ const genPartyId = (party) => {
31
+ const { partyIdType, partyIdentifier, partySubIdOrType } = party.party.partyIdInfo;
32
+ return PartiesModel.channelName({
33
+ type: partyIdType,
34
+ id: partyIdentifier,
35
+ subId: partySubIdOrType
36
+ });
37
+ };
38
+
39
+ // util function to simulate a party resolution subscription message on a cache client
40
+ const emitPartyCacheMessage = (cache, party) => cache.publish(genPartyId(party), JSON.stringify(party));
41
+
42
+ // util function to simulate a quote response subscription message on a cache client
43
+ const emitQuoteResponseCacheMessage = (cache, quoteId, quoteResponse) => cache.publish(`qt_${quoteId}`, JSON.stringify(quoteResponse));
44
+
45
+ // util function to simulate a transfer fulfilment subscription message on a cache client
46
+ const emitTransferFulfilCacheMessage = (cache, transferId, fulfil) => cache.publish(`tf_${transferId}`, JSON.stringify(fulfil));
47
+
48
+ describe('outboundModel', () => {
49
+ let quoteResponse;
50
+ let config;
51
+ let logger;
52
+ let cache;
53
+
54
+ /**
55
+ *
56
+ * @param {Object} opts
57
+ * @param {Number} opts.expirySeconds
58
+ * @param {Object} opts.delays
59
+ * @param {Number} delays.requestQuotes
60
+ * @param {Number} delays.prepareTransfer
61
+ * @param {Object} opts.rejects
62
+ * @param {boolean} rejects.quoteResponse
63
+ * @param {boolean} rejects.transferFulfils
64
+ */
65
+ async function testTransferWithDelay({expirySeconds, delays, rejects}) {
66
+ const config = JSON.parse(JSON.stringify(defaultConfig));
67
+ config.autoAcceptParty = true;
68
+ config.autoAcceptQuotes = true;
69
+ config.expirySeconds = expirySeconds;
70
+ config.rejectExpiredQuoteResponses = rejects.quoteResponse;
71
+ config.rejectExpiredTransferFulfils = rejects.transferFulfils;
72
+
73
+ // simulate a callback with the resolved party
74
+ MojaloopRequests.__getParties = jest.fn(() => emitPartyCacheMessage(cache, payeeParty));
75
+
76
+ // simulate a delayed callback with the quote response
77
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
78
+ setTimeout(() => {
79
+ emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
80
+ }, delays.requestQuotes ? delays.requestQuotes * 1000 : 0);
81
+ });
82
+
83
+ // simulate a delayed callback with the transfer fulfilment
84
+ MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
85
+ setTimeout(() => {
86
+ emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
87
+ }, delays.prepareTransfer ? delays.prepareTransfer * 1000 : 0);
88
+ });
89
+
90
+ const model = new Model({
91
+ ...config,
92
+ cache,
93
+ logger,
94
+ });
95
+
96
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
97
+
98
+ let expectError;
99
+ if (rejects.quoteResponse && delays.requestQuotes && expirySeconds < delays.requestQuotes) {
100
+ expectError = 'Quote response missed expiry deadline';
101
+ }
102
+ if (rejects.transferFulfils && delays.prepareTransfer && expirySeconds < delays.prepareTransfer) {
103
+ expectError = 'Transfer fulfil missed expiry deadline';
104
+ }
105
+ if (expectError) {
106
+ await expect(model.run()).rejects.toThrowError(expectError);
107
+ } else {
108
+ const result = await model.run();
109
+ await expect(result.currentState).toBe('COMPLETED');
110
+ }
111
+ }
112
+
113
+ beforeAll(async () => {
114
+ logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' });
115
+ quoteResponse = JSON.parse(JSON.stringify(quoteResponseTemplate));
116
+ });
117
+
118
+ beforeEach(async () => {
119
+ config = JSON.parse(JSON.stringify(defaultConfig));
120
+ MojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve());
121
+ MojaloopRequests.__getParties = jest.fn(() => Promise.resolve());
122
+ MojaloopRequests.__postQuotes = jest.fn(() => Promise.resolve());
123
+ MojaloopRequests.__putQuotes = jest.fn(() => Promise.resolve());
124
+ MojaloopRequests.__putQuotesError = jest.fn(() => Promise.resolve());
125
+ MojaloopRequests.__postTransfers = jest.fn(() => Promise.resolve());
126
+
127
+ cache = new Cache({
128
+ host: 'dummycachehost',
129
+ port: 1234,
130
+ logger,
131
+ });
132
+ await cache.connect();
133
+ });
134
+
135
+ afterEach(async () => {
136
+ await cache.disconnect();
137
+ });
138
+
139
+ test('initializes to starting state', async () => {
140
+ const model = new Model({
141
+ cache,
142
+ logger,
143
+ ...config,
144
+ });
145
+
146
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
147
+ expect(StateMachine.__instance.state).toBe('start');
148
+ });
149
+
150
+
151
+ test('executes all three transfer stages without halting when AUTO_ACCEPT_PARTY and AUTO_ACCEPT_QUOTES are true', async () => {
152
+ config.autoAcceptParty = true;
153
+ config.autoAcceptQuotes = true;
154
+
155
+ MojaloopRequests.__getParties = jest.fn(() => {
156
+ emitPartyCacheMessage(cache, payeeParty);
157
+ return Promise.resolve();
158
+ });
159
+
160
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
161
+ // ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
162
+ // including extension list
163
+ const extensionList = postQuotesBody.extensionList.extension;
164
+ expect(extensionList).toBeTruthy();
165
+ expect(extensionList.length).toBe(2);
166
+ expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
167
+ expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
168
+
169
+ // simulate a callback with the quote response
170
+ emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
171
+ return Promise.resolve();
172
+ });
173
+
174
+ MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
175
+ //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
176
+ // set as the destination FSPID, picked up from the header's value `fspiop-source`
177
+ expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
178
+
179
+ const extensionList = postTransfersBody.extensionList.extension;
180
+ expect(extensionList).toBeTruthy();
181
+ expect(extensionList.length).toBe(2);
182
+ expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
183
+ expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
184
+
185
+ expect(destFspId).toBe(quoteResponse.headers['fspiop-source']);
186
+ expect(model.data.to.fspId).toBe(payeeParty.party.partyIdInfo.fspId);
187
+ expect(quoteResponse.headers['fspiop-source']).not.toBe(model.data.to.fspId);
188
+
189
+ // simulate a callback with the transfer fulfilment
190
+ emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
191
+ return Promise.resolve();
192
+ });
193
+
194
+ const model = new Model({
195
+ cache,
196
+ logger,
197
+ ...config,
198
+ });
199
+
200
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
201
+
202
+ expect(StateMachine.__instance.state).toBe('start');
203
+
204
+ // start the model running
205
+ const result = await model.run();
206
+
207
+ expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
208
+ expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
209
+ expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
210
+
211
+ // check we stopped at payeeResolved state
212
+ expect(result.currentState).toBe('COMPLETED');
213
+ expect(StateMachine.__instance.state).toBe('succeeded');
214
+ });
215
+
216
+
217
+ test('uses quote response transfer amount for transfer prepare', async () => {
218
+ config.autoAcceptParty = true;
219
+ config.autoAcceptQuotes = true;
220
+
221
+ MojaloopRequests.__getParties = jest.fn(() => {
222
+ emitPartyCacheMessage(cache, payeeParty);
223
+ return Promise.resolve();
224
+ });
225
+
226
+ // change the the transfer amount and currency in the quote response
227
+ // so it is different to the initial request
228
+ quoteResponse.data.transferAmount = {
229
+ currency: 'XYZ',
230
+ amount: '9876543210'
231
+ };
232
+
233
+ expect(quoteResponse.data.transferAmount).not.toEqual({
234
+ amount: transferRequest.amount,
235
+ currency: transferRequest.currency
236
+ });
237
+
238
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
239
+ // ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
240
+ // including extension list
241
+ const extensionList = postQuotesBody.extensionList.extension;
242
+ expect(extensionList).toBeTruthy();
243
+ expect(extensionList.length).toBe(2);
244
+ expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
245
+ expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
246
+
247
+ // simulate a callback with the quote response
248
+ emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
249
+ return Promise.resolve();
250
+ });
251
+
252
+ MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
253
+ //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
254
+ // set as the destination FSPID, picked up from the header's value `fspiop-source`
255
+ expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
256
+
257
+ const extensionList = postTransfersBody.extensionList.extension;
258
+ expect(extensionList).toBeTruthy();
259
+ expect(extensionList.length).toBe(2);
260
+ expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
261
+ expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
262
+
263
+ expect(destFspId).toBe(quoteResponse.headers['fspiop-source']);
264
+ expect(model.data.to.fspId).toBe(payeeParty.party.partyIdInfo.fspId);
265
+ expect(quoteResponse.headers['fspiop-source']).not.toBe(model.data.to.fspId);
266
+
267
+ expect(postTransfersBody.amount).toEqual(quoteResponse.data.transferAmount);
268
+
269
+ // simulate a callback with the transfer fulfilment
270
+ emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
271
+ return Promise.resolve();
272
+ });
273
+
274
+ const model = new Model({
275
+ cache,
276
+ logger,
277
+ ...config,
278
+ });
279
+
280
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
281
+
282
+ expect(StateMachine.__instance.state).toBe('start');
283
+
284
+ // start the model running
285
+ const result = await model.run();
286
+
287
+ expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
288
+ expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
289
+ expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
290
+
291
+ // check we stopped at payeeResolved state
292
+ expect(result.currentState).toBe('COMPLETED');
293
+ expect(StateMachine.__instance.state).toBe('succeeded');
294
+ });
295
+
296
+
297
+ test('test get transfer', async () => {
298
+ MojaloopRequests.__getTransfers = jest.fn((transferId) => {
299
+ emitTransferFulfilCacheMessage(cache, transferId, transferFulfil);
300
+ return Promise.resolve();
301
+ });
302
+
303
+ const model = new Model({
304
+ cache,
305
+ logger,
306
+ ...config,
307
+ });
308
+
309
+ const TRANSFER_ID = 'tx-id000011';
310
+
311
+ await model.initialize(JSON.parse(JSON.stringify({
312
+ ...transferRequest,
313
+ currentState: 'getTransfer',
314
+ transferId: TRANSFER_ID,
315
+ })));
316
+
317
+ expect(StateMachine.__instance.state).toBe('getTransfer');
318
+
319
+ // start the model running
320
+ const result = await model.run();
321
+
322
+ expect(MojaloopRequests.__getTransfers).toHaveBeenCalledTimes(1);
323
+
324
+ // check we stopped at payeeResolved state
325
+ expect(result.currentState).toBe('COMPLETED');
326
+ expect(StateMachine.__instance.state).toBe('succeeded');
327
+ });
328
+
329
+
330
+ test('resolves payee and halts when AUTO_ACCEPT_PARTY is false', async () => {
331
+ config.autoAcceptParty = false;
332
+
333
+ const model = new Model({
334
+ cache,
335
+ logger,
336
+ ...config,
337
+ });
338
+
339
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
340
+
341
+ expect(StateMachine.__instance.state).toBe('start');
342
+
343
+ // start the model running
344
+ const resultPromise = model.run();
345
+
346
+ // now we started the model running we simulate a callback with the resolved party
347
+ emitPartyCacheMessage(cache, payeeParty);
348
+
349
+ // wait for the model to reach a terminal state
350
+ const result = await resultPromise;
351
+
352
+ // check we stopped at payeeResolved state
353
+ expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
354
+ expect(StateMachine.__instance.state).toBe('payeeResolved');
355
+ });
356
+
357
+
358
+ test('halts after resolving payee, resumes and then halts after receiving quote response when AUTO_ACCEPT_PARTY is false and AUTO_ACCEPT_QUOTES is false', async () => {
359
+ config.autoAcceptParty = false;
360
+ config.autoAcceptQuotes = false;
361
+
362
+ let model = new Model({
363
+ cache,
364
+ logger,
365
+ ...config,
366
+ });
367
+
368
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
369
+
370
+ expect(StateMachine.__instance.state).toBe('start');
371
+
372
+ // start the model running
373
+ let resultPromise = model.run();
374
+
375
+ // now we started the model running we simulate a callback with the resolved party
376
+ emitPartyCacheMessage(cache, payeeParty);
377
+
378
+ // wait for the model to reach a terminal state
379
+ let result = await resultPromise;
380
+
381
+ // check we stopped at payeeResolved state
382
+ expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
383
+ expect(StateMachine.__instance.state).toBe('payeeResolved');
384
+
385
+ const transferId = result.transferId;
386
+
387
+ // load a new model from the saved state
388
+ model = new Model({
389
+ cache,
390
+ logger,
391
+ ...config,
392
+ });
393
+
394
+ await model.load(transferId);
395
+
396
+ // check the model loaded to the correct state
397
+ expect(StateMachine.__instance.state).toBe('payeeResolved');
398
+
399
+ // now run the model again. this should trigger transition to quote request
400
+ resultPromise = model.run();
401
+
402
+ // now we started the model running we simulate a callback with the quote response
403
+ cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
404
+
405
+ // wait for the model to reach a terminal state
406
+ result = await resultPromise;
407
+
408
+ // check we stopped at payeeResolved state
409
+ expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
410
+ expect(StateMachine.__instance.state).toBe('quoteReceived');
411
+ });
412
+
413
+
414
+ test('halts and resumes after parties and quotes stages when AUTO_ACCEPT_PARTY is false and AUTO_ACCEPT_QUOTES is false', async () => {
415
+ config.autoAcceptParty = false;
416
+ config.autoAcceptQuotes = false;
417
+
418
+ let model = new Model({
419
+ cache,
420
+ logger,
421
+ ...config,
422
+ });
423
+
424
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
425
+
426
+ expect(StateMachine.__instance.state).toBe('start');
427
+
428
+ // start the model running
429
+ let resultPromise = model.run();
430
+
431
+ // now we started the model running we simulate a callback with the resolved party
432
+ emitPartyCacheMessage(cache, payeeParty);
433
+
434
+ // wait for the model to reach a terminal state
435
+ let result = await resultPromise;
436
+
437
+ // check we stopped at payeeResolved state
438
+ expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
439
+ expect(StateMachine.__instance.state).toBe('payeeResolved');
440
+
441
+ const transferId = result.transferId;
442
+
443
+ // load a new model from the saved state
444
+ model = new Model({
445
+ cache,
446
+ logger,
447
+ ...config,
448
+ });
449
+
450
+ await model.load(transferId);
451
+
452
+ // check the model loaded to the correct state
453
+ expect(StateMachine.__instance.state).toBe('payeeResolved');
454
+
455
+ // now run the model again. this should trigger transition to quote request
456
+ resultPromise = model.run();
457
+
458
+ // now we started the model running we simulate a callback with the quote response
459
+ cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
460
+
461
+ // wait for the model to reach a terminal state
462
+ result = await resultPromise;
463
+
464
+ // check we stopped at quoteReceived state
465
+ expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
466
+ expect(StateMachine.__instance.state).toBe('quoteReceived');
467
+
468
+ // load a new model from the saved state
469
+ model = new Model({
470
+ cache,
471
+ logger,
472
+ ...config,
473
+ });
474
+
475
+ await model.load(transferId);
476
+
477
+ // check the model loaded to the correct state
478
+ expect(StateMachine.__instance.state).toBe('quoteReceived');
479
+
480
+ // now run the model again. this should trigger transition to quote request
481
+ resultPromise = model.run();
482
+
483
+ // now we started the model running we simulate a callback with the transfer fulfilment
484
+ cache.publish(`tf_${model.data.transferId}`, JSON.stringify(transferFulfil));
485
+
486
+ // wait for the model to reach a terminal state
487
+ result = await resultPromise;
488
+
489
+ // check we stopped at quoteReceived state
490
+ expect(result.currentState).toBe('COMPLETED');
491
+ expect(StateMachine.__instance.state).toBe('succeeded');
492
+ });
493
+
494
+ test('uses payee party fspid for transfer prepare when config USE_QUOTE_SOURCE_FSP_AS_TRANSFER_PAYEE_FSP is false', async () => {
495
+ config.autoAcceptParty = true;
496
+ config.autoAcceptQuotes = true;
497
+ config.useQuoteSourceFSPAsTransferPayeeFSP = false;
498
+
499
+ MojaloopRequests.__getParties = jest.fn(() => {
500
+ // simulate a callback with the resolved party
501
+ emitPartyCacheMessage(cache, payeeParty);
502
+ return Promise.resolve();
503
+ });
504
+
505
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
506
+ // simulate a callback with the quote response
507
+ emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
508
+ return Promise.resolve();
509
+ });
510
+
511
+ MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
512
+ //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
513
+ // set as the destination FSPID, picked up from the header's value `fspiop-source`
514
+ expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
515
+ expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
516
+ const payeeFsp = MojaloopRequests.__postTransfers.mock.calls[0][0].payeeFsp;
517
+ expect(payeeFsp).toEqual(payeeParty.party.partyIdInfo.fspId);
518
+
519
+ // simulate a callback with the transfer fulfilment
520
+ emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
521
+ return Promise.resolve();
522
+ });
523
+
524
+ const model = new Model({
525
+ cache,
526
+ logger,
527
+ ...config,
528
+ });
529
+
530
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
531
+
532
+ expect(StateMachine.__instance.state).toBe('start');
533
+
534
+ // start the model running
535
+ const resultPromise = model.run();
536
+
537
+ // wait for the model to reach a terminal state
538
+ const result = await resultPromise;
539
+
540
+ // check we stopped at payeeResolved state
541
+ expect(result.currentState).toBe('COMPLETED');
542
+ expect(StateMachine.__instance.state).toBe('succeeded');
543
+ });
544
+
545
+ test('uses quote response source fspid for transfer prepare when config USE_QUOTE_SOURCE_FSP_AS_TRANSFER_PAYEE_FSP is true', async () => {
546
+ config.autoAcceptParty = true;
547
+ config.autoAcceptQuotes = true;
548
+ config.useQuoteSourceFSPAsTransferPayeeFSP = true;
549
+
550
+ MojaloopRequests.__getParties = jest.fn(() => {
551
+ // simulate a callback with the resolved party
552
+ emitPartyCacheMessage(cache, payeeParty);
553
+ return Promise.resolve();
554
+ });
555
+
556
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
557
+ // simulate a callback with the quote response
558
+ emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
559
+ return Promise.resolve();
560
+ });
561
+
562
+ MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
563
+ //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
564
+ // set as the destination FSPID, picked up from the header's value `fspiop-source`
565
+ expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
566
+ expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
567
+ const payeeFsp = MojaloopRequests.__postTransfers.mock.calls[0][0].payeeFsp;
568
+ expect(payeeFsp).toEqual(quoteResponse.headers['fspiop-source']);
569
+
570
+ // simulate a callback with the transfer fulfilment
571
+ emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
572
+ return Promise.resolve();
573
+ });
574
+
575
+ const model = new Model({
576
+ cache,
577
+ logger,
578
+ ...config,
579
+ });
580
+
581
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
582
+
583
+ expect(StateMachine.__instance.state).toBe('start');
584
+
585
+ // start the model running
586
+ const resultPromise = model.run();
587
+
588
+ // wait for the model to reach a terminal state
589
+ const result = await resultPromise;
590
+
591
+ // check we stopped at payeeResolved state
592
+ expect(result.currentState).toBe('COMPLETED');
593
+ expect(StateMachine.__instance.state).toBe('succeeded');
594
+ });
595
+
596
+ test('pass quote response `expiration` deadline', () =>
597
+ testTransferWithDelay({
598
+ expirySeconds: 2,
599
+ delays: {
600
+ requestQuotes: 1,
601
+ },
602
+ rejects: {
603
+ quoteResponse: true,
604
+ }
605
+ })
606
+ );
607
+
608
+ test('pass transfer fulfills `expiration` deadline', () =>
609
+ testTransferWithDelay({
610
+ expirySeconds: 2,
611
+ delays: {
612
+ prepareTransfer: 1,
613
+ },
614
+ rejects: {
615
+ transferFulfils: true,
616
+ }
617
+ })
618
+ );
619
+
620
+ test('pass all stages `expiration` deadlines', () =>
621
+ testTransferWithDelay({
622
+ expirySeconds: 2,
623
+ delays: {
624
+ requestQuotes: 1,
625
+ prepareTransfer: 1,
626
+ },
627
+ rejects: {
628
+ quoteResponse: true,
629
+ transferFulfils: true,
630
+ }
631
+ })
632
+ );
633
+
634
+ test('fail on quote response `expiration` deadline', () =>
635
+ testTransferWithDelay({
636
+ expirySeconds: 1,
637
+ delays: {
638
+ requestQuotes: 2,
639
+ },
640
+ rejects: {
641
+ quoteResponse: true,
642
+ }
643
+ })
644
+ );
645
+
646
+ test('fail on transfer fulfills `expiration` deadline', () =>
647
+ testTransferWithDelay({
648
+ expirySeconds: 1,
649
+ delays: {
650
+ prepareTransfer: 2,
651
+ },
652
+ rejects: {
653
+ transferFulfils: true,
654
+ }
655
+ })
656
+ );
657
+
658
+ test('Throws with mojaloop error in response body when party resolution error callback occurs', async () => {
659
+ config.autoAcceptParty = true;
660
+ config.autoAcceptQuotes = true;
661
+
662
+ MojaloopRequests.__getParties = jest.fn(() => {
663
+ // simulate a callback with the resolved party
664
+ cache.publish(genPartyId(payeeParty), JSON.stringify(expectError));
665
+ return Promise.resolve();
666
+ });
667
+
668
+ const model = new Model({
669
+ cache,
670
+ logger,
671
+ ...config,
672
+ });
673
+
674
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
675
+
676
+ expect(StateMachine.__instance.state).toBe('start');
677
+
678
+ const expectError = {
679
+ errorInformation: {
680
+ errorCode: '3204',
681
+ errorDescription: 'Party not found'
682
+ }
683
+ };
684
+
685
+ const errMsg = 'Got an error response resolving party: { errorInformation: { errorCode: \'3204\', errorDescription: \'Party not found\' } }';
686
+
687
+ try {
688
+ await model.run();
689
+ }
690
+ catch(err) {
691
+ expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
692
+ expect(err.transferState).toBeTruthy();
693
+ expect(err.transferState.lastError).toBeTruthy();
694
+ expect(err.transferState.lastError.mojaloopError).toEqual(expectError);
695
+ expect(err.transferState.lastError.transferState).toBe(undefined);
696
+ return;
697
+ }
698
+
699
+ throw new Error('Outbound model should have thrown');
700
+ });
701
+
702
+
703
+ test('Throws with mojaloop error in response body when quote request error callback occurs', async () => {
704
+ config.autoAcceptParty = true;
705
+ config.autoAcceptQuotes = true;
706
+
707
+ const expectError = {
708
+ type: 'quoteResponseError',
709
+ data: {
710
+ errorInformation: {
711
+ errorCode: '3205',
712
+ errorDescription: 'Quote ID not found'
713
+ }
714
+ }
715
+ };
716
+
717
+
718
+ MojaloopRequests.__getParties = jest.fn(() => {
719
+ // simulate a callback with the resolved party
720
+ emitPartyCacheMessage(cache, payeeParty);
721
+ return Promise.resolve();
722
+ });
723
+
724
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
725
+ // simulate a callback with the quote response
726
+ cache.publish(`qt_${postQuotesBody.quoteId}`, JSON.stringify(expectError));
727
+ return Promise.resolve();
728
+ });
729
+
730
+ const model = new Model({
731
+ cache,
732
+ logger,
733
+ ...config,
734
+ });
735
+
736
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
737
+
738
+ expect(StateMachine.__instance.state).toBe('start');
739
+
740
+ const errMsg = 'Got an error response requesting quote: { errorInformation:\n { errorCode: \'3205\', errorDescription: \'Quote ID not found\' } }';
741
+
742
+ try {
743
+ await model.run();
744
+ }
745
+ catch(err) {
746
+ expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
747
+ expect(err.transferState).toBeTruthy();
748
+ expect(err.transferState.lastError).toBeTruthy();
749
+ expect(err.transferState.lastError.mojaloopError).toEqual(expectError.data);
750
+ expect(err.transferState.lastError.transferState).toBe(undefined);
751
+ return;
752
+ }
753
+
754
+ throw new Error('Outbound model should have thrown');
755
+ });
756
+
757
+
758
+ test('Throws with mojaloop error in response body when transfer request error callback occurs', async () => {
759
+ config.autoAcceptParty = true;
760
+ config.autoAcceptQuotes = true;
761
+
762
+ const expectError = {
763
+ type: 'transferError',
764
+ data: {
765
+ errorInformation: {
766
+ errorCode: '4001',
767
+ errorDescription: 'Payer FSP insufficient liquidity'
768
+ }
769
+ }
770
+ };
771
+
772
+ MojaloopRequests.__getParties = jest.fn(() => {
773
+ // simulate a callback with the resolved party
774
+ emitPartyCacheMessage(cache, payeeParty);
775
+ return Promise.resolve();
776
+ });
777
+
778
+ MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
779
+ // simulate a callback with the quote response
780
+ emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
781
+ return Promise.resolve();
782
+ });
783
+
784
+ MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
785
+ // simulate an error callback with the transfer fulfilment
786
+ cache.publish(`tf_${postTransfersBody.transferId}`, JSON.stringify(expectError));
787
+ return Promise.resolve();
788
+ });
789
+
790
+ const model = new Model({
791
+ cache,
792
+ logger,
793
+ ...config,
794
+ });
795
+
796
+ await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
797
+
798
+ expect(StateMachine.__instance.state).toBe('start');
799
+
800
+ const errMsg = 'Got an error response preparing transfer: { errorInformation:\n { errorCode: \'4001\',\n errorDescription: \'Payer FSP insufficient liquidity\' } }';
801
+
802
+ try {
803
+ await model.run();
804
+ }
805
+ catch(err) {
806
+ expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
807
+ expect(err.transferState).toBeTruthy();
808
+ expect(err.transferState.lastError).toBeTruthy();
809
+ expect(err.transferState.lastError.mojaloopError).toEqual(expectError.data);
810
+ expect(err.transferState.lastError.transferState).toBe(undefined);
811
+ return;
812
+ }
813
+
814
+ throw new Error('Outbound model should have thrown');
815
+ });
816
+
817
+
818
+ async function testTlsServer(enableTls) {
819
+ config.tls.enabled = enableTls;
820
+
821
+ new Model({
822
+ cache,
823
+ logger,
824
+ ...config
825
+ });
826
+
827
+ const scheme = enableTls ? 'https' : 'http';
828
+ expect(MojaloopRequests.__instance.transportScheme).toBe(scheme);
829
+ }
830
+
831
+ test('Outbound server should use HTTPS if outbound mTLS enabled', () =>
832
+ testTlsServer(true));
833
+
834
+ test('Outbound server should use HTTP if outbound mTLS disabled', () =>
835
+ testTlsServer(false));
836
+ });