@mojaloop/sdk-scheme-adapter 12.2.2 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/CHANGELOG.md +26 -0
- package/audit-resolve.json +71 -1
- package/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json +2 -2
- package/docker/ml-testing-toolkit/spec_files/rules_callback/default.json +7 -7
- package/docker/ml-testing-toolkit/spec_files/rules_response/default.json +16 -16
- package/docker/ml-testing-toolkit/spec_files/rules_response/default_pisp_rules.json +5 -5
- package/docker/ml-testing-toolkit/spec_files/rules_validation/default.json +10 -10
- package/package.json +4 -1
- package/src/ControlAgent/index.js +2 -3
- package/src/ControlServer/index.js +2 -2
- package/src/InboundServer/handlers.js +114 -52
- package/src/InboundServer/index.js +7 -7
- package/src/InboundServer/middlewares.js +2 -2
- package/src/OutboundServer/api.yaml +54 -3
- package/src/OutboundServer/api_interfaces/openapi.d.ts +24 -3
- package/src/OutboundServer/api_template/components/schemas/accountsResponse.yaml +9 -0
- package/src/OutboundServer/api_template/components/schemas/transferRequest.yaml +3 -0
- package/src/OutboundServer/api_template/components/schemas/transferResponse.yaml +28 -2
- package/src/OutboundServer/api_template/components/schemas/transferStatusResponse.yaml +8 -1
- package/src/OutboundServer/handlers.js +4 -1
- package/src/OutboundServer/index.js +10 -11
- package/src/config.js +29 -12
- package/src/index.js +198 -10
- package/src/lib/cache.js +110 -52
- package/src/lib/metrics.js +148 -0
- package/src/lib/model/AccountsModel.js +17 -12
- package/src/lib/model/Async2SyncModel.js +4 -1
- package/src/lib/model/InboundTransfersModel.js +170 -25
- package/src/lib/model/OutboundBulkQuotesModel.js +4 -1
- package/src/lib/model/OutboundBulkTransfersModel.js +4 -1
- package/src/lib/model/OutboundRequestToPayModel.js +9 -7
- package/src/lib/model/OutboundRequestToPayTransferModel.js +6 -3
- package/src/lib/model/OutboundTransfersModel.js +318 -53
- package/src/lib/model/PartiesModel.js +1 -1
- package/src/lib/model/ProxyModel/index.js +4 -2
- package/src/lib/model/common/BackendError.js +28 -4
- package/src/lib/model/common/index.js +2 -1
- package/src/lib/validate.js +2 -2
- package/test/__mocks__/@mojaloop/sdk-standard-components.js +3 -2
- package/test/__mocks__/redis.js +4 -0
- package/test/config/integration.env +5 -0
- package/test/integration/lib/Outbound/parties.test.js +1 -1
- package/test/unit/ControlServer/index.js +3 -3
- package/test/unit/InboundServer.test.js +10 -10
- package/test/unit/TestServer.test.js +11 -13
- package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +11 -3
- package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +14 -0
- package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +13 -0
- package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +18 -0
- package/test/unit/api/accounts/utils.js +15 -1
- package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +18 -15
- package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +1 -0
- package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +9 -0
- package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +1 -0
- package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +74 -47
- package/test/unit/api/transfers/utils.js +85 -4
- package/test/unit/api/utils.js +4 -1
- package/test/unit/config.test.js +2 -2
- package/test/unit/data/commonHttpHeaders.json +1 -0
- package/test/unit/data/defaultConfig.json +23 -7
- package/test/unit/inboundApi/handlers.test.js +45 -14
- package/test/unit/index.test.js +95 -4
- package/test/unit/lib/model/AccountsModel.test.js +9 -6
- package/test/unit/lib/model/InboundTransfersModel.test.js +210 -30
- package/test/unit/lib/model/OutboundRequestToPayModel.test.js +1 -1
- package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +3 -3
- package/test/unit/lib/model/OutboundTransfersModel.test.js +863 -158
- package/test/unit/lib/model/data/defaultConfig.json +25 -10
- package/test/unit/lib/model/data/mockArguments.json +97 -40
- package/test/unit/lib/model/data/payeeParty.json +13 -11
- package/test/unit/lib/model/data/quoteResponse.json +36 -25
- package/test/unit/lib/model/data/transferFulfil.json +5 -3
- package/src/lib/api/index.js +0 -12
- package/src/lib/randomphrase/index.js +0 -21
- package/src/lib/randomphrase/words.json +0 -3397
|
@@ -15,6 +15,7 @@ jest.mock('@mojaloop/sdk-standard-components');
|
|
|
15
15
|
jest.mock('redis');
|
|
16
16
|
|
|
17
17
|
const Cache = require('~/lib/cache');
|
|
18
|
+
const { MetricsClient } = require('~/lib/metrics');
|
|
18
19
|
const Model = require('~/lib/model').OutboundTransfersModel;
|
|
19
20
|
const PartiesModel = require('~/lib/model').PartiesModel;
|
|
20
21
|
|
|
@@ -28,7 +29,7 @@ const quoteResponseTemplate = require('./data/quoteResponse');
|
|
|
28
29
|
const transferFulfil = require('./data/transferFulfil');
|
|
29
30
|
|
|
30
31
|
const genPartyId = (party) => {
|
|
31
|
-
const { partyIdType, partyIdentifier, partySubIdOrType } = party.party.partyIdInfo;
|
|
32
|
+
const { partyIdType, partyIdentifier, partySubIdOrType } = party.body.party.partyIdInfo;
|
|
32
33
|
return PartiesModel.channelName({
|
|
33
34
|
type: partyIdType,
|
|
34
35
|
id: partyIdentifier,
|
|
@@ -38,6 +39,7 @@ const genPartyId = (party) => {
|
|
|
38
39
|
|
|
39
40
|
// util function to simulate a party resolution subscription message on a cache client
|
|
40
41
|
const emitPartyCacheMessage = (cache, party) => cache.publish(genPartyId(party), JSON.stringify(party));
|
|
42
|
+
const emitMultiPartiesCacheMessage = (cache, party) => cache.add(genPartyId(party), JSON.stringify(party));
|
|
41
43
|
|
|
42
44
|
// util function to simulate a quote response subscription message on a cache client
|
|
43
45
|
const emitQuoteResponseCacheMessage = (cache, quoteId, quoteResponse) => cache.publish(`qt_${quoteId}`, JSON.stringify(quoteResponse));
|
|
@@ -45,11 +47,16 @@ const emitQuoteResponseCacheMessage = (cache, quoteId, quoteResponse) => cache.p
|
|
|
45
47
|
// util function to simulate a transfer fulfilment subscription message on a cache client
|
|
46
48
|
const emitTransferFulfilCacheMessage = (cache, transferId, fulfil) => cache.publish(`tf_${transferId}`, JSON.stringify(fulfil));
|
|
47
49
|
|
|
50
|
+
const dummyRequestsModuleResponse = {
|
|
51
|
+
originalRequest: {}
|
|
52
|
+
};
|
|
53
|
+
|
|
48
54
|
describe('outboundModel', () => {
|
|
49
55
|
let quoteResponse;
|
|
50
56
|
let config;
|
|
51
57
|
let logger;
|
|
52
58
|
let cache;
|
|
59
|
+
let metricsClient;
|
|
53
60
|
|
|
54
61
|
/**
|
|
55
62
|
*
|
|
@@ -71,13 +78,27 @@ describe('outboundModel', () => {
|
|
|
71
78
|
config.rejectExpiredTransferFulfils = rejects.transferFulfils;
|
|
72
79
|
|
|
73
80
|
// simulate a callback with the resolved party
|
|
74
|
-
MojaloopRequests.__getParties = jest.fn(() =>
|
|
81
|
+
MojaloopRequests.__getParties = jest.fn(() => {
|
|
82
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
83
|
+
return {
|
|
84
|
+
originalRequest: {
|
|
85
|
+
headers: [],
|
|
86
|
+
body: {},
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
});
|
|
75
90
|
|
|
76
91
|
// simulate a delayed callback with the quote response
|
|
77
92
|
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
78
93
|
setTimeout(() => {
|
|
79
94
|
emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
|
|
80
95
|
}, delays.requestQuotes ? delays.requestQuotes * 1000 : 0);
|
|
96
|
+
return {
|
|
97
|
+
originalRequest: {
|
|
98
|
+
headers: [],
|
|
99
|
+
body: postQuotesBody,
|
|
100
|
+
}
|
|
101
|
+
};
|
|
81
102
|
});
|
|
82
103
|
|
|
83
104
|
// simulate a delayed callback with the transfer fulfilment
|
|
@@ -85,12 +106,19 @@ describe('outboundModel', () => {
|
|
|
85
106
|
setTimeout(() => {
|
|
86
107
|
emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
|
|
87
108
|
}, delays.prepareTransfer ? delays.prepareTransfer * 1000 : 0);
|
|
109
|
+
return {
|
|
110
|
+
originalRequest: {
|
|
111
|
+
headers: [],
|
|
112
|
+
body: postTransfersBody,
|
|
113
|
+
}
|
|
114
|
+
};
|
|
88
115
|
});
|
|
89
116
|
|
|
90
117
|
const model = new Model({
|
|
91
118
|
...config,
|
|
92
119
|
cache,
|
|
93
120
|
logger,
|
|
121
|
+
metricsClient,
|
|
94
122
|
});
|
|
95
123
|
|
|
96
124
|
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
@@ -113,17 +141,27 @@ describe('outboundModel', () => {
|
|
|
113
141
|
beforeAll(async () => {
|
|
114
142
|
logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' });
|
|
115
143
|
quoteResponse = JSON.parse(JSON.stringify(quoteResponseTemplate));
|
|
144
|
+
metricsClient = new MetricsClient();
|
|
116
145
|
});
|
|
117
146
|
|
|
118
147
|
beforeEach(async () => {
|
|
119
148
|
config = JSON.parse(JSON.stringify(defaultConfig));
|
|
120
|
-
MojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve());
|
|
121
|
-
MojaloopRequests.__getParties = jest.fn(() => Promise.resolve());
|
|
122
|
-
MojaloopRequests.
|
|
123
|
-
MojaloopRequests.
|
|
124
|
-
MojaloopRequests.
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
MojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
|
|
150
|
+
MojaloopRequests.__getParties = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
|
|
151
|
+
MojaloopRequests.__putQuotes = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
|
|
152
|
+
MojaloopRequests.__putQuotesError = jest.fn(() => Promise.resolve(dummyRequestsModuleResponse));
|
|
153
|
+
MojaloopRequests.__postQuotes = jest.fn((body) => Promise.resolve({
|
|
154
|
+
originalRequest: {
|
|
155
|
+
headers: [],
|
|
156
|
+
body: body,
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
MojaloopRequests.__postTransfers = jest.fn((body) => Promise.resolve({
|
|
160
|
+
originalRequest: {
|
|
161
|
+
headers: [],
|
|
162
|
+
body: body,
|
|
163
|
+
}
|
|
164
|
+
}));
|
|
127
165
|
cache = new Cache({
|
|
128
166
|
host: 'dummycachehost',
|
|
129
167
|
port: 1234,
|
|
@@ -140,6 +178,7 @@ describe('outboundModel', () => {
|
|
|
140
178
|
const model = new Model({
|
|
141
179
|
cache,
|
|
142
180
|
logger,
|
|
181
|
+
metricsClient,
|
|
143
182
|
...config,
|
|
144
183
|
});
|
|
145
184
|
|
|
@@ -147,14 +186,13 @@ describe('outboundModel', () => {
|
|
|
147
186
|
expect(StateMachine.__instance.state).toBe('start');
|
|
148
187
|
});
|
|
149
188
|
|
|
150
|
-
|
|
151
189
|
test('executes all three transfer stages without halting when AUTO_ACCEPT_PARTY and AUTO_ACCEPT_QUOTES are true', async () => {
|
|
152
190
|
config.autoAcceptParty = true;
|
|
153
191
|
config.autoAcceptQuotes = true;
|
|
154
192
|
|
|
155
193
|
MojaloopRequests.__getParties = jest.fn(() => {
|
|
156
194
|
emitPartyCacheMessage(cache, payeeParty);
|
|
157
|
-
return Promise.resolve();
|
|
195
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
158
196
|
});
|
|
159
197
|
|
|
160
198
|
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
@@ -168,13 +206,13 @@ describe('outboundModel', () => {
|
|
|
168
206
|
|
|
169
207
|
// simulate a callback with the quote response
|
|
170
208
|
emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
|
|
171
|
-
return Promise.resolve();
|
|
209
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
172
210
|
});
|
|
173
211
|
|
|
174
212
|
MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
|
|
175
213
|
//ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
|
|
176
214
|
// set as the destination FSPID, picked up from the header's value `fspiop-source`
|
|
177
|
-
expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
|
|
215
|
+
expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
178
216
|
|
|
179
217
|
const extensionList = postTransfersBody.extensionList.extension;
|
|
180
218
|
expect(extensionList).toBeTruthy();
|
|
@@ -182,98 +220,667 @@ describe('outboundModel', () => {
|
|
|
182
220
|
expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
|
|
183
221
|
expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
|
|
184
222
|
|
|
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);
|
|
223
|
+
expect(destFspId).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
224
|
+
expect(model.data.to.fspId).toBe(payeeParty.body.party.partyIdInfo.fspId);
|
|
225
|
+
expect(quoteResponse.data.headers['fspiop-source']).not.toBe(model.data.to.fspId);
|
|
188
226
|
|
|
189
227
|
// simulate a callback with the transfer fulfilment
|
|
190
228
|
emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
|
|
229
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const model = new Model({
|
|
233
|
+
cache,
|
|
234
|
+
logger,
|
|
235
|
+
metricsClient,
|
|
236
|
+
...config,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
240
|
+
|
|
241
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
242
|
+
|
|
243
|
+
// start the model running
|
|
244
|
+
const result = await model.run();
|
|
245
|
+
|
|
246
|
+
expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
|
|
248
|
+
expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
|
|
249
|
+
|
|
250
|
+
// make sure no PATCH was sent as we did not set config or receive a RESERVED state
|
|
251
|
+
expect(MojaloopRequests.__patchTransfers).toHaveBeenCalledTimes(0);
|
|
252
|
+
|
|
253
|
+
// check we stopped at payeeResolved state
|
|
254
|
+
expect(result.currentState).toBe('COMPLETED');
|
|
255
|
+
expect(StateMachine.__instance.state).toBe('succeeded');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('sends a PATCH /transfers/{transferId} request to payee DFSP when SEND_FINAL_NOTIFICATION_IF_REQUESTED is true', async () => {
|
|
259
|
+
config.autoAcceptParty = true;
|
|
260
|
+
config.autoAcceptQuotes = true;
|
|
261
|
+
config.sendFinalNotificationIfRequested = true;
|
|
262
|
+
MojaloopRequests.__getParties = jest.fn(() => {
|
|
263
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
264
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
265
|
+
});
|
|
266
|
+
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
267
|
+
// ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
|
|
268
|
+
// including extension list
|
|
269
|
+
const extensionList = postQuotesBody.extensionList.extension;
|
|
270
|
+
expect(extensionList).toBeTruthy();
|
|
271
|
+
expect(extensionList.length).toBe(2);
|
|
272
|
+
expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
|
|
273
|
+
expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
|
|
274
|
+
// simulate a callback with the quote response
|
|
275
|
+
emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
|
|
276
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
277
|
+
});
|
|
278
|
+
const pb = JSON.parse(JSON.stringify(transferFulfil));
|
|
279
|
+
pb.data.body.transferState = 'RESERVED';
|
|
280
|
+
MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
|
|
281
|
+
//ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
|
|
282
|
+
// set as the destination FSPID, picked up from the header's value `fspiop-source`
|
|
283
|
+
expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
284
|
+
const extensionList = postTransfersBody.extensionList.extension;
|
|
285
|
+
expect(extensionList).toBeTruthy();
|
|
286
|
+
expect(extensionList.length).toBe(2);
|
|
287
|
+
expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
|
|
288
|
+
expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
|
|
289
|
+
expect(destFspId).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
290
|
+
expect(model.data.to.fspId).toBe(payeeParty.body.party.partyIdInfo.fspId);
|
|
291
|
+
expect(quoteResponse.data.headers['fspiop-source']).not.toBe(model.data.to.fspId);
|
|
292
|
+
// simulate a callback with the transfer fulfilment
|
|
293
|
+
emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, pb);
|
|
294
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
295
|
+
});
|
|
296
|
+
const model = new Model({
|
|
297
|
+
cache,
|
|
298
|
+
logger,
|
|
299
|
+
metricsClient,
|
|
300
|
+
...config,
|
|
301
|
+
});
|
|
302
|
+
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
303
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
304
|
+
// start the model running
|
|
305
|
+
const result = await model.run();
|
|
306
|
+
expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
|
|
307
|
+
expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
|
|
308
|
+
expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(MojaloopRequests.__patchTransfers).toHaveBeenCalledTimes(1);
|
|
310
|
+
expect(MojaloopRequests.__patchTransfers.mock.calls[0][0]).toEqual(model.data.transferId);
|
|
311
|
+
expect(MojaloopRequests.__patchTransfers.mock.calls[0][1].transferState).toEqual('COMMITTED');
|
|
312
|
+
expect(MojaloopRequests.__patchTransfers.mock.calls[0][1].completedTimestamp).not.toBeUndefined();
|
|
313
|
+
expect(MojaloopRequests.__patchTransfers.mock.calls[0][2]).toEqual(quoteResponse.data.headers['fspiop-source']);
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
// check we stopped at payeeResolved state
|
|
317
|
+
expect(result.currentState).toBe('COMPLETED');
|
|
318
|
+
expect(StateMachine.__instance.state).toBe('succeeded');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('uses quote response transfer amount for transfer prepare', async () => {
|
|
322
|
+
config.autoAcceptParty = true;
|
|
323
|
+
config.autoAcceptQuotes = true;
|
|
324
|
+
|
|
325
|
+
MojaloopRequests.__getParties = jest.fn(() => {
|
|
326
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
327
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// change the the transfer amount and currency in the quote response
|
|
331
|
+
// so it is different to the initial request
|
|
332
|
+
quoteResponse.data.body.transferAmount = {
|
|
333
|
+
currency: 'XYZ',
|
|
334
|
+
amount: '9876543210'
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
expect(quoteResponse.data.body.transferAmount).not.toEqual({
|
|
338
|
+
amount: transferRequest.amount,
|
|
339
|
+
currency: transferRequest.currency
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
343
|
+
// ensure that the `MojaloopRequests.postQuotes` method has been called with correct arguments
|
|
344
|
+
// including extension list
|
|
345
|
+
const extensionList = postQuotesBody.extensionList.extension;
|
|
346
|
+
expect(extensionList).toBeTruthy();
|
|
347
|
+
expect(extensionList.length).toBe(2);
|
|
348
|
+
expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' });
|
|
349
|
+
expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' });
|
|
350
|
+
|
|
351
|
+
// simulate a callback with the quote response
|
|
352
|
+
emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
|
|
353
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
|
|
357
|
+
//ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
|
|
358
|
+
// set as the destination FSPID, picked up from the header's value `fspiop-source`
|
|
359
|
+
expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
360
|
+
|
|
361
|
+
const extensionList = postTransfersBody.extensionList.extension;
|
|
362
|
+
expect(extensionList).toBeTruthy();
|
|
363
|
+
expect(extensionList.length).toBe(2);
|
|
364
|
+
expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
|
|
365
|
+
expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
|
|
366
|
+
|
|
367
|
+
expect(destFspId).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
368
|
+
expect(model.data.to.fspId).toBe(payeeParty.body.party.partyIdInfo.fspId);
|
|
369
|
+
expect(quoteResponse.data.headers['fspiop-source']).not.toBe(model.data.to.fspId);
|
|
370
|
+
|
|
371
|
+
expect(postTransfersBody.amount).toEqual(quoteResponse.data.body.transferAmount);
|
|
372
|
+
|
|
373
|
+
// simulate a callback with the transfer fulfilment
|
|
374
|
+
emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
|
|
375
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const model = new Model({
|
|
379
|
+
cache,
|
|
380
|
+
logger,
|
|
381
|
+
metricsClient,
|
|
382
|
+
...config,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
386
|
+
|
|
387
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
388
|
+
|
|
389
|
+
// start the model running
|
|
390
|
+
const result = await model.run();
|
|
391
|
+
|
|
392
|
+
expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1);
|
|
393
|
+
expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1);
|
|
394
|
+
expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
|
|
395
|
+
|
|
396
|
+
// make sure no PATCH was sent as we did not set config or receive a RESERVED state
|
|
397
|
+
expect(MojaloopRequests.__patchTransfers).toHaveBeenCalledTimes(0);
|
|
398
|
+
|
|
399
|
+
// check we stopped at payeeResolved state
|
|
400
|
+
expect(result.currentState).toBe('COMPLETED');
|
|
401
|
+
expect(StateMachine.__instance.state).toBe('succeeded');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('test get transfer', async () => {
|
|
405
|
+
MojaloopRequests.__getTransfers = jest.fn((transferId) => {
|
|
406
|
+
emitTransferFulfilCacheMessage(cache, transferId, transferFulfil);
|
|
191
407
|
return Promise.resolve();
|
|
192
408
|
});
|
|
193
409
|
|
|
194
410
|
const model = new Model({
|
|
195
411
|
cache,
|
|
196
412
|
logger,
|
|
413
|
+
metricsClient,
|
|
414
|
+
...config,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const TRANSFER_ID = 'tx-id000011';
|
|
418
|
+
|
|
419
|
+
await model.initialize(JSON.parse(JSON.stringify({
|
|
420
|
+
...transferRequest,
|
|
421
|
+
currentState: 'getTransfer',
|
|
422
|
+
transferId: TRANSFER_ID,
|
|
423
|
+
})));
|
|
424
|
+
|
|
425
|
+
expect(StateMachine.__instance.state).toBe('getTransfer');
|
|
426
|
+
|
|
427
|
+
// start the model running
|
|
428
|
+
const result = await model.run();
|
|
429
|
+
|
|
430
|
+
expect(MojaloopRequests.__getTransfers).toHaveBeenCalledTimes(1);
|
|
431
|
+
|
|
432
|
+
// check we stopped at payeeResolved state
|
|
433
|
+
expect(result.currentState).toBe('COMPLETED');
|
|
434
|
+
expect(StateMachine.__instance.state).toBe('succeeded');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
test('resolves payee and halts when AUTO_ACCEPT_PARTY is false', async () => {
|
|
439
|
+
config.autoAcceptParty = false;
|
|
440
|
+
|
|
441
|
+
const model = new Model({
|
|
442
|
+
cache,
|
|
443
|
+
logger,
|
|
444
|
+
metricsClient,
|
|
445
|
+
...config,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
449
|
+
|
|
450
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
451
|
+
|
|
452
|
+
// start the model running
|
|
453
|
+
const resultPromise = model.run();
|
|
454
|
+
|
|
455
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
456
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
457
|
+
|
|
458
|
+
// wait for the model to reach a terminal state
|
|
459
|
+
const result = await resultPromise;
|
|
460
|
+
|
|
461
|
+
// check we stopped at payeeResolved state
|
|
462
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
463
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('uses payee party fspid as source header when supplied - resolving payee', async () => {
|
|
467
|
+
config.autoAcceptParty = false;
|
|
468
|
+
|
|
469
|
+
const model = new Model({
|
|
470
|
+
cache,
|
|
471
|
+
logger,
|
|
472
|
+
metricsClient,
|
|
473
|
+
...config,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
let req = JSON.parse(JSON.stringify(transferRequest));
|
|
477
|
+
const testFspId = 'TESTDESTFSPID';
|
|
478
|
+
req.to.fspId = testFspId;
|
|
479
|
+
|
|
480
|
+
await model.initialize(req);
|
|
481
|
+
|
|
482
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
483
|
+
|
|
484
|
+
// start the model running
|
|
485
|
+
const resultPromise = model.run();
|
|
486
|
+
|
|
487
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
488
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
489
|
+
|
|
490
|
+
// wait for the model to reach a terminal state
|
|
491
|
+
const result = await resultPromise;
|
|
492
|
+
|
|
493
|
+
// check we stopped at payeeResolved state
|
|
494
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
495
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
496
|
+
|
|
497
|
+
// check getParties mojaloop requests method was called with the correct arguments
|
|
498
|
+
expect(MojaloopRequests.__getParties).toHaveBeenCalledWith(req.to.idType, req.to.idValue, req.to.idSubValue, testFspId);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('resolves multiple payees and halts', async () => {
|
|
502
|
+
config.autoAcceptParty = false;
|
|
503
|
+
config.multiplePartiesResponse = true;
|
|
504
|
+
config.multiplePartiesResponseSeconds = 2;
|
|
505
|
+
const model = new Model({
|
|
506
|
+
cache,
|
|
507
|
+
logger,
|
|
508
|
+
metricsClient,
|
|
509
|
+
...config,
|
|
510
|
+
});
|
|
511
|
+
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
512
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
513
|
+
// start the model running
|
|
514
|
+
const resultPromise = model.run();
|
|
515
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
516
|
+
const payeeParty1 = JSON.parse(JSON.stringify(payeeParty));
|
|
517
|
+
payeeParty1.body.party.partyIdInfo.fspId = 'FirstFspId';
|
|
518
|
+
await emitMultiPartiesCacheMessage(cache, payeeParty1);
|
|
519
|
+
const payeeParty2 = JSON.parse(JSON.stringify(payeeParty));
|
|
520
|
+
payeeParty2.body.party.partyIdInfo.fspId = 'SecondFspId';
|
|
521
|
+
await emitMultiPartiesCacheMessage(cache, payeeParty2);
|
|
522
|
+
// wait for the model to reach a terminal state
|
|
523
|
+
const result = await resultPromise;
|
|
524
|
+
// check we stopped at payeeResolved state
|
|
525
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
526
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
527
|
+
expect(result.to[0].fspId).toEqual('FirstFspId');
|
|
528
|
+
expect(result.to[1].fspId).toEqual('SecondFspId');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
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 () => {
|
|
532
|
+
config.autoAcceptParty = false;
|
|
533
|
+
config.autoAcceptQuotes = false;
|
|
534
|
+
|
|
535
|
+
let model = new Model({
|
|
536
|
+
cache,
|
|
537
|
+
logger,
|
|
538
|
+
metricsClient,
|
|
539
|
+
...config,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
543
|
+
|
|
544
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
545
|
+
|
|
546
|
+
// start the model running
|
|
547
|
+
let resultPromise = model.run();
|
|
548
|
+
|
|
549
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
550
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
551
|
+
|
|
552
|
+
// wait for the model to reach a terminal state
|
|
553
|
+
let result = await resultPromise;
|
|
554
|
+
|
|
555
|
+
// check we stopped at payeeResolved state
|
|
556
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
557
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
558
|
+
|
|
559
|
+
const transferId = result.transferId;
|
|
560
|
+
|
|
561
|
+
// load a new model from the saved state
|
|
562
|
+
model = new Model({
|
|
563
|
+
cache,
|
|
564
|
+
logger,
|
|
565
|
+
metricsClient,
|
|
566
|
+
...config,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
await model.load(transferId);
|
|
570
|
+
|
|
571
|
+
// check the model loaded to the correct state
|
|
572
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
573
|
+
|
|
574
|
+
// now run the model again. this should trigger transition to quote request
|
|
575
|
+
resultPromise = model.run({ acceptParty: true });
|
|
576
|
+
// now we started the model running we simulate a callback with the quote response
|
|
577
|
+
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
578
|
+
|
|
579
|
+
// wait for the model to reach a terminal state
|
|
580
|
+
result = await resultPromise;
|
|
581
|
+
|
|
582
|
+
// check we stopped at payeeResolved state
|
|
583
|
+
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
584
|
+
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('Allows change of transferAmount at accept party phase', async () => {
|
|
588
|
+
config.autoAcceptParty = false;
|
|
589
|
+
config.autoAcceptQuotes = false;
|
|
590
|
+
|
|
591
|
+
let model = new Model({
|
|
592
|
+
cache,
|
|
593
|
+
logger,
|
|
594
|
+
metricsClient,
|
|
595
|
+
...config,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const req = JSON.parse(JSON.stringify(transferRequest));
|
|
599
|
+
|
|
600
|
+
// record the initial requested transfer amount
|
|
601
|
+
const initialAmount = req.amount;
|
|
602
|
+
|
|
603
|
+
await model.initialize(req);
|
|
604
|
+
|
|
605
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
606
|
+
|
|
607
|
+
// start the model running
|
|
608
|
+
let resultPromise = model.run();
|
|
609
|
+
|
|
610
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
611
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
612
|
+
|
|
613
|
+
// wait for the model to reach a terminal state
|
|
614
|
+
let result = await resultPromise;
|
|
615
|
+
|
|
616
|
+
// check we stopped at payeeResolved state
|
|
617
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
618
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
619
|
+
|
|
620
|
+
expect(result.amount).toEqual(initialAmount);
|
|
621
|
+
|
|
622
|
+
const transferId = result.transferId;
|
|
623
|
+
|
|
624
|
+
// load a new model from the saved state
|
|
625
|
+
model = new Model({
|
|
626
|
+
cache,
|
|
627
|
+
logger,
|
|
628
|
+
metricsClient,
|
|
629
|
+
...config,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await model.load(transferId);
|
|
633
|
+
|
|
634
|
+
// check the model loaded to the correct state
|
|
635
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
636
|
+
|
|
637
|
+
const resume = {
|
|
638
|
+
amount: 999,
|
|
639
|
+
acceptParty: true,
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// now run the model again. this should trigger transition to quote request
|
|
643
|
+
resultPromise = model.run(resume);
|
|
644
|
+
|
|
645
|
+
// now we started the model running we simulate a callback with the quote response
|
|
646
|
+
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
647
|
+
|
|
648
|
+
// wait for the model to reach a terminal state
|
|
649
|
+
result = await resultPromise;
|
|
650
|
+
|
|
651
|
+
// check we stopped at quoteReceived state
|
|
652
|
+
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
653
|
+
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
654
|
+
|
|
655
|
+
// check the accept party key got merged to the state
|
|
656
|
+
expect(result.acceptParty).toEqual(true);
|
|
657
|
+
|
|
658
|
+
// check the amount key got changed
|
|
659
|
+
expect(result.amount).toEqual(resume.amount);
|
|
660
|
+
|
|
661
|
+
// check the quote request amount is the NEW amount, not the initial amount
|
|
662
|
+
expect(result.quoteRequest.body.amount.amount).toStrictEqual(resume.amount);
|
|
663
|
+
expect(result.quoteRequest.body.amount.amount).not.toEqual(initialAmount);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('Allows change of payee party at accept party phase (round-robin support)', async () => {
|
|
667
|
+
config.autoAcceptParty = false;
|
|
668
|
+
config.autoAcceptQuotes = false;
|
|
669
|
+
|
|
670
|
+
let model = new Model({
|
|
671
|
+
cache,
|
|
672
|
+
logger,
|
|
673
|
+
metricsClient,
|
|
674
|
+
...config,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const req = JSON.parse(JSON.stringify(transferRequest));
|
|
678
|
+
|
|
679
|
+
// record the initial requested transfer amount
|
|
680
|
+
const initialAmount = req.amount;
|
|
681
|
+
|
|
682
|
+
await model.initialize(req);
|
|
683
|
+
|
|
684
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
685
|
+
|
|
686
|
+
// start the model running
|
|
687
|
+
let resultPromise = model.run();
|
|
688
|
+
|
|
689
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
690
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
691
|
+
|
|
692
|
+
// wait for the model to reach a terminal state
|
|
693
|
+
let result = await resultPromise;
|
|
694
|
+
|
|
695
|
+
// check we stopped at payeeResolved state
|
|
696
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
697
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
698
|
+
|
|
699
|
+
expect(result.amount).toEqual(initialAmount);
|
|
700
|
+
|
|
701
|
+
const transferId = result.transferId;
|
|
702
|
+
|
|
703
|
+
// load a new model from the saved state
|
|
704
|
+
model = new Model({
|
|
705
|
+
cache,
|
|
706
|
+
logger,
|
|
707
|
+
metricsClient,
|
|
708
|
+
...config,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
await model.load(transferId);
|
|
712
|
+
|
|
713
|
+
// check the model loaded to the correct state
|
|
714
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
715
|
+
|
|
716
|
+
const newPayee = {
|
|
717
|
+
partyIdInfo: {
|
|
718
|
+
partySubIdOrType: undefined,
|
|
719
|
+
partyIdType: 'PASSPORT',
|
|
720
|
+
partyIdentifier: 'AAABBBCCCDDDEEE',
|
|
721
|
+
fspId: 'TESTDFSP'
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const newPayeeInternal = {
|
|
726
|
+
idType: newPayee.partyIdInfo.partyIdType,
|
|
727
|
+
idValue: newPayee.partyIdInfo.partyIdentifier,
|
|
728
|
+
fspId: newPayee.partyIdInfo.fspId,
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const resume = {
|
|
732
|
+
acceptParty: true,
|
|
733
|
+
to: newPayeeInternal,
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// now run the model again. this should trigger transition to quote request
|
|
737
|
+
resultPromise = model.run(resume);
|
|
738
|
+
|
|
739
|
+
// now we started the model running we simulate a callback with the quote response
|
|
740
|
+
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
741
|
+
|
|
742
|
+
// wait for the model to reach a terminal state
|
|
743
|
+
result = await resultPromise;
|
|
744
|
+
|
|
745
|
+
// check we stopped at quoteReceived state
|
|
746
|
+
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
747
|
+
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
748
|
+
|
|
749
|
+
// check the accept party key got merged to the state
|
|
750
|
+
expect(result.acceptParty).toEqual(true);
|
|
751
|
+
|
|
752
|
+
// check the "to" passed in to model resume is merged into the model state correctly
|
|
753
|
+
expect(result.to).toStrictEqual(newPayeeInternal);
|
|
754
|
+
|
|
755
|
+
// check the quote request payee party is the NEW one, not the initial one.
|
|
756
|
+
expect(result.quoteRequest.body.payee).toStrictEqual(newPayee);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('Does not merge resume data keys into state that are not permitted', async () => {
|
|
760
|
+
config.autoAcceptParty = false;
|
|
761
|
+
config.autoAcceptQuotes = false;
|
|
762
|
+
|
|
763
|
+
let model = new Model({
|
|
764
|
+
cache,
|
|
765
|
+
logger,
|
|
766
|
+
metricsClient,
|
|
767
|
+
...config,
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const req = JSON.parse(JSON.stringify(transferRequest));
|
|
771
|
+
|
|
772
|
+
// record the initial requested transfer amount
|
|
773
|
+
const initialAmount = req.amount;
|
|
774
|
+
|
|
775
|
+
await model.initialize(req);
|
|
776
|
+
|
|
777
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
778
|
+
|
|
779
|
+
// start the model running
|
|
780
|
+
let resultPromise = model.run();
|
|
781
|
+
|
|
782
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
783
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
784
|
+
|
|
785
|
+
// wait for the model to reach a terminal state
|
|
786
|
+
let result = await resultPromise;
|
|
787
|
+
|
|
788
|
+
// check we stopped at payeeResolved state
|
|
789
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
790
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
791
|
+
|
|
792
|
+
expect(result.amount).toEqual(initialAmount);
|
|
793
|
+
|
|
794
|
+
const transferId = result.transferId;
|
|
795
|
+
|
|
796
|
+
// load a new model from the saved state
|
|
797
|
+
model = new Model({
|
|
798
|
+
cache,
|
|
799
|
+
logger,
|
|
800
|
+
metricsClient,
|
|
197
801
|
...config,
|
|
198
802
|
});
|
|
199
803
|
|
|
200
|
-
await model.
|
|
804
|
+
await model.load(transferId);
|
|
201
805
|
|
|
202
|
-
|
|
806
|
+
// check the model loaded to the correct state
|
|
807
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
203
808
|
|
|
204
|
-
|
|
205
|
-
|
|
809
|
+
const resume = {
|
|
810
|
+
amount: 999,
|
|
811
|
+
acceptParty: true,
|
|
812
|
+
someRandomKey: 'this key name is not permitted',
|
|
813
|
+
};
|
|
206
814
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
|
|
815
|
+
// now run the model again. this should trigger transition to quote request
|
|
816
|
+
resultPromise = model.run(resume);
|
|
210
817
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
expect(StateMachine.__instance.state).toBe('succeeded');
|
|
214
|
-
});
|
|
818
|
+
// now we started the model running we simulate a callback with the quote response
|
|
819
|
+
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
215
820
|
|
|
821
|
+
// wait for the model to reach a terminal state
|
|
822
|
+
result = await resultPromise;
|
|
216
823
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
824
|
+
// check we stopped at quoteReceived state
|
|
825
|
+
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
826
|
+
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
220
827
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return Promise.resolve();
|
|
224
|
-
});
|
|
828
|
+
// check the accept party key got merged to the state
|
|
829
|
+
expect(result.acceptParty).toEqual(true);
|
|
225
830
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
quoteResponse.data.transferAmount = {
|
|
229
|
-
currency: 'XYZ',
|
|
230
|
-
amount: '9876543210'
|
|
231
|
-
};
|
|
831
|
+
// check the amount key got changed
|
|
832
|
+
expect(result.amount).toEqual(resume.amount);
|
|
232
833
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
834
|
+
// check the quote request amount is the NEW amount, not the initial amount
|
|
835
|
+
expect(result.quoteRequest.body.amount.amount).toStrictEqual(resume.amount);
|
|
836
|
+
expect(result.quoteRequest.body.amount.amount).not.toEqual(initialAmount);
|
|
237
837
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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' });
|
|
838
|
+
// check that our disallowed key is not merged to the transfer state
|
|
839
|
+
expect(result.someRandomKey).toBeUndefined();
|
|
840
|
+
});
|
|
246
841
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
842
|
+
test('skips resolving party when to.fspid is specified and skipPartyLookup is truthy', async () => {
|
|
843
|
+
config.autoAcceptParty = false;
|
|
844
|
+
config.autoAcceptQuotes = false;
|
|
845
|
+
|
|
846
|
+
let model = new Model({
|
|
847
|
+
cache,
|
|
848
|
+
logger,
|
|
849
|
+
metricsClient,
|
|
850
|
+
...config,
|
|
250
851
|
});
|
|
251
852
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
853
|
+
let req = JSON.parse(JSON.stringify(transferRequest));
|
|
854
|
+
const testFspId = 'TESTDESTFSPID';
|
|
855
|
+
req.to.fspId = testFspId;
|
|
856
|
+
req.skipPartyLookup = true;
|
|
256
857
|
|
|
257
|
-
|
|
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' });
|
|
858
|
+
await model.initialize(req);
|
|
262
859
|
|
|
263
|
-
|
|
264
|
-
expect(model.data.to.fspId).toBe(payeeParty.party.partyIdInfo.fspId);
|
|
265
|
-
expect(quoteResponse.headers['fspiop-source']).not.toBe(model.data.to.fspId);
|
|
860
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
266
861
|
|
|
267
|
-
|
|
862
|
+
// start the model running
|
|
863
|
+
let resultPromise = model.run();
|
|
268
864
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return Promise.resolve();
|
|
272
|
-
});
|
|
865
|
+
// now we started the model running we simulate a callback with the quote response
|
|
866
|
+
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
273
867
|
|
|
274
|
-
|
|
868
|
+
// wait for the model to reach a terminal state
|
|
869
|
+
let result = await resultPromise;
|
|
870
|
+
|
|
871
|
+
// check we stopped at quoteReceived state
|
|
872
|
+
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
873
|
+
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test('aborts after party rejected by backend', async () => {
|
|
877
|
+
config.autoAcceptParty = false;
|
|
878
|
+
config.autoAcceptQuotes = false;
|
|
879
|
+
|
|
880
|
+
let model = new Model({
|
|
275
881
|
cache,
|
|
276
882
|
logger,
|
|
883
|
+
metricsClient,
|
|
277
884
|
...config,
|
|
278
885
|
});
|
|
279
886
|
|
|
@@ -282,57 +889,50 @@ describe('outboundModel', () => {
|
|
|
282
889
|
expect(StateMachine.__instance.state).toBe('start');
|
|
283
890
|
|
|
284
891
|
// start the model running
|
|
285
|
-
|
|
892
|
+
let resultPromise = model.run();
|
|
286
893
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
|
|
894
|
+
// now we started the model running we simulate a callback with the resolved party
|
|
895
|
+
emitPartyCacheMessage(cache, payeeParty);
|
|
290
896
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
expect(StateMachine.__instance.state).toBe('succeeded');
|
|
294
|
-
});
|
|
897
|
+
// wait for the model to reach a terminal state
|
|
898
|
+
let result = await resultPromise;
|
|
295
899
|
|
|
900
|
+
// check we stopped at payeeResolved state
|
|
901
|
+
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
902
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
296
903
|
|
|
297
|
-
|
|
298
|
-
MojaloopRequests.__getTransfers = jest.fn((transferId) => {
|
|
299
|
-
emitTransferFulfilCacheMessage(cache, transferId, transferFulfil);
|
|
300
|
-
return Promise.resolve();
|
|
301
|
-
});
|
|
904
|
+
const transferId = result.transferId;
|
|
302
905
|
|
|
303
|
-
|
|
906
|
+
// load a new model from the saved state
|
|
907
|
+
model = new Model({
|
|
304
908
|
cache,
|
|
305
909
|
logger,
|
|
910
|
+
metricsClient,
|
|
306
911
|
...config,
|
|
307
912
|
});
|
|
308
913
|
|
|
309
|
-
|
|
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');
|
|
914
|
+
await model.load(transferId);
|
|
318
915
|
|
|
319
|
-
//
|
|
320
|
-
|
|
916
|
+
// check the model loaded to the correct state
|
|
917
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
321
918
|
|
|
322
|
-
|
|
919
|
+
// now run the model again with a party rejection. this should trigger transition to quote request
|
|
920
|
+
result = await model.run({ resume: { acceptParty: false } });
|
|
323
921
|
|
|
324
|
-
// check we stopped at
|
|
325
|
-
expect(result.currentState).toBe('
|
|
326
|
-
expect(
|
|
922
|
+
// check we stopped at quoteReceived state
|
|
923
|
+
expect(result.currentState).toBe('ABORTED');
|
|
924
|
+
expect(result.abortedReason).toBe('Payee rejected by backend');
|
|
925
|
+
expect(StateMachine.__instance.state).toBe('aborted');
|
|
327
926
|
});
|
|
328
927
|
|
|
329
|
-
|
|
330
|
-
test('resolves payee and halts when AUTO_ACCEPT_PARTY is false', async () => {
|
|
928
|
+
test('aborts after quote rejected by backend', async () => {
|
|
331
929
|
config.autoAcceptParty = false;
|
|
930
|
+
config.autoAcceptQuotes = false;
|
|
332
931
|
|
|
333
|
-
|
|
932
|
+
let model = new Model({
|
|
334
933
|
cache,
|
|
335
934
|
logger,
|
|
935
|
+
metricsClient,
|
|
336
936
|
...config,
|
|
337
937
|
});
|
|
338
938
|
|
|
@@ -341,60 +941,88 @@ describe('outboundModel', () => {
|
|
|
341
941
|
expect(StateMachine.__instance.state).toBe('start');
|
|
342
942
|
|
|
343
943
|
// start the model running
|
|
344
|
-
|
|
944
|
+
let resultPromise = model.run();
|
|
345
945
|
|
|
346
946
|
// now we started the model running we simulate a callback with the resolved party
|
|
347
947
|
emitPartyCacheMessage(cache, payeeParty);
|
|
348
948
|
|
|
349
949
|
// wait for the model to reach a terminal state
|
|
350
|
-
|
|
950
|
+
let result = await resultPromise;
|
|
351
951
|
|
|
352
952
|
// check we stopped at payeeResolved state
|
|
353
953
|
expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
|
|
354
954
|
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
355
|
-
});
|
|
356
955
|
|
|
357
|
-
|
|
358
|
-
config.autoAcceptParty = false;
|
|
956
|
+
const transferId = result.transferId;
|
|
359
957
|
|
|
360
|
-
|
|
958
|
+
// load a new model from the saved state
|
|
959
|
+
model = new Model({
|
|
361
960
|
cache,
|
|
362
961
|
logger,
|
|
962
|
+
metricsClient,
|
|
363
963
|
...config,
|
|
364
964
|
});
|
|
365
965
|
|
|
366
|
-
|
|
367
|
-
const testFspId = 'TESTDESTFSPID';
|
|
368
|
-
req.to.fspId = testFspId;
|
|
369
|
-
|
|
370
|
-
await model.initialize(req);
|
|
966
|
+
await model.load(transferId);
|
|
371
967
|
|
|
372
|
-
|
|
968
|
+
// check the model loaded to the correct state
|
|
969
|
+
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
373
970
|
|
|
374
|
-
//
|
|
375
|
-
|
|
971
|
+
// now run the model again. this should trigger transition to quote request
|
|
972
|
+
resultPromise = model.run({ acceptParty: true });
|
|
376
973
|
|
|
377
|
-
// now we started the model running we simulate a callback with the
|
|
378
|
-
|
|
974
|
+
// now we started the model running we simulate a callback with the quote response
|
|
975
|
+
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
379
976
|
|
|
380
|
-
// wait for the model to reach
|
|
381
|
-
|
|
977
|
+
// wait for the model to reach quote received
|
|
978
|
+
result = await resultPromise;
|
|
382
979
|
|
|
383
980
|
// check we stopped at payeeResolved state
|
|
384
|
-
expect(result.currentState).toBe('
|
|
385
|
-
expect(StateMachine.__instance.state).toBe('
|
|
981
|
+
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
982
|
+
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
386
983
|
|
|
387
|
-
//
|
|
388
|
-
|
|
984
|
+
// now run the model again. this should trigger abort as the quote was not accepted
|
|
985
|
+
result = await model.run({ acceptQuote: false });
|
|
986
|
+
|
|
987
|
+
expect(result.currentState).toBe('ABORTED');
|
|
988
|
+
expect(result.abortedReason).toBe('Quote rejected by backend');
|
|
989
|
+
expect(StateMachine.__instance.state).toBe('aborted');
|
|
389
990
|
});
|
|
390
991
|
|
|
391
|
-
test('
|
|
992
|
+
test('should handle unknown state with a meaningful error message', async () => {
|
|
993
|
+
config.autoAcceptParty = false;
|
|
994
|
+
config.autoAcceptQuotes = false;
|
|
995
|
+
|
|
996
|
+
let model = new Model({
|
|
997
|
+
cache,
|
|
998
|
+
logger,
|
|
999
|
+
metricsClient,
|
|
1000
|
+
...config,
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
await model.initialize(JSON.parse(JSON.stringify({
|
|
1004
|
+
...transferRequest,
|
|
1005
|
+
currentState: 'abc'
|
|
1006
|
+
})));
|
|
1007
|
+
|
|
1008
|
+
expect(StateMachine.__instance.state).toBe('abc');
|
|
1009
|
+
|
|
1010
|
+
// start the model running
|
|
1011
|
+
let resultPromise = model.run();
|
|
1012
|
+
|
|
1013
|
+
// wait for the model to reach a terminal state
|
|
1014
|
+
let result = await resultPromise;
|
|
1015
|
+
expect(result).toBe(undefined);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test('should handle subsequent put transfer calls incase of aborted transfer', async () => {
|
|
392
1019
|
config.autoAcceptParty = false;
|
|
393
1020
|
config.autoAcceptQuotes = false;
|
|
394
1021
|
|
|
395
1022
|
let model = new Model({
|
|
396
1023
|
cache,
|
|
397
1024
|
logger,
|
|
1025
|
+
metricsClient,
|
|
398
1026
|
...config,
|
|
399
1027
|
});
|
|
400
1028
|
|
|
@@ -421,6 +1049,7 @@ describe('outboundModel', () => {
|
|
|
421
1049
|
model = new Model({
|
|
422
1050
|
cache,
|
|
423
1051
|
logger,
|
|
1052
|
+
metricsClient,
|
|
424
1053
|
...config,
|
|
425
1054
|
});
|
|
426
1055
|
|
|
@@ -430,19 +1059,32 @@ describe('outboundModel', () => {
|
|
|
430
1059
|
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
431
1060
|
|
|
432
1061
|
// now run the model again. this should trigger transition to quote request
|
|
433
|
-
resultPromise = model.run();
|
|
1062
|
+
resultPromise = model.run({ acceptParty: true });
|
|
434
1063
|
|
|
435
1064
|
// now we started the model running we simulate a callback with the quote response
|
|
436
1065
|
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
437
1066
|
|
|
438
|
-
// wait for the model to reach
|
|
1067
|
+
// wait for the model to reach quote received
|
|
439
1068
|
result = await resultPromise;
|
|
440
1069
|
|
|
441
1070
|
// check we stopped at payeeResolved state
|
|
442
1071
|
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
443
1072
|
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
444
|
-
});
|
|
445
1073
|
|
|
1074
|
+
// now run the model again. this should trigger abort as the quote was not accepted
|
|
1075
|
+
result = await model.run({ acceptQuote: false });
|
|
1076
|
+
|
|
1077
|
+
expect(result.currentState).toBe('ABORTED');
|
|
1078
|
+
expect(result.abortedReason).toBe('Quote rejected by backend');
|
|
1079
|
+
expect(StateMachine.__instance.state).toBe('aborted');
|
|
1080
|
+
|
|
1081
|
+
// now run the model again. this should get the same result as previous one
|
|
1082
|
+
result = await model.run({ acceptQuote: false });
|
|
1083
|
+
|
|
1084
|
+
expect(result.currentState).toBe('ABORTED');
|
|
1085
|
+
expect(result.abortedReason).toBe('Quote rejected by backend');
|
|
1086
|
+
expect(StateMachine.__instance.state).toBe('aborted');
|
|
1087
|
+
});
|
|
446
1088
|
|
|
447
1089
|
test('halts and resumes after parties and quotes stages when AUTO_ACCEPT_PARTY is false and AUTO_ACCEPT_QUOTES is false', async () => {
|
|
448
1090
|
config.autoAcceptParty = false;
|
|
@@ -451,6 +1093,7 @@ describe('outboundModel', () => {
|
|
|
451
1093
|
let model = new Model({
|
|
452
1094
|
cache,
|
|
453
1095
|
logger,
|
|
1096
|
+
metricsClient,
|
|
454
1097
|
...config,
|
|
455
1098
|
});
|
|
456
1099
|
|
|
@@ -477,6 +1120,7 @@ describe('outboundModel', () => {
|
|
|
477
1120
|
model = new Model({
|
|
478
1121
|
cache,
|
|
479
1122
|
logger,
|
|
1123
|
+
metricsClient,
|
|
480
1124
|
...config,
|
|
481
1125
|
});
|
|
482
1126
|
|
|
@@ -486,7 +1130,7 @@ describe('outboundModel', () => {
|
|
|
486
1130
|
expect(StateMachine.__instance.state).toBe('payeeResolved');
|
|
487
1131
|
|
|
488
1132
|
// now run the model again. this should trigger transition to quote request
|
|
489
|
-
resultPromise = model.run();
|
|
1133
|
+
resultPromise = model.run({ acceptParty: true });
|
|
490
1134
|
|
|
491
1135
|
// now we started the model running we simulate a callback with the quote response
|
|
492
1136
|
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
@@ -502,6 +1146,7 @@ describe('outboundModel', () => {
|
|
|
502
1146
|
model = new Model({
|
|
503
1147
|
cache,
|
|
504
1148
|
logger,
|
|
1149
|
+
metricsClient,
|
|
505
1150
|
...config,
|
|
506
1151
|
});
|
|
507
1152
|
|
|
@@ -511,7 +1156,7 @@ describe('outboundModel', () => {
|
|
|
511
1156
|
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
512
1157
|
|
|
513
1158
|
// now run the model again. this should trigger transition to quote request
|
|
514
|
-
resultPromise = model.run();
|
|
1159
|
+
resultPromise = model.run({ acceptQuote: true });
|
|
515
1160
|
|
|
516
1161
|
// now we started the model running we simulate a callback with the transfer fulfilment
|
|
517
1162
|
cache.publish(`tf_${model.data.transferId}`, JSON.stringify(transferFulfil));
|
|
@@ -532,31 +1177,32 @@ describe('outboundModel', () => {
|
|
|
532
1177
|
MojaloopRequests.__getParties = jest.fn(() => {
|
|
533
1178
|
// simulate a callback with the resolved party
|
|
534
1179
|
emitPartyCacheMessage(cache, payeeParty);
|
|
535
|
-
return Promise.resolve();
|
|
1180
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
536
1181
|
});
|
|
537
1182
|
|
|
538
1183
|
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
539
1184
|
// simulate a callback with the quote response
|
|
540
1185
|
emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
|
|
541
|
-
return Promise.resolve();
|
|
1186
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
542
1187
|
});
|
|
543
1188
|
|
|
544
1189
|
MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
|
|
545
1190
|
//ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
|
|
546
1191
|
// set as the destination FSPID, picked up from the header's value `fspiop-source`
|
|
547
|
-
expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
|
|
1192
|
+
expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
548
1193
|
expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
|
|
549
1194
|
const payeeFsp = MojaloopRequests.__postTransfers.mock.calls[0][0].payeeFsp;
|
|
550
|
-
expect(payeeFsp).toEqual(payeeParty.party.partyIdInfo.fspId);
|
|
1195
|
+
expect(payeeFsp).toEqual(payeeParty.body.party.partyIdInfo.fspId);
|
|
551
1196
|
|
|
552
1197
|
// simulate a callback with the transfer fulfilment
|
|
553
1198
|
emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
|
|
554
|
-
return Promise.resolve();
|
|
1199
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
555
1200
|
});
|
|
556
1201
|
|
|
557
1202
|
const model = new Model({
|
|
558
1203
|
cache,
|
|
559
1204
|
logger,
|
|
1205
|
+
metricsClient,
|
|
560
1206
|
...config,
|
|
561
1207
|
});
|
|
562
1208
|
|
|
@@ -583,31 +1229,32 @@ describe('outboundModel', () => {
|
|
|
583
1229
|
MojaloopRequests.__getParties = jest.fn(() => {
|
|
584
1230
|
// simulate a callback with the resolved party
|
|
585
1231
|
emitPartyCacheMessage(cache, payeeParty);
|
|
586
|
-
return Promise.resolve();
|
|
1232
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
587
1233
|
});
|
|
588
1234
|
|
|
589
1235
|
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
590
1236
|
// simulate a callback with the quote response
|
|
591
1237
|
emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
|
|
592
|
-
return Promise.resolve();
|
|
1238
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
593
1239
|
});
|
|
594
1240
|
|
|
595
1241
|
MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
|
|
596
1242
|
//ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
|
|
597
1243
|
// set as the destination FSPID, picked up from the header's value `fspiop-source`
|
|
598
|
-
expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
|
|
1244
|
+
expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
|
|
599
1245
|
expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1);
|
|
600
1246
|
const payeeFsp = MojaloopRequests.__postTransfers.mock.calls[0][0].payeeFsp;
|
|
601
|
-
expect(payeeFsp).toEqual(quoteResponse.headers['fspiop-source']);
|
|
1247
|
+
expect(payeeFsp).toEqual(quoteResponse.data.headers['fspiop-source']);
|
|
602
1248
|
|
|
603
1249
|
// simulate a callback with the transfer fulfilment
|
|
604
1250
|
emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);
|
|
605
|
-
return Promise.resolve();
|
|
1251
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
606
1252
|
});
|
|
607
1253
|
|
|
608
1254
|
const model = new Model({
|
|
609
1255
|
cache,
|
|
610
1256
|
logger,
|
|
1257
|
+
metricsClient,
|
|
611
1258
|
...config,
|
|
612
1259
|
});
|
|
613
1260
|
|
|
@@ -695,12 +1342,13 @@ describe('outboundModel', () => {
|
|
|
695
1342
|
MojaloopRequests.__getParties = jest.fn(() => {
|
|
696
1343
|
// simulate a callback with the resolved party
|
|
697
1344
|
cache.publish(genPartyId(payeeParty), JSON.stringify(expectError));
|
|
698
|
-
return Promise.resolve();
|
|
1345
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
699
1346
|
});
|
|
700
1347
|
|
|
701
1348
|
const model = new Model({
|
|
702
1349
|
cache,
|
|
703
1350
|
logger,
|
|
1351
|
+
metricsClient,
|
|
704
1352
|
...config,
|
|
705
1353
|
});
|
|
706
1354
|
|
|
@@ -709,9 +1357,11 @@ describe('outboundModel', () => {
|
|
|
709
1357
|
expect(StateMachine.__instance.state).toBe('start');
|
|
710
1358
|
|
|
711
1359
|
const expectError = {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1360
|
+
body: {
|
|
1361
|
+
errorInformation: {
|
|
1362
|
+
errorCode: '3204',
|
|
1363
|
+
errorDescription: 'Party not found'
|
|
1364
|
+
}
|
|
715
1365
|
}
|
|
716
1366
|
};
|
|
717
1367
|
|
|
@@ -724,7 +1374,7 @@ describe('outboundModel', () => {
|
|
|
724
1374
|
expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
|
|
725
1375
|
expect(err.transferState).toBeTruthy();
|
|
726
1376
|
expect(err.transferState.lastError).toBeTruthy();
|
|
727
|
-
expect(err.transferState.lastError.mojaloopError).toEqual(expectError);
|
|
1377
|
+
expect(err.transferState.lastError.mojaloopError).toEqual(expectError.body);
|
|
728
1378
|
expect(err.transferState.lastError.transferState).toBe(undefined);
|
|
729
1379
|
return;
|
|
730
1380
|
}
|
|
@@ -751,18 +1401,19 @@ describe('outboundModel', () => {
|
|
|
751
1401
|
MojaloopRequests.__getParties = jest.fn(() => {
|
|
752
1402
|
// simulate a callback with the resolved party
|
|
753
1403
|
emitPartyCacheMessage(cache, payeeParty);
|
|
754
|
-
return Promise.resolve();
|
|
1404
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
755
1405
|
});
|
|
756
1406
|
|
|
757
1407
|
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
758
1408
|
// simulate a callback with the quote response
|
|
759
1409
|
cache.publish(`qt_${postQuotesBody.quoteId}`, JSON.stringify(expectError));
|
|
760
|
-
return Promise.resolve();
|
|
1410
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
761
1411
|
});
|
|
762
1412
|
|
|
763
1413
|
const model = new Model({
|
|
764
1414
|
cache,
|
|
765
1415
|
logger,
|
|
1416
|
+
metricsClient,
|
|
766
1417
|
...config,
|
|
767
1418
|
});
|
|
768
1419
|
|
|
@@ -795,9 +1446,11 @@ describe('outboundModel', () => {
|
|
|
795
1446
|
const expectError = {
|
|
796
1447
|
type: 'transferError',
|
|
797
1448
|
data: {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1449
|
+
body: {
|
|
1450
|
+
errorInformation: {
|
|
1451
|
+
errorCode: '4001',
|
|
1452
|
+
errorDescription: 'Payer FSP insufficient liquidity'
|
|
1453
|
+
}
|
|
801
1454
|
}
|
|
802
1455
|
}
|
|
803
1456
|
};
|
|
@@ -805,24 +1458,25 @@ describe('outboundModel', () => {
|
|
|
805
1458
|
MojaloopRequests.__getParties = jest.fn(() => {
|
|
806
1459
|
// simulate a callback with the resolved party
|
|
807
1460
|
emitPartyCacheMessage(cache, payeeParty);
|
|
808
|
-
return Promise.resolve();
|
|
1461
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
809
1462
|
});
|
|
810
1463
|
|
|
811
1464
|
MojaloopRequests.__postQuotes = jest.fn((postQuotesBody) => {
|
|
812
1465
|
// simulate a callback with the quote response
|
|
813
1466
|
emitQuoteResponseCacheMessage(cache, postQuotesBody.quoteId, quoteResponse);
|
|
814
|
-
return Promise.resolve();
|
|
1467
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
815
1468
|
});
|
|
816
1469
|
|
|
817
1470
|
MojaloopRequests.__postTransfers = jest.fn((postTransfersBody) => {
|
|
818
1471
|
// simulate an error callback with the transfer fulfilment
|
|
819
1472
|
cache.publish(`tf_${postTransfersBody.transferId}`, JSON.stringify(expectError));
|
|
820
|
-
return Promise.resolve();
|
|
1473
|
+
return Promise.resolve(dummyRequestsModuleResponse);
|
|
821
1474
|
});
|
|
822
1475
|
|
|
823
1476
|
const model = new Model({
|
|
824
1477
|
cache,
|
|
825
1478
|
logger,
|
|
1479
|
+
metricsClient,
|
|
826
1480
|
...config,
|
|
827
1481
|
});
|
|
828
1482
|
|
|
@@ -839,7 +1493,7 @@ describe('outboundModel', () => {
|
|
|
839
1493
|
expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,''));
|
|
840
1494
|
expect(err.transferState).toBeTruthy();
|
|
841
1495
|
expect(err.transferState.lastError).toBeTruthy();
|
|
842
|
-
expect(err.transferState.lastError.mojaloopError).toEqual(expectError.data);
|
|
1496
|
+
expect(err.transferState.lastError.mojaloopError).toEqual(expectError.data.body);
|
|
843
1497
|
expect(err.transferState.lastError.transferState).toBe(undefined);
|
|
844
1498
|
return;
|
|
845
1499
|
}
|
|
@@ -849,11 +1503,12 @@ describe('outboundModel', () => {
|
|
|
849
1503
|
|
|
850
1504
|
|
|
851
1505
|
async function testTlsServer(enableTls) {
|
|
852
|
-
config.tls.enabled = enableTls;
|
|
1506
|
+
config.outbound.tls.mutualTLS.enabled = enableTls;
|
|
853
1507
|
|
|
854
1508
|
new Model({
|
|
855
1509
|
cache,
|
|
856
1510
|
logger,
|
|
1511
|
+
metricsClient,
|
|
857
1512
|
...config
|
|
858
1513
|
});
|
|
859
1514
|
|
|
@@ -866,4 +1521,54 @@ describe('outboundModel', () => {
|
|
|
866
1521
|
|
|
867
1522
|
test('Outbound server should use HTTP if outbound mTLS disabled', () =>
|
|
868
1523
|
testTlsServer(false));
|
|
1524
|
+
|
|
1525
|
+
test('Outbound transfers model should record metrics', async () => {
|
|
1526
|
+
const metrics = metricsClient._prometheusRegister.metrics();
|
|
1527
|
+
expect(metrics).toBeTruthy();
|
|
1528
|
+
|
|
1529
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_request_count'));
|
|
1530
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_response_count'));
|
|
1531
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_request_count'));
|
|
1532
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_response_count'));
|
|
1533
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_prepare_count'));
|
|
1534
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_fulfil_response_count'));
|
|
1535
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_request_latency'));
|
|
1536
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_latency'));
|
|
1537
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_latency'));
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
test('skips resolving party when to.fspid is specified and skipPartyLookup is truthy', async () => {
|
|
1541
|
+
config.autoAcceptParty = false;
|
|
1542
|
+
config.autoAcceptQuotes = false;
|
|
1543
|
+
|
|
1544
|
+
let model = new Model({
|
|
1545
|
+
cache,
|
|
1546
|
+
logger,
|
|
1547
|
+
metricsClient,
|
|
1548
|
+
...config,
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
let req = JSON.parse(JSON.stringify(transferRequest));
|
|
1552
|
+
const testFspId = 'TESTDESTFSPID';
|
|
1553
|
+
req.to.fspId = testFspId;
|
|
1554
|
+
req.skipPartyLookup = true;
|
|
1555
|
+
|
|
1556
|
+
await model.initialize(req);
|
|
1557
|
+
|
|
1558
|
+
expect(StateMachine.__instance.state).toBe('start');
|
|
1559
|
+
|
|
1560
|
+
// start the model running
|
|
1561
|
+
let resultPromise = model.run();
|
|
1562
|
+
|
|
1563
|
+
// now we started the model running we simulate a callback with the quote response
|
|
1564
|
+
cache.publish(`qt_${model.data.quoteId}`, JSON.stringify(quoteResponse));
|
|
1565
|
+
|
|
1566
|
+
// wait for the model to reach a terminal state
|
|
1567
|
+
let result = await resultPromise;
|
|
1568
|
+
|
|
1569
|
+
// check we stopped at quoteReceived state
|
|
1570
|
+
expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE');
|
|
1571
|
+
expect(StateMachine.__instance.state).toBe('quoteReceived');
|
|
1572
|
+
});
|
|
1573
|
+
|
|
869
1574
|
});
|