@mojaloop/sdk-scheme-adapter 18.0.0 → 18.0.2

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 (119) hide show
  1. package/.dockerignore +18 -0
  2. package/.eslintignore +2 -0
  3. package/.ncurc.yaml +7 -0
  4. package/CHANGELOG.md +20 -0
  5. package/CODEOWNERS +31 -1
  6. package/README.md +1 -0
  7. package/audit-resolve.json +35 -0
  8. package/package.json +13 -13
  9. package/test/__mocks__/@mojaloop/sdk-standard-components.js +0 -151
  10. package/test/__mocks__/javascript-state-machine.js +0 -21
  11. package/test/__mocks__/redis.js +0 -78
  12. package/test/__mocks__/uuidv4.js +0 -16
  13. package/test/config/integration.env +0 -146
  14. package/test/integration/lib/Outbound/data/quotesPostRequest.json +0 -52
  15. package/test/integration/lib/Outbound/data/transfersPostRequest.json +0 -24
  16. package/test/integration/lib/Outbound/parties.test.js +0 -31
  17. package/test/integration/lib/Outbound/quotes.test.js +0 -62
  18. package/test/integration/lib/Outbound/simpleTransfers.test.js +0 -70
  19. package/test/integration/lib/cache.test.js +0 -79
  20. package/test/integration/testEnv.js +0 -4
  21. package/test/unit/ControlClient.test.js +0 -69
  22. package/test/unit/ControlServer/events.js +0 -41
  23. package/test/unit/ControlServer/index.js +0 -227
  24. package/test/unit/ControlServer.test.js +0 -66
  25. package/test/unit/InboundServer.test.js +0 -443
  26. package/test/unit/TestServer.test.js +0 -392
  27. package/test/unit/api/accounts/accounts.test.js +0 -128
  28. package/test/unit/api/accounts/data/postAccountsBody.json +0 -7
  29. package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +0 -33
  30. package/test/unit/api/accounts/data/postAccountsErrorTimeoutResponse.json +0 -19
  31. package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +0 -31
  32. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +0 -34
  33. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +0 -39
  34. package/test/unit/api/accounts/utils.js +0 -79
  35. package/test/unit/api/proxy/data/proxyConfig.yaml +0 -82
  36. package/test/unit/api/proxy/data/requestBody.json +0 -22
  37. package/test/unit/api/proxy/data/requestHeaders.json +0 -5
  38. package/test/unit/api/proxy/data/requestQuery.json +0 -6
  39. package/test/unit/api/proxy/data/responseBody.json +0 -21
  40. package/test/unit/api/proxy/data/responseHeaders.json +0 -5
  41. package/test/unit/api/proxy/proxy.test.js +0 -220
  42. package/test/unit/api/proxy/utils.js +0 -79
  43. package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +0 -24
  44. package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +0 -18
  45. package/test/unit/api/transfers/data/postQuotesBody.json +0 -52
  46. package/test/unit/api/transfers/data/postTransfersBadBody.json +0 -17
  47. package/test/unit/api/transfers/data/postTransfersBody.json +0 -24
  48. package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +0 -62
  49. package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +0 -48
  50. package/test/unit/api/transfers/data/postTransfersSimpleBody.json +0 -26
  51. package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +0 -128
  52. package/test/unit/api/transfers/data/putPartiesBody.json +0 -20
  53. package/test/unit/api/transfers/data/putQuotesBody.json +0 -37
  54. package/test/unit/api/transfers/data/putTransfersBody.json +0 -17
  55. package/test/unit/api/transfers/transfers.test.js +0 -191
  56. package/test/unit/api/transfers/utils.js +0 -264
  57. package/test/unit/api/utils.js +0 -86
  58. package/test/unit/config.test.js +0 -119
  59. package/test/unit/data/commonHttpHeaders.json +0 -7
  60. package/test/unit/data/defaultConfig.json +0 -70
  61. package/test/unit/data/postQuotesBody.json +0 -52
  62. package/test/unit/data/putParticipantsBody.json +0 -12
  63. package/test/unit/data/putPartiesBody.json +0 -20
  64. package/test/unit/data/testFile.json +0 -29
  65. package/test/unit/data/testFile.yaml +0 -14
  66. package/test/unit/inboundApi/data/mockArguments.json +0 -117
  67. package/test/unit/inboundApi/data/mockTransactionRequest.json +0 -42
  68. package/test/unit/inboundApi/handlers.test.js +0 -786
  69. package/test/unit/index.test.js +0 -88
  70. package/test/unit/lib/cache.test.js +0 -145
  71. package/test/unit/lib/model/AccountsModel.test.js +0 -124
  72. package/test/unit/lib/model/InboundTransfersModel.test.js +0 -889
  73. package/test/unit/lib/model/OutboundBulkQuotesModel.test.js +0 -253
  74. package/test/unit/lib/model/OutboundBulkTransfersModel.test.js +0 -247
  75. package/test/unit/lib/model/OutboundRequestToPayModel.test.js +0 -166
  76. package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +0 -245
  77. package/test/unit/lib/model/OutboundTransfersModel.test.js +0 -1579
  78. package/test/unit/lib/model/PartiesModel.test.js +0 -478
  79. package/test/unit/lib/model/QuotesModel.test.js +0 -477
  80. package/test/unit/lib/model/TransfersModel.test.js +0 -481
  81. package/test/unit/lib/model/common/PersistentStateMachine.test.js +0 -178
  82. package/test/unit/lib/model/data/authorizationsResponse.json +0 -13
  83. package/test/unit/lib/model/data/bulkQuoteRequest.json +0 -27
  84. package/test/unit/lib/model/data/bulkQuoteResponse.json +0 -35
  85. package/test/unit/lib/model/data/bulkTransferFulfil.json +0 -13
  86. package/test/unit/lib/model/data/bulkTransferRequest.json +0 -29
  87. package/test/unit/lib/model/data/defaultConfig.json +0 -59
  88. package/test/unit/lib/model/data/getBulkTransfersBackendResponse.json +0 -42
  89. package/test/unit/lib/model/data/getBulkTransfersMojaloopResponse.json +0 -22
  90. package/test/unit/lib/model/data/getTransfersBackendResponse.json +0 -34
  91. package/test/unit/lib/model/data/getTransfersMojaloopResponse.json +0 -17
  92. package/test/unit/lib/model/data/mockArguments.json +0 -188
  93. package/test/unit/lib/model/data/mockTxnRequestsArguments.json +0 -63
  94. package/test/unit/lib/model/data/notificationAbortedToPayee.json +0 -10
  95. package/test/unit/lib/model/data/notificationReservedToPayee.json +0 -10
  96. package/test/unit/lib/model/data/notificationToPayee.json +0 -10
  97. package/test/unit/lib/model/data/payeeParty.json +0 -18
  98. package/test/unit/lib/model/data/putQuotesResponse.json +0 -33
  99. package/test/unit/lib/model/data/putTransfersResponse.json +0 -5
  100. package/test/unit/lib/model/data/quoteResponse.json +0 -42
  101. package/test/unit/lib/model/data/requestToPayRequest.json +0 -20
  102. package/test/unit/lib/model/data/requestToPayTransferRequest.json +0 -27
  103. package/test/unit/lib/model/data/transactionRequestResponse.json +0 -18
  104. package/test/unit/lib/model/data/transferFulfil.json +0 -10
  105. package/test/unit/lib/model/data/transferRequest.json +0 -26
  106. package/test/unit/lib/model/mockedLibRequests.js +0 -74
  107. package/test/unit/mockLogger.js +0 -39
  108. package/test/unit/outboundApi/data/bulkQuoteRequest.json +0 -28
  109. package/test/unit/outboundApi/data/bulkTransferRequest.json +0 -28
  110. package/test/unit/outboundApi/data/mockBulkQuoteError.json +0 -45
  111. package/test/unit/outboundApi/data/mockBulkTransferError.json +0 -48
  112. package/test/unit/outboundApi/data/mockError.json +0 -41
  113. package/test/unit/outboundApi/data/mockGetPartiesError.json +0 -4
  114. package/test/unit/outboundApi/data/mockRequestToPayError.json +0 -32
  115. package/test/unit/outboundApi/data/mockRequestToPayTransferError.json +0 -39
  116. package/test/unit/outboundApi/data/requestToPay.json +0 -21
  117. package/test/unit/outboundApi/data/requestToPayTransferRequest.json +0 -20
  118. package/test/unit/outboundApi/data/transferRequest.json +0 -21
  119. package/test/unit/outboundApi/handlers.test.js +0 -887
@@ -1,1579 +0,0 @@
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 { MetricsClient } = require('~/lib/metrics');
19
- const Model = require('~/lib/model').OutboundTransfersModel;
20
- const PartiesModel = require('~/lib/model').PartiesModel;
21
-
22
- const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components');
23
- const StateMachine = require('javascript-state-machine');
24
-
25
- const defaultConfig = require('./data/defaultConfig');
26
- const transferRequest = require('./data/transferRequest');
27
- const payeeParty = require('./data/payeeParty');
28
- const quoteResponseTemplate = require('./data/quoteResponse');
29
- const transferFulfil = require('./data/transferFulfil');
30
-
31
- const { SDKStateEnum } = require('../../../../src/lib/model/common');
32
- const FSPIOPTransferStateEnum = require('@mojaloop/central-services-shared').Enum.Transfers.TransferState;
33
-
34
- const genPartyId = (party) => {
35
- const { partyIdType, partyIdentifier, partySubIdOrType } = party.body.party.partyIdInfo;
36
- return PartiesModel.channelName({
37
- type: partyIdType,
38
- id: partyIdentifier,
39
- subId: partySubIdOrType
40
- });
41
- };
42
-
43
- // util function to simulate a party resolution subscription message on a cache client
44
- const emitPartyCacheMessage = (cache, party) => cache.publish(genPartyId(party), JSON.stringify(party));
45
- const emitMultiPartiesCacheMessage = (cache, party) => cache.add(genPartyId(party), JSON.stringify(party));
46
-
47
- // util function to simulate a quote response subscription message on a cache client
48
- const emitQuoteResponseCacheMessage = (cache, quoteId, quoteResponse) => cache.publish(`qt_${quoteId}`, JSON.stringify(quoteResponse));
49
-
50
- // util function to simulate a transfer fulfilment subscription message on a cache client
51
- const emitTransferFulfilCacheMessage = (cache, transferId, fulfil) => cache.publish(`tf_${transferId}`, JSON.stringify(fulfil));
52
-
53
- const dummyRequestsModuleResponse = {
54
- originalRequest: {}
55
- };
56
-
57
- describe('outboundModel', () => {
58
- let quoteResponse;
59
- let config;
60
- let logger;
61
- let cache;
62
- let metricsClient;
63
-
64
- /**
65
- *
66
- * @param {Object} opts
67
- * @param {Number} opts.expirySeconds
68
- * @param {Object} opts.delays
69
- * @param {Number} delays.requestQuotes
70
- * @param {Number} delays.prepareTransfer
71
- * @param {Object} opts.rejects
72
- * @param {boolean} rejects.quoteResponse
73
- * @param {boolean} rejects.transferFulfils
74
- */
75
- async function testTransferWithDelay({expirySeconds, delays, rejects}) {
76
- const config = JSON.parse(JSON.stringify(defaultConfig));
77
- config.autoAcceptParty = true;
78
- config.autoAcceptQuotes = true;
79
- config.expirySeconds = expirySeconds;
80
- config.rejectExpiredQuoteResponses = rejects.quoteResponse;
81
- config.rejectExpiredTransferFulfils = rejects.transferFulfils;
82
-
83
- // simulate a callback with the resolved party
84
- MojaloopRequests.__getParties = jest.fn(() => {
85
- emitPartyCacheMessage(cache, payeeParty);
86
- return {
87
- originalRequest: {
88
- headers: [],
89
- body: {},
90
- }
91
- };
92
- });
93
-
94
- // simulate a delayed callback with the quote response
95
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
96
- setTimeout(() => {
97
- emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
98
- }, delays.requestQuotes ? delays.requestQuotes * 1000 : 0);
99
- return {
100
- originalRequest: {
101
- headers: [],
102
- body: postQuotesBody,
103
- }
104
- };
105
- });
106
-
107
- // simulate a delayed callback with the transfer fulfilment
108
- MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
109
- setTimeout(() => {
110
- emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
111
- }, delays.prepareTransfer ? delays.prepareTransfer * 1000 : 0);
112
- return {
113
- originalRequest: {
114
- headers: [],
115
- body: postTransfersBody,
116
- }
117
- };
118
- });
119
-
120
- const model = new Model({
121
- ...config,
122
- cache,
123
- logger,
124
- metricsClient,
125
- });
126
-
127
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
128
-
129
- let expectError;
130
- if (rejects.quoteResponse && delays.requestQuotes && expirySeconds < delays.requestQuotes) {
131
- expectError = 'Quote response missed expiry deadline';
132
- }
133
- if (rejects.transferFulfils && delays.prepareTransfer && expirySeconds < delays.prepareTransfer) {
134
- expectError = 'Transfer fulfil missed expiry deadline';
135
- }
136
- if (expectError) {
137
- await expect(model.run()).rejects.toThrowError(expectError);
138
- } else {
139
- const result = await model.run();
140
- await expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
141
- }
142
- }
143
-
144
- beforeAll(async () => {
145
- logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' });
146
- quoteResponse = JSON.parse(JSON.stringify(quoteResponseTemplate));
147
- metricsClient = new MetricsClient();
148
- });
149
-
150
- beforeEach(async () => {
151
- config = JSON.parse(JSON.stringify(defaultConfig));
152
- MojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
153
- MojaloopRequests.__getParties = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
154
- MojaloopRequests.__putQuotes = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
155
- MojaloopRequests.__putQuotesError = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
156
- MojaloopRequests.__postQuotes = jest.fn((body) => Promise.resolve({
157
- originalRequest: {
158
- headers: [],
159
- body: body,
160
- }
161
- }));
162
- MojaloopRequests.__postTransfers = jest.fn((body) => Promise.resolve({
163
- originalRequest: {
164
- headers: [],
165
- body: body,
166
- }
167
- }));
168
- cache = new Cache({
169
- cacheUrl: 'redis://dummy:1234',
170
- logger,
171
- });
172
- await cache.connect();
173
- });
174
-
175
- afterEach(async () => {
176
- await cache.disconnect();
177
- });
178
-
179
- test('initializes to starting state', async () => {
180
- const model = new Model({
181
- cache,
182
- logger,
183
- metricsClient,
184
- ...config,
185
- });
186
-
187
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
188
- expect(StateMachine.__instance.state).toBe('start');
189
- });
190
-
191
- test('executes all three transfer stages without halting when AUTO_ACCEPT_PARTY and AUTO_ACCEPT_QUOTES are true', async () => {
192
- config.autoAcceptParty = true;
193
- config.autoAcceptQuotes = true;
194
-
195
- MojaloopRequests.__getParties = jest.fn(() => {
196
- emitPartyCacheMessage(cache, payeeParty);
197
- return Promise.resolve(dummyRequestsModuleResponse);
198
- });
199
-
200
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
201
- // ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
202
- // including extension list
203
- const extensionList = postQuotesBody.extensionList.extension;
204
- expect(extensionList).toBeTruthy();
205
- expect(extensionList.length).toBe(2);
206
- expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
207
- expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
208
-
209
- // simulate a callback with the quote response
210
- emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
211
- return Promise.resolve(dummyRequestsModuleResponse);
212
- });
213
-
214
- MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
215
- //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
216
- // set as the destination FSPID, picked up from the header's value `fspiop-source`
217
- expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
218
-
219
- const extensionList = postTransfersBody.extensionList.extension;
220
- expect(extensionList).toBeTruthy();
221
- expect(extensionList.length).toBe(2);
222
- expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
223
- expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
224
-
225
- expect(destFspId).toBe(quoteResponse.data.headers['fspiop-source']);
226
- expect(model.data.to.fspId).toBe(payeeParty.body.party.partyIdInfo.fspId);
227
- expect(quoteResponse.data.headers['fspiop-source']).not.toBe(model.data.to.fspId);
228
-
229
- // simulate a callback with the transfer fulfilment
230
- emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
231
- return Promise.resolve(dummyRequestsModuleResponse);
232
- });
233
-
234
- const model = new Model({
235
- cache,
236
- logger,
237
- metricsClient,
238
- ...config,
239
- });
240
-
241
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
242
-
243
- expect(StateMachine.__instance.state).toBe('start');
244
-
245
- // start the model running
246
- const result = await model.run();
247
-
248
- expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
249
- expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
250
- expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
251
-
252
- // make sure no PATCH was sent as we did not set config or receive a RESERVED state
253
- expect(MojaloopRequests.__patchTransfers).toHaveBeenCalledTimes(0);
254
-
255
- // check we stopped at payeeResolved state
256
- expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
257
- expect(StateMachine.__instance.state).toBe('succeeded');
258
- });
259
-
260
- test('sends a PATCH /transfers/{transferId} request to payee DFSP when SEND_FINAL_NOTIFICATION_IF_REQUESTED is true', async () => {
261
- config.autoAcceptParty = true;
262
- config.autoAcceptQuotes = true;
263
- config.sendFinalNotificationIfRequested = true;
264
- MojaloopRequests.__getParties = jest.fn(() => {
265
- emitPartyCacheMessage(cache, payeeParty);
266
- return Promise.resolve(dummyRequestsModuleResponse);
267
- });
268
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
269
- // ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
270
- // including extension list
271
- const extensionList = postQuotesBody.extensionList.extension;
272
- expect(extensionList).toBeTruthy();
273
- expect(extensionList.length).toBe(2);
274
- expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
275
- expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
276
- // simulate a callback with the quote response
277
- emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
278
- return Promise.resolve(dummyRequestsModuleResponse);
279
- });
280
- const pb = JSON.parse(JSON.stringify(transferFulfil));
281
- pb.data.body.transferState = FSPIOPTransferStateEnum.RESERVED;
282
- MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
283
- //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
284
- // set as the destination FSPID, picked up from the header's value `fspiop-source`
285
- expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
286
- const extensionList = postTransfersBody.extensionList.extension;
287
- expect(extensionList).toBeTruthy();
288
- expect(extensionList.length).toBe(2);
289
- expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
290
- expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
291
- expect(destFspId).toBe(quoteResponse.data.headers['fspiop-source']);
292
- expect(model.data.to.fspId).toBe(payeeParty.body.party.partyIdInfo.fspId);
293
- expect(quoteResponse.data.headers['fspiop-source']).not.toBe(model.data.to.fspId);
294
- // simulate a callback with the transfer fulfilment
295
- emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, pb);
296
- return Promise.resolve(dummyRequestsModuleResponse);
297
- });
298
- const model = new Model({
299
- cache,
300
- logger,
301
- metricsClient,
302
- ...config,
303
- });
304
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
305
- expect(StateMachine.__instance.state).toBe('start');
306
- // start the model running
307
- const result = await model.run();
308
- expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
309
- expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
310
- expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
311
- expect(MojaloopRequests.__patchTransfers).toHaveBeenCalledTimes(1);
312
- expect(MojaloopRequests.__patchTransfers.mock.calls[0][0]).toEqual(model.data.transferId);
313
- expect(MojaloopRequests.__patchTransfers.mock.calls[0][1].transferState).toEqual(FSPIOPTransferStateEnum.COMMITTED);
314
- expect(MojaloopRequests.__patchTransfers.mock.calls[0][1].completedTimestamp).not.toBeUndefined();
315
- expect(MojaloopRequests.__patchTransfers.mock.calls[0][2]).toEqual(quoteResponse.data.headers['fspiop-source']);
316
-
317
-
318
- // check we stopped at payeeResolved state
319
- expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
320
- expect(StateMachine.__instance.state).toBe('succeeded');
321
- });
322
-
323
- test('uses quote response transfer amount for transfer prepare', async () => {
324
- config.autoAcceptParty = true;
325
- config.autoAcceptQuotes = true;
326
-
327
- MojaloopRequests.__getParties = jest.fn(() => {
328
- emitPartyCacheMessage(cache, payeeParty);
329
- return Promise.resolve(dummyRequestsModuleResponse);
330
- });
331
-
332
- // change the the transfer amount and currency in the quote response
333
- // so it is different to the initial request
334
- quoteResponse.data.body.transferAmount = {
335
- currency: 'XYZ',
336
- amount: '9876543210'
337
- };
338
-
339
- expect(quoteResponse.data.body.transferAmount).not.toEqual({
340
- amount: transferRequest.amount,
341
- currency: transferRequest.currency
342
- });
343
-
344
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
345
- // ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
346
- // including extension list
347
- const extensionList = postQuotesBody.extensionList.extension;
348
- expect(extensionList).toBeTruthy();
349
- expect(extensionList.length).toBe(2);
350
- expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
351
- expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
352
-
353
- // simulate a callback with the quote response
354
- emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
355
- return Promise.resolve(dummyRequestsModuleResponse);
356
- });
357
-
358
- MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
359
- //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
360
- // set as the destination FSPID, picked up from the header's value `fspiop-source`
361
- expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
362
-
363
- const extensionList = postTransfersBody.extensionList.extension;
364
- expect(extensionList).toBeTruthy();
365
- expect(extensionList.length).toBe(2);
366
- expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
367
- expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
368
-
369
- expect(destFspId).toBe(quoteResponse.data.headers['fspiop-source']);
370
- expect(model.data.to.fspId).toBe(payeeParty.body.party.partyIdInfo.fspId);
371
- expect(quoteResponse.data.headers['fspiop-source']).not.toBe(model.data.to.fspId);
372
-
373
- expect(postTransfersBody.amount).toEqual(quoteResponse.data.body.transferAmount);
374
-
375
- // simulate a callback with the transfer fulfilment
376
- emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
377
- return Promise.resolve(dummyRequestsModuleResponse);
378
- });
379
-
380
- const model = new Model({
381
- cache,
382
- logger,
383
- metricsClient,
384
- ...config,
385
- });
386
-
387
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
388
-
389
- expect(StateMachine.__instance.state).toBe('start');
390
-
391
- // start the model running
392
- const result = await model.run();
393
-
394
- expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
395
- expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
396
- expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
397
-
398
- // make sure no PATCH was sent as we did not set config or receive a RESERVED state
399
- expect(MojaloopRequests.__patchTransfers).toHaveBeenCalledTimes(0);
400
-
401
- // check we stopped at payeeResolved state
402
- expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
403
- expect(StateMachine.__instance.state).toBe('succeeded');
404
- });
405
-
406
- test('test get transfer', async () => {
407
- MojaloopRequests.__getTransfers = jest.fn((transferId) => {
408
- emitTransferFulfilCacheMessage(cache, transferId, transferFulfil);
409
- return Promise.resolve();
410
- });
411
-
412
- const model = new Model({
413
- cache,
414
- logger,
415
- metricsClient,
416
- ...config,
417
- });
418
-
419
- const TRANSFER_ID = 'tx-id000011';
420
-
421
- await model.initialize(JSON.parse(JSON.stringify({
422
- ...transferRequest,
423
- currentState: 'getTransfer',
424
- transferId: TRANSFER_ID,
425
- })));
426
-
427
- expect(StateMachine.__instance.state).toBe('getTransfer');
428
-
429
- // start the model running
430
- const result = await model.run();
431
-
432
- expect(MojaloopRequests.__getTransfers).toHaveBeenCalledTimes(1);
433
-
434
- // check we stopped at payeeResolved state
435
- expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
436
- expect(StateMachine.__instance.state).toBe('succeeded');
437
- });
438
-
439
-
440
- test('resolves payee and halts when AUTO_ACCEPT_PARTY is false', async () => {
441
- config.autoAcceptParty = false;
442
-
443
- const model = new Model({
444
- cache,
445
- logger,
446
- metricsClient,
447
- ...config,
448
- });
449
-
450
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
451
-
452
- expect(StateMachine.__instance.state).toBe('start');
453
-
454
- // start the model running
455
- const resultPromise = model.run();
456
-
457
- // now we started the model running we simulate a callback with the resolved party
458
- emitPartyCacheMessage(cache, payeeParty);
459
-
460
- // wait for the model to reach a terminal state
461
- const result = await resultPromise;
462
-
463
- // check we stopped at payeeResolved state
464
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
465
- expect(StateMachine.__instance.state).toBe('payeeResolved');
466
- });
467
-
468
- test('uses payee party fspid as source header when supplied - resolving payee', async () => {
469
- config.autoAcceptParty = false;
470
-
471
- const model = new Model({
472
- cache,
473
- logger,
474
- metricsClient,
475
- ...config,
476
- });
477
-
478
- let req = JSON.parse(JSON.stringify(transferRequest));
479
- const testFspId = 'TESTDESTFSPID';
480
- req.to.fspId = testFspId;
481
-
482
- await model.initialize(req);
483
-
484
- expect(StateMachine.__instance.state).toBe('start');
485
-
486
- // start the model running
487
- const resultPromise = model.run();
488
-
489
- // now we started the model running we simulate a callback with the resolved party
490
- emitPartyCacheMessage(cache, payeeParty);
491
-
492
- // wait for the model to reach a terminal state
493
- const result = await resultPromise;
494
-
495
- // check we stopped at payeeResolved state
496
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
497
- expect(StateMachine.__instance.state).toBe('payeeResolved');
498
-
499
- // check getParties mojaloop requests method was called with the correct arguments
500
- expect(MojaloopRequests.__getParties).toHaveBeenCalledWith(req.to.idType, req.to.idValue, req.to.idSubValue, testFspId);
501
- });
502
-
503
- test('resolves multiple payees and halts', async () => {
504
- config.autoAcceptParty = false;
505
- config.multiplePartiesResponse = true;
506
- config.multiplePartiesResponseSeconds = 2;
507
- const model = new Model({
508
- cache,
509
- logger,
510
- metricsClient,
511
- ...config,
512
- });
513
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
514
- expect(StateMachine.__instance.state).toBe('start');
515
- // start the model running
516
- const resultPromise = model.run();
517
- // now we started the model running we simulate a callback with the resolved party
518
- const payeeParty1 = JSON.parse(JSON.stringify(payeeParty));
519
- payeeParty1.body.party.partyIdInfo.fspId = 'FirstFspId';
520
- await emitMultiPartiesCacheMessage(cache, payeeParty1);
521
- const payeeParty2 = JSON.parse(JSON.stringify(payeeParty));
522
- payeeParty2.body.party.partyIdInfo.fspId = 'SecondFspId';
523
- await emitMultiPartiesCacheMessage(cache, payeeParty2);
524
- // wait for the model to reach a terminal state
525
- const result = await resultPromise;
526
- // check we stopped at payeeResolved state
527
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
528
- expect(StateMachine.__instance.state).toBe('payeeResolved');
529
- expect(result.to[0].fspId).toEqual('FirstFspId');
530
- expect(result.to[1].fspId).toEqual('SecondFspId');
531
- });
532
-
533
- 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 () => {
534
- config.autoAcceptParty = false;
535
- config.autoAcceptQuotes = false;
536
-
537
- let model = new Model({
538
- cache,
539
- logger,
540
- metricsClient,
541
- ...config,
542
- });
543
-
544
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
545
-
546
- expect(StateMachine.__instance.state).toBe('start');
547
-
548
- // start the model running
549
- let resultPromise = model.run();
550
-
551
- // now we started the model running we simulate a callback with the resolved party
552
- emitPartyCacheMessage(cache, payeeParty);
553
-
554
- // wait for the model to reach a terminal state
555
- let result = await resultPromise;
556
-
557
- // check we stopped at payeeResolved state
558
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
559
- expect(StateMachine.__instance.state).toBe('payeeResolved');
560
-
561
- const transferId = result.transferId;
562
-
563
- // load a new model from the saved state
564
- model = new Model({
565
- cache,
566
- logger,
567
- metricsClient,
568
- ...config,
569
- });
570
-
571
- await model.load(transferId);
572
-
573
- // check the model loaded to the correct state
574
- expect(StateMachine.__instance.state).toBe('payeeResolved');
575
-
576
- // now run the model again. this should trigger transition to quote request
577
- resultPromise = model.run({ acceptParty: true });
578
- // now we started the model running we simulate a callback with the quote response
579
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
580
-
581
- // wait for the model to reach a terminal state
582
- result = await resultPromise;
583
-
584
- // check we stopped at payeeResolved state
585
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
586
- expect(StateMachine.__instance.state).toBe('quoteReceived');
587
- });
588
-
589
- test('Allows change of transferAmount at accept party phase', async () => {
590
- config.autoAcceptParty = false;
591
- config.autoAcceptQuotes = false;
592
-
593
- let model = new Model({
594
- cache,
595
- logger,
596
- metricsClient,
597
- ...config,
598
- });
599
-
600
- const req = JSON.parse(JSON.stringify(transferRequest));
601
-
602
- // record the initial requested transfer amount
603
- const initialAmount = req.amount;
604
-
605
- await model.initialize(req);
606
-
607
- expect(StateMachine.__instance.state).toBe('start');
608
-
609
- // start the model running
610
- let resultPromise = model.run();
611
-
612
- // now we started the model running we simulate a callback with the resolved party
613
- emitPartyCacheMessage(cache, payeeParty);
614
-
615
- // wait for the model to reach a terminal state
616
- let result = await resultPromise;
617
-
618
- // check we stopped at payeeResolved state
619
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
620
- expect(StateMachine.__instance.state).toBe('payeeResolved');
621
-
622
- expect(result.amount).toEqual(initialAmount);
623
-
624
- const transferId = result.transferId;
625
-
626
- // load a new model from the saved state
627
- model = new Model({
628
- cache,
629
- logger,
630
- metricsClient,
631
- ...config,
632
- });
633
-
634
- await model.load(transferId);
635
-
636
- // check the model loaded to the correct state
637
- expect(StateMachine.__instance.state).toBe('payeeResolved');
638
-
639
- const resume = {
640
- amount: 999,
641
- acceptParty: true,
642
- };
643
-
644
- // now run the model again. this should trigger transition to quote request
645
- resultPromise = model.run(resume);
646
-
647
- // now we started the model running we simulate a callback with the quote response
648
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
649
-
650
- // wait for the model to reach a terminal state
651
- result = await resultPromise;
652
-
653
- // check we stopped at quoteReceived state
654
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
655
- expect(StateMachine.__instance.state).toBe('quoteReceived');
656
-
657
- // check the accept party key got merged to the state
658
- expect(result.acceptParty).toEqual(true);
659
-
660
- // check the amount key got changed
661
- expect(result.amount).toEqual(resume.amount);
662
-
663
- // check the quote request amount is the NEW amount, not the initial amount
664
- expect(result.quoteRequest.body.amount.amount).toStrictEqual(resume.amount);
665
- expect(result.quoteRequest.body.amount.amount).not.toEqual(initialAmount);
666
- });
667
-
668
- test('Allows change of payee party at accept party phase (round-robin support)', async () => {
669
- config.autoAcceptParty = false;
670
- config.autoAcceptQuotes = false;
671
-
672
- let model = new Model({
673
- cache,
674
- logger,
675
- metricsClient,
676
- ...config,
677
- });
678
-
679
- const req = JSON.parse(JSON.stringify(transferRequest));
680
-
681
- // record the initial requested transfer amount
682
- const initialAmount = req.amount;
683
-
684
- await model.initialize(req);
685
-
686
- expect(StateMachine.__instance.state).toBe('start');
687
-
688
- // start the model running
689
- let resultPromise = model.run();
690
-
691
- // now we started the model running we simulate a callback with the resolved party
692
- emitPartyCacheMessage(cache, payeeParty);
693
-
694
- // wait for the model to reach a terminal state
695
- let result = await resultPromise;
696
-
697
- // check we stopped at payeeResolved state
698
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
699
- expect(StateMachine.__instance.state).toBe('payeeResolved');
700
-
701
- expect(result.amount).toEqual(initialAmount);
702
-
703
- const transferId = result.transferId;
704
-
705
- // load a new model from the saved state
706
- model = new Model({
707
- cache,
708
- logger,
709
- metricsClient,
710
- ...config,
711
- });
712
-
713
- await model.load(transferId);
714
-
715
- // check the model loaded to the correct state
716
- expect(StateMachine.__instance.state).toBe('payeeResolved');
717
-
718
- const newPayee = {
719
- partyIdInfo: {
720
- partySubIdOrType: undefined,
721
- partyIdType: 'PASSPORT',
722
- partyIdentifier: 'AAABBBCCCDDDEEE',
723
- fspId: 'TESTDFSP'
724
- }
725
- };
726
-
727
- const newPayeeInternal = {
728
- idType: newPayee.partyIdInfo.partyIdType,
729
- idValue: newPayee.partyIdInfo.partyIdentifier,
730
- fspId: newPayee.partyIdInfo.fspId,
731
- };
732
-
733
- const resume = {
734
- acceptParty: true,
735
- to: newPayeeInternal,
736
- };
737
-
738
- // now run the model again. this should trigger transition to quote request
739
- resultPromise = model.run(resume);
740
-
741
- // now we started the model running we simulate a callback with the quote response
742
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
743
-
744
- // wait for the model to reach a terminal state
745
- result = await resultPromise;
746
-
747
- // check we stopped at quoteReceived state
748
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
749
- expect(StateMachine.__instance.state).toBe('quoteReceived');
750
-
751
- // check the accept party key got merged to the state
752
- expect(result.acceptParty).toEqual(true);
753
-
754
- // check the "to" passed in to model resume is merged into the model state correctly
755
- expect(result.to).toStrictEqual(newPayeeInternal);
756
-
757
- // check the quote request payee party is the NEW one, not the initial one.
758
- expect(result.quoteRequest.body.payee).toStrictEqual(newPayee);
759
- });
760
-
761
- test('Does not merge resume data keys into state that are not permitted', async () => {
762
- config.autoAcceptParty = false;
763
- config.autoAcceptQuotes = false;
764
-
765
- let model = new Model({
766
- cache,
767
- logger,
768
- metricsClient,
769
- ...config,
770
- });
771
-
772
- const req = JSON.parse(JSON.stringify(transferRequest));
773
-
774
- // record the initial requested transfer amount
775
- const initialAmount = req.amount;
776
-
777
- await model.initialize(req);
778
-
779
- expect(StateMachine.__instance.state).toBe('start');
780
-
781
- // start the model running
782
- let resultPromise = model.run();
783
-
784
- // now we started the model running we simulate a callback with the resolved party
785
- emitPartyCacheMessage(cache, payeeParty);
786
-
787
- // wait for the model to reach a terminal state
788
- let result = await resultPromise;
789
-
790
- // check we stopped at payeeResolved state
791
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
792
- expect(StateMachine.__instance.state).toBe('payeeResolved');
793
-
794
- expect(result.amount).toEqual(initialAmount);
795
-
796
- const transferId = result.transferId;
797
-
798
- // load a new model from the saved state
799
- model = new Model({
800
- cache,
801
- logger,
802
- metricsClient,
803
- ...config,
804
- });
805
-
806
- await model.load(transferId);
807
-
808
- // check the model loaded to the correct state
809
- expect(StateMachine.__instance.state).toBe('payeeResolved');
810
-
811
- const resume = {
812
- amount: 999,
813
- acceptParty: true,
814
- someRandomKey: 'this key name is not permitted',
815
- };
816
-
817
- // now run the model again. this should trigger transition to quote request
818
- resultPromise = model.run(resume);
819
-
820
- // now we started the model running we simulate a callback with the quote response
821
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
822
-
823
- // wait for the model to reach a terminal state
824
- result = await resultPromise;
825
-
826
- // check we stopped at quoteReceived state
827
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
828
- expect(StateMachine.__instance.state).toBe('quoteReceived');
829
-
830
- // check the accept party key got merged to the state
831
- expect(result.acceptParty).toEqual(true);
832
-
833
- // check the amount key got changed
834
- expect(result.amount).toEqual(resume.amount);
835
-
836
- // check the quote request amount is the NEW amount, not the initial amount
837
- expect(result.quoteRequest.body.amount.amount).toStrictEqual(resume.amount);
838
- expect(result.quoteRequest.body.amount.amount).not.toEqual(initialAmount);
839
-
840
- // check that our disallowed key is not merged to the transfer state
841
- expect(result.someRandomKey).toBeUndefined();
842
- });
843
-
844
- test('skips resolving party when to.fspid is specified and skipPartyLookup is truthy', async () => {
845
- config.autoAcceptParty = false;
846
- config.autoAcceptQuotes = false;
847
-
848
- let model = new Model({
849
- cache,
850
- logger,
851
- metricsClient,
852
- ...config,
853
- });
854
-
855
- let req = JSON.parse(JSON.stringify(transferRequest));
856
- const testFspId = 'TESTDESTFSPID';
857
- req.to.fspId = testFspId;
858
- req.skipPartyLookup = true;
859
-
860
- await model.initialize(req);
861
-
862
- expect(StateMachine.__instance.state).toBe('start');
863
-
864
- // start the model running
865
- let resultPromise = model.run();
866
-
867
- // now we started the model running we simulate a callback with the quote response
868
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
869
-
870
- // wait for the model to reach a terminal state
871
- let result = await resultPromise;
872
-
873
- // check we stopped at quoteReceived state
874
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
875
- expect(StateMachine.__instance.state).toBe('quoteReceived');
876
- });
877
-
878
- test('aborts after party rejected by backend', async () => {
879
- config.autoAcceptParty = false;
880
- config.autoAcceptQuotes = false;
881
-
882
- let model = new Model({
883
- cache,
884
- logger,
885
- metricsClient,
886
- ...config,
887
- });
888
-
889
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
890
-
891
- expect(StateMachine.__instance.state).toBe('start');
892
-
893
- // start the model running
894
- let resultPromise = model.run();
895
-
896
- // now we started the model running we simulate a callback with the resolved party
897
- emitPartyCacheMessage(cache, payeeParty);
898
-
899
- // wait for the model to reach a terminal state
900
- let result = await resultPromise;
901
-
902
- // check we stopped at payeeResolved state
903
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
904
- expect(StateMachine.__instance.state).toBe('payeeResolved');
905
-
906
- const transferId = result.transferId;
907
-
908
- // load a new model from the saved state
909
- model = new Model({
910
- cache,
911
- logger,
912
- metricsClient,
913
- ...config,
914
- });
915
-
916
- await model.load(transferId);
917
-
918
- // check the model loaded to the correct state
919
- expect(StateMachine.__instance.state).toBe('payeeResolved');
920
-
921
- // now run the model again with a party rejection. this should trigger transition to quote request
922
- result = await model.run({ resume: { acceptParty: false } });
923
-
924
- // check we stopped at quoteReceived state
925
- expect(result.currentState).toBe(SDKStateEnum.ABORTED);
926
- expect(result.abortedReason).toBe('Payee rejected by backend');
927
- expect(StateMachine.__instance.state).toBe('aborted');
928
- });
929
-
930
- test('aborts after quote rejected by backend', async () => {
931
- config.autoAcceptParty = false;
932
- config.autoAcceptQuotes = false;
933
-
934
- let model = new Model({
935
- cache,
936
- logger,
937
- metricsClient,
938
- ...config,
939
- });
940
-
941
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
942
-
943
- expect(StateMachine.__instance.state).toBe('start');
944
-
945
- // start the model running
946
- let resultPromise = model.run();
947
-
948
- // now we started the model running we simulate a callback with the resolved party
949
- emitPartyCacheMessage(cache, payeeParty);
950
-
951
- // wait for the model to reach a terminal state
952
- let result = await resultPromise;
953
-
954
- // check we stopped at payeeResolved state
955
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
956
- expect(StateMachine.__instance.state).toBe('payeeResolved');
957
-
958
- const transferId = result.transferId;
959
-
960
- // load a new model from the saved state
961
- model = new Model({
962
- cache,
963
- logger,
964
- metricsClient,
965
- ...config,
966
- });
967
-
968
- await model.load(transferId);
969
-
970
- // check the model loaded to the correct state
971
- expect(StateMachine.__instance.state).toBe('payeeResolved');
972
-
973
- // now run the model again. this should trigger transition to quote request
974
- resultPromise = model.run({ acceptParty: true });
975
-
976
- // now we started the model running we simulate a callback with the quote response
977
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
978
-
979
- // wait for the model to reach quote received
980
- result = await resultPromise;
981
-
982
- // check we stopped at payeeResolved state
983
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
984
- expect(StateMachine.__instance.state).toBe('quoteReceived');
985
-
986
- // now run the model again. this should trigger abort as the quote was not accepted
987
- result = await model.run({ acceptQuote: false });
988
-
989
- expect(result.currentState).toBe(SDKStateEnum.ABORTED);
990
- expect(result.abortedReason).toBe('Quote rejected by backend');
991
- expect(StateMachine.__instance.state).toBe('aborted');
992
- });
993
-
994
- test('should handle unknown state with a meaningful error message', async () => {
995
- config.autoAcceptParty = false;
996
- config.autoAcceptQuotes = false;
997
-
998
- let model = new Model({
999
- cache,
1000
- logger,
1001
- metricsClient,
1002
- ...config,
1003
- });
1004
-
1005
- await model.initialize(JSON.parse(JSON.stringify({
1006
- ...transferRequest,
1007
- currentState: 'abc'
1008
- })));
1009
-
1010
- expect(StateMachine.__instance.state).toBe('abc');
1011
-
1012
- // start the model running
1013
- let resultPromise = model.run();
1014
-
1015
- // wait for the model to reach a terminal state
1016
- let result = await resultPromise;
1017
- expect(result).toBe(undefined);
1018
- });
1019
-
1020
- test('should handle subsequent put transfer calls incase of aborted transfer', async () => {
1021
- config.autoAcceptParty = false;
1022
- config.autoAcceptQuotes = false;
1023
-
1024
- let model = new Model({
1025
- cache,
1026
- logger,
1027
- metricsClient,
1028
- ...config,
1029
- });
1030
-
1031
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
1032
-
1033
- expect(StateMachine.__instance.state).toBe('start');
1034
-
1035
- // start the model running
1036
- let resultPromise = model.run();
1037
-
1038
- // now we started the model running we simulate a callback with the resolved party
1039
- emitPartyCacheMessage(cache, payeeParty);
1040
-
1041
- // wait for the model to reach a terminal state
1042
- let result = await resultPromise;
1043
-
1044
- // check we stopped at payeeResolved state
1045
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
1046
- expect(StateMachine.__instance.state).toBe('payeeResolved');
1047
-
1048
- const transferId = result.transferId;
1049
-
1050
- // load a new model from the saved state
1051
- model = new Model({
1052
- cache,
1053
- logger,
1054
- metricsClient,
1055
- ...config,
1056
- });
1057
-
1058
- await model.load(transferId);
1059
-
1060
- // check the model loaded to the correct state
1061
- expect(StateMachine.__instance.state).toBe('payeeResolved');
1062
-
1063
- // now run the model again. this should trigger transition to quote request
1064
- resultPromise = model.run({ acceptParty: true });
1065
-
1066
- // now we started the model running we simulate a callback with the quote response
1067
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
1068
-
1069
- // wait for the model to reach quote received
1070
- result = await resultPromise;
1071
-
1072
- // check we stopped at payeeResolved state
1073
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
1074
- expect(StateMachine.__instance.state).toBe('quoteReceived');
1075
-
1076
- // now run the model again. this should trigger abort as the quote was not accepted
1077
- result = await model.run({ acceptQuote: false });
1078
-
1079
- expect(result.currentState).toBe(SDKStateEnum.ABORTED);
1080
- expect(result.abortedReason).toBe('Quote rejected by backend');
1081
- expect(StateMachine.__instance.state).toBe('aborted');
1082
-
1083
- // now run the model again. this should get the same result as previous one
1084
- result = await model.run({ acceptQuote: false });
1085
-
1086
- expect(result.currentState).toBe(SDKStateEnum.ABORTED);
1087
- expect(result.abortedReason).toBe('Quote rejected by backend');
1088
- expect(StateMachine.__instance.state).toBe('aborted');
1089
- });
1090
-
1091
- test('halts and resumes after parties and quotes stages when AUTO_ACCEPT_PARTY is false and AUTO_ACCEPT_QUOTES is false', async () => {
1092
- config.autoAcceptParty = false;
1093
- config.autoAcceptQuotes = false;
1094
-
1095
- let model = new Model({
1096
- cache,
1097
- logger,
1098
- metricsClient,
1099
- ...config,
1100
- });
1101
-
1102
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
1103
-
1104
- expect(StateMachine.__instance.state).toBe('start');
1105
-
1106
- // start the model running
1107
- let resultPromise = model.run();
1108
-
1109
- // now we started the model running we simulate a callback with the resolved party
1110
- emitPartyCacheMessage(cache, payeeParty);
1111
-
1112
- // wait for the model to reach a terminal state
1113
- let result = await resultPromise;
1114
-
1115
- // check we stopped at payeeResolved state
1116
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_PARTY_ACCEPTANCE);
1117
- expect(StateMachine.__instance.state).toBe('payeeResolved');
1118
-
1119
- const transferId = result.transferId;
1120
-
1121
- // load a new model from the saved state
1122
- model = new Model({
1123
- cache,
1124
- logger,
1125
- metricsClient,
1126
- ...config,
1127
- });
1128
-
1129
- await model.load(transferId);
1130
-
1131
- // check the model loaded to the correct state
1132
- expect(StateMachine.__instance.state).toBe('payeeResolved');
1133
-
1134
- // now run the model again. this should trigger transition to quote request
1135
- resultPromise = model.run({ acceptParty: true });
1136
-
1137
- // now we started the model running we simulate a callback with the quote response
1138
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
1139
-
1140
- // wait for the model to reach a terminal state
1141
- result = await resultPromise;
1142
-
1143
- // check we stopped at quoteReceived state
1144
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
1145
- expect(StateMachine.__instance.state).toBe('quoteReceived');
1146
-
1147
- // load a new model from the saved state
1148
- model = new Model({
1149
- cache,
1150
- logger,
1151
- metricsClient,
1152
- ...config,
1153
- });
1154
-
1155
- await model.load(transferId);
1156
-
1157
- // check the model loaded to the correct state
1158
- expect(StateMachine.__instance.state).toBe('quoteReceived');
1159
-
1160
- // now run the model again. this should trigger transition to quote request
1161
- resultPromise = model.run({ acceptQuote: true });
1162
-
1163
- // now we started the model running we simulate a callback with the transfer fulfilment
1164
- cache.publish(`tf_${model.data.transferId}`, JSON.stringify(transferFulfil));
1165
-
1166
- // wait for the model to reach a terminal state
1167
- result = await resultPromise;
1168
-
1169
- // check we stopped at quoteReceived state
1170
- expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
1171
- expect(StateMachine.__instance.state).toBe('succeeded');
1172
- });
1173
-
1174
- test('uses payee party fspid for transfer prepare when config USE_QUOTE_SOURCE_FSP_AS_TRANSFER_PAYEE_FSP is false', async () => {
1175
- config.autoAcceptParty = true;
1176
- config.autoAcceptQuotes = true;
1177
- config.useQuoteSourceFSPAsTransferPayeeFSP = false;
1178
-
1179
- MojaloopRequests.__getParties = jest.fn(() => {
1180
- // simulate a callback with the resolved party
1181
- emitPartyCacheMessage(cache, payeeParty);
1182
- return Promise.resolve(dummyRequestsModuleResponse);
1183
- });
1184
-
1185
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
1186
- // simulate a callback with the quote response
1187
- emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
1188
- return Promise.resolve(dummyRequestsModuleResponse);
1189
- });
1190
-
1191
- MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
1192
- //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
1193
- // set as the destination FSPID, picked up from the header's value `fspiop-source`
1194
- expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
1195
- expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
1196
- const payeeFsp = MojaloopRequests.__postTransfers.mock.calls[0][0].payeeFsp;
1197
- expect(payeeFsp).toEqual(payeeParty.body.party.partyIdInfo.fspId);
1198
-
1199
- // simulate a callback with the transfer fulfilment
1200
- emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
1201
- return Promise.resolve(dummyRequestsModuleResponse);
1202
- });
1203
-
1204
- const model = new Model({
1205
- cache,
1206
- logger,
1207
- metricsClient,
1208
- ...config,
1209
- });
1210
-
1211
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
1212
-
1213
- expect(StateMachine.__instance.state).toBe('start');
1214
-
1215
- // start the model running
1216
- const resultPromise = model.run();
1217
-
1218
- // wait for the model to reach a terminal state
1219
- const result = await resultPromise;
1220
-
1221
- // check we stopped at payeeResolved state
1222
- expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
1223
- expect(StateMachine.__instance.state).toBe('succeeded');
1224
- });
1225
-
1226
- test('uses quote response source fspid for transfer prepare when config USE_QUOTE_SOURCE_FSP_AS_TRANSFER_PAYEE_FSP is true', async () => {
1227
- config.autoAcceptParty = true;
1228
- config.autoAcceptQuotes = true;
1229
- config.useQuoteSourceFSPAsTransferPayeeFSP = true;
1230
-
1231
- MojaloopRequests.__getParties = jest.fn(() => {
1232
- // simulate a callback with the resolved party
1233
- emitPartyCacheMessage(cache, payeeParty);
1234
- return Promise.resolve(dummyRequestsModuleResponse);
1235
- });
1236
-
1237
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
1238
- // simulate a callback with the quote response
1239
- emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
1240
- return Promise.resolve(dummyRequestsModuleResponse);
1241
- });
1242
-
1243
- MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
1244
- //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
1245
- // set as the destination FSPID, picked up from the header's value `fspiop-source`
1246
- expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
1247
- expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
1248
- const payeeFsp = MojaloopRequests.__postTransfers.mock.calls[0][0].payeeFsp;
1249
- expect(payeeFsp).toEqual(quoteResponse.data.headers['fspiop-source']);
1250
-
1251
- // simulate a callback with the transfer fulfilment
1252
- emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
1253
- return Promise.resolve(dummyRequestsModuleResponse);
1254
- });
1255
-
1256
- const model = new Model({
1257
- cache,
1258
- logger,
1259
- metricsClient,
1260
- ...config,
1261
- });
1262
-
1263
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
1264
-
1265
- expect(StateMachine.__instance.state).toBe('start');
1266
-
1267
- // start the model running
1268
- const resultPromise = model.run();
1269
-
1270
- // wait for the model to reach a terminal state
1271
- const result = await resultPromise;
1272
-
1273
- // check we stopped at payeeResolved state
1274
- expect(result.currentState).toBe(SDKStateEnum.COMPLETED);
1275
- expect(StateMachine.__instance.state).toBe('succeeded');
1276
- });
1277
-
1278
- test('pass quote response `expiration` deadline', () =>
1279
- testTransferWithDelay({
1280
- expirySeconds: 2,
1281
- delays: {
1282
- requestQuotes: 1,
1283
- },
1284
- rejects: {
1285
- quoteResponse: true,
1286
- }
1287
- })
1288
- );
1289
-
1290
- test('pass transfer fulfills `expiration` deadline', () =>
1291
- testTransferWithDelay({
1292
- expirySeconds: 2,
1293
- delays: {
1294
- prepareTransfer: 1,
1295
- },
1296
- rejects: {
1297
- transferFulfils: true,
1298
- }
1299
- })
1300
- );
1301
-
1302
- test('pass all stages `expiration` deadlines', () =>
1303
- testTransferWithDelay({
1304
- expirySeconds: 2,
1305
- delays: {
1306
- requestQuotes: 1,
1307
- prepareTransfer: 1,
1308
- },
1309
- rejects: {
1310
- quoteResponse: true,
1311
- transferFulfils: true,
1312
- }
1313
- })
1314
- );
1315
-
1316
- test('fail on quote response `expiration` deadline', () =>
1317
- testTransferWithDelay({
1318
- expirySeconds: 1,
1319
- delays: {
1320
- requestQuotes: 2,
1321
- },
1322
- rejects: {
1323
- quoteResponse: true,
1324
- }
1325
- })
1326
- );
1327
-
1328
- test('fail on transfer fulfills `expiration` deadline', () =>
1329
- testTransferWithDelay({
1330
- expirySeconds: 1,
1331
- delays: {
1332
- prepareTransfer: 2,
1333
- },
1334
- rejects: {
1335
- transferFulfils: true,
1336
- }
1337
- })
1338
- );
1339
-
1340
- test('Throws with mojaloop error in response body when party resolution error callback occurs', async () => {
1341
- config.autoAcceptParty = true;
1342
- config.autoAcceptQuotes = true;
1343
-
1344
- MojaloopRequests.__getParties = jest.fn(() => {
1345
- // simulate a callback with the resolved party
1346
- cache.publish(genPartyId(payeeParty), JSON.stringify(expectError));
1347
- return Promise.resolve(dummyRequestsModuleResponse);
1348
- });
1349
-
1350
- const model = new Model({
1351
- cache,
1352
- logger,
1353
- metricsClient,
1354
- ...config,
1355
- });
1356
-
1357
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
1358
-
1359
- expect(StateMachine.__instance.state).toBe('start');
1360
-
1361
- const expectError = {
1362
- body: {
1363
- errorInformation: {
1364
- errorCode: '3204',
1365
- errorDescription: 'Party not found'
1366
- }
1367
- }
1368
- };
1369
-
1370
- const errMsg = 'Got an error response resolving party: { errorInformation: { errorCode: \'3204\', errorDescription: \'Party not found\' } }';
1371
-
1372
- try {
1373
- await model.run();
1374
- }
1375
- catch(err) {
1376
- expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
1377
- expect(err.transferState).toBeTruthy();
1378
- expect(err.transferState.lastError).toBeTruthy();
1379
- expect(err.transferState.lastError.mojaloopError).toEqual(expectError.body);
1380
- expect(err.transferState.lastError.transferState).toBe(undefined);
1381
- return;
1382
- }
1383
-
1384
- throw new Error('Outbound model should have thrown');
1385
- });
1386
-
1387
-
1388
- test('Throws with mojaloop error in response body when quote request error callback occurs', async () => {
1389
- config.autoAcceptParty = true;
1390
- config.autoAcceptQuotes = true;
1391
-
1392
- const expectError = {
1393
- type: 'quoteResponseError',
1394
- data: {
1395
- body: {
1396
- errorInformation: {
1397
- errorCode: '3205',
1398
- errorDescription: 'Quote ID not found'
1399
- }
1400
- },
1401
- headers: {}
1402
- }
1403
- };
1404
-
1405
-
1406
- MojaloopRequests.__getParties = jest.fn(() => {
1407
- // simulate a callback with the resolved party
1408
- emitPartyCacheMessage(cache, payeeParty);
1409
- return Promise.resolve(dummyRequestsModuleResponse);
1410
- });
1411
-
1412
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
1413
- // simulate a callback with the quote response
1414
- cache.publish(`qt_${postQuotesBody.quoteId}`, JSON.stringify(expectError));
1415
- return Promise.resolve(dummyRequestsModuleResponse);
1416
- });
1417
-
1418
- const model = new Model({
1419
- cache,
1420
- logger,
1421
- metricsClient,
1422
- ...config,
1423
- });
1424
-
1425
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
1426
-
1427
- expect(StateMachine.__instance.state).toBe('start');
1428
-
1429
- const errMsg = 'Got an error response requesting quote: { errorInformation:\n { errorCode: \'3205\', errorDescription: \'Quote ID not found\' } }';
1430
-
1431
- try {
1432
- await model.run();
1433
- }
1434
- catch(err) {
1435
- expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
1436
- expect(err.transferState).toBeTruthy();
1437
- expect(err.transferState.lastError).toBeTruthy();
1438
- expect(err.transferState.lastError.mojaloopError).toEqual(expectError.data.body);
1439
- expect(err.transferState.lastError.transferState).toBe(undefined);
1440
- return;
1441
- }
1442
-
1443
- throw new Error('Outbound model should have thrown');
1444
- });
1445
-
1446
-
1447
- test('Throws with mojaloop error in response body when transfer request error callback occurs', async () => {
1448
- config.autoAcceptParty = true;
1449
- config.autoAcceptQuotes = true;
1450
-
1451
- const expectError = {
1452
- type: 'transferError',
1453
- data: {
1454
- body: {
1455
- errorInformation: {
1456
- errorCode: '4001',
1457
- errorDescription: 'Payer FSP insufficient liquidity'
1458
- }
1459
- }
1460
- }
1461
- };
1462
-
1463
- MojaloopRequests.__getParties = jest.fn(() => {
1464
- // simulate a callback with the resolved party
1465
- emitPartyCacheMessage(cache, payeeParty);
1466
- return Promise.resolve(dummyRequestsModuleResponse);
1467
- });
1468
-
1469
- MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
1470
- // simulate a callback with the quote response
1471
- emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
1472
- return Promise.resolve(dummyRequestsModuleResponse);
1473
- });
1474
-
1475
- MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
1476
- // simulate an error callback with the transfer fulfilment
1477
- cache.publish(`tf_${postTransfersBody.transferId}`, JSON.stringify(expectError));
1478
- return Promise.resolve(dummyRequestsModuleResponse);
1479
- });
1480
-
1481
- const model = new Model({
1482
- cache,
1483
- logger,
1484
- metricsClient,
1485
- ...config,
1486
- });
1487
-
1488
- await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
1489
-
1490
- expect(StateMachine.__instance.state).toBe('start');
1491
-
1492
- const errMsg = 'Got an error response preparing transfer: { errorInformation:\n { errorCode: \'4001\',\n errorDescription: \'Payer FSP insufficient liquidity\' } }';
1493
-
1494
- try {
1495
- await model.run();
1496
- }
1497
- catch(err) {
1498
- expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
1499
- expect(err.transferState).toBeTruthy();
1500
- expect(err.transferState.lastError).toBeTruthy();
1501
- expect(err.transferState.lastError.mojaloopError).toEqual(expectError.data.body);
1502
- expect(err.transferState.lastError.transferState).toBe(undefined);
1503
- return;
1504
- }
1505
-
1506
- throw new Error('Outbound model should have thrown');
1507
- });
1508
-
1509
-
1510
- async function testTlsServer(enableTls) {
1511
- config.outbound.tls.mutualTLS.enabled = enableTls;
1512
-
1513
- new Model({
1514
- cache,
1515
- logger,
1516
- metricsClient,
1517
- ...config
1518
- });
1519
-
1520
- const scheme = enableTls ? 'https' : 'http';
1521
- expect(MojaloopRequests.__instance.transportScheme).toBe(scheme);
1522
- }
1523
-
1524
- test('Outbound server should use HTTPS if outbound mTLS enabled', () =>
1525
- testTlsServer(true));
1526
-
1527
- test('Outbound server should use HTTP if outbound mTLS disabled', () =>
1528
- testTlsServer(false));
1529
-
1530
- test('Outbound transfers model should record metrics', async () => {
1531
- const metrics = await metricsClient._prometheusRegister.metrics();
1532
- expect(metrics).toBeTruthy();
1533
-
1534
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_request_count'));
1535
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_response_count'));
1536
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_request_count'));
1537
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_response_count'));
1538
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_prepare_count'));
1539
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_fulfil_response_count'));
1540
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_request_latency'));
1541
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_latency'));
1542
- expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_latency'));
1543
- });
1544
-
1545
- test('skips resolving party when to.fspid is specified and skipPartyLookup is truthy', async () => {
1546
- config.autoAcceptParty = false;
1547
- config.autoAcceptQuotes = false;
1548
-
1549
- let model = new Model({
1550
- cache,
1551
- logger,
1552
- metricsClient,
1553
- ...config,
1554
- });
1555
-
1556
- let req = JSON.parse(JSON.stringify(transferRequest));
1557
- const testFspId = 'TESTDESTFSPID';
1558
- req.to.fspId = testFspId;
1559
- req.skipPartyLookup = true;
1560
-
1561
- await model.initialize(req);
1562
-
1563
- expect(StateMachine.__instance.state).toBe('start');
1564
-
1565
- // start the model running
1566
- let resultPromise = model.run();
1567
-
1568
- // now we started the model running we simulate a callback with the quote response
1569
- cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
1570
-
1571
- // wait for the model to reach a terminal state
1572
- let result = await resultPromise;
1573
-
1574
- // check we stopped at quoteReceived state
1575
- expect(result.currentState).toBe(SDKStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE);
1576
- expect(StateMachine.__instance.state).toBe('quoteReceived');
1577
- });
1578
-
1579
- });