@mojaloop/sdk-scheme-adapter 12.0.2 → 12.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+
2
+ const ControlAgent = require('~/ControlAgent');
3
+ const TestControlServer = require('./ControlServer');
4
+ const InboundServer = require('~/InboundServer');
5
+ const OutboundServer = require('~/OutboundServer');
6
+ const TestServer = require('~/TestServer');
7
+ const defaultConfig = require('./data/defaultConfig.json');
8
+ const { Logger } = require('@mojaloop/sdk-standard-components');
9
+
10
+ jest.mock('~/lib/cache');
11
+ const Cache = require('~/lib/cache');
12
+
13
+ // TODO:
14
+ // - diff against master to determine what else needs testing
15
+ // - especially look for assertions in the code
16
+ // - err.. grep the code for TODO
17
+
18
+ describe('ControlAgent', () => {
19
+ it('exposes a valid message API', () => {
20
+ expect(Object.keys(ControlAgent.build).sort()).toEqual(
21
+ Object.keys(ControlAgent.MESSAGE).sort(),
22
+ 'The API exposed by the builder object must contain as top-level keys all of the message types exposed in the MESSAGE constant. Check that ControlAgent.MESSAGE has the same keys as ControlAgent.build.'
23
+ );
24
+ Object.entries(ControlAgent.build).forEach(([messageType, builders]) => {
25
+ expect(Object.keys(ControlAgent.VERB)).toEqual(
26
+ expect.arrayContaining(Object.keys(builders)),
27
+ `For message type '${messageType}' every builder must correspond to a verb. Check that ControlAgent.build.${messageType} has the same keys as ControlAgent.VERB.`
28
+ );
29
+ });
30
+ expect(Object.keys(ControlAgent.build.ERROR.NOTIFY).sort()).toEqual(
31
+ Object.keys(ControlAgent.ERROR).sort(),
32
+ 'ControlAgent.ERROR.NOTIFY should contain the same keys as ControlAgent.ERROR'
33
+ );
34
+ });
35
+
36
+ describe('API', () => {
37
+ let server, logger, client;
38
+ const appConfig = { control: { port: 4005 }, what: 'ever' };
39
+ const changedConfig = { ...appConfig, some: 'thing' };
40
+
41
+ beforeEach(async () => {
42
+ logger = new Logger.Logger({ stringify: () => '' });
43
+ server = new TestControlServer.Server({ logger, appConfig });
44
+ client = await ControlAgent.Client.Create({
45
+ address: 'localhost',
46
+ port: server.address().port,
47
+ logger,
48
+ appConfig
49
+ });
50
+ });
51
+
52
+ afterEach(async () => {
53
+ await client.stop();
54
+ await server.stop();
55
+ });
56
+
57
+ it('receives config when requested', async () => {
58
+ await client.send(ControlAgent.build.CONFIGURATION.READ());
59
+ const response = await client.receive();
60
+ expect(response).toEqual({
61
+ ...JSON.parse(ControlAgent.build.CONFIGURATION.NOTIFY(appConfig, response.id)),
62
+ });
63
+ });
64
+
65
+ it('emits new config when received', async () => {
66
+ const newConfigEvent = new Promise(
67
+ (resolve) => client.on(ControlAgent.EVENT.RECONFIGURE, resolve)
68
+ );
69
+ await server.broadcastConfigChange(changedConfig);
70
+ const newConfEventData = await newConfigEvent;
71
+ expect(newConfEventData).toEqual(changedConfig);
72
+ });
73
+ });
74
+ });
75
+
76
+ describe('Server reconfigure methods', () => {
77
+ let conf, logger, cache;
78
+
79
+ const isPromise = (o) => Promise.resolve(o) === o;
80
+
81
+ beforeEach(() => {
82
+ conf = JSON.parse(JSON.stringify(defaultConfig));
83
+ logger = new Logger.Logger({ stringify: () => '' });
84
+ cache = new Cache({ ...conf.cacheConfig, logger: logger.push({ component: 'cache' }) });
85
+ });
86
+
87
+ test('InboundServer reconfigure method returns sync function', async () => {
88
+ const server = new InboundServer(conf, logger, cache);
89
+ const res = await server.reconfigure(conf, logger, cache);
90
+ expect(isPromise(res)).toEqual(false);
91
+ });
92
+
93
+ test('OutboundServer reconfigure method returns sync function', async () => {
94
+ const server = new OutboundServer(conf, logger, cache);
95
+ const res = await server.reconfigure(conf, logger, cache);
96
+ expect(isPromise(res)).toEqual(false);
97
+ });
98
+
99
+ test('TestServer reconfigure method returns sync function', async () => {
100
+ const server = new TestServer({ logger, cache });
101
+ const res = await server.reconfigure({ logger, cache });
102
+ expect(isPromise(res)).toEqual(false);
103
+ });
104
+
105
+ test('ControlClient reconfigure method returns sync function', async () => {
106
+ const server = new TestControlServer.Server({ logger, appConfig: { ...conf, control: { port: 4005 }}});
107
+ const client = await ControlAgent.Client.Create({ port: 4005, logger, appConfig: {} });
108
+ const res = await client.reconfigure({ logger, port: 4005, appConfig: {} });
109
+ expect(isPromise(res)).toEqual(false);
110
+ await client.close();
111
+ await server.close();
112
+ });
113
+ });
@@ -0,0 +1,41 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2020 - 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
+ * Steven Oderayi - steven.oderayi@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ const { EventEmitter } = require('events');
12
+
13
+ /**************************************************************************
14
+ * Internal events received by the control server via the exposed internal
15
+ * event emitter.
16
+ *************************************************************************/
17
+ const INTERNAL_EVENTS = {
18
+ SERVER: {
19
+ BROADCAST_CONFIG_CHANGE: 'BROADCAST_CONFIG_CHANGE',
20
+ }
21
+ };
22
+ const internalEventEmitter = new EventEmitter();
23
+
24
+ /**************************************************************************
25
+ * getInternalEventEmitter
26
+ *
27
+ * Returns an EventEmmitter that can be used to exchange internal events with
28
+ * either the control server or the client from other modules within this service.
29
+ * This prevents the need to pass down references to either the server or the client
30
+ * from one module to another in order to use their interfaces.
31
+ *
32
+ * @returns {events.EventEmitter}
33
+ *************************************************************************/
34
+ const getInternalEventEmitter = () => {
35
+ return internalEventEmitter;
36
+ };
37
+
38
+ module.exports = {
39
+ getInternalEventEmitter,
40
+ INTERNAL_EVENTS
41
+ };
@@ -0,0 +1,231 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2020 - 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
+ * Matt Kingston - matt.kingston@modusbox.com *
9
+ **************************************************************************/
10
+ 'use strict';
11
+
12
+ const ws = require('ws');
13
+ const jsonPatch = require('fast-json-patch');
14
+ const randomPhrase = require('~/lib/randomphrase');
15
+ const { getInternalEventEmitter, INTERNAL_EVENTS } = require('./events');
16
+
17
+ const ControlServerEventEmitter = getInternalEventEmitter();
18
+
19
+
20
+ /**************************************************************************
21
+ * The message protocol messages, verbs, and errors
22
+ *************************************************************************/
23
+ const MESSAGE = {
24
+ CONFIGURATION: 'CONFIGURATION',
25
+ ERROR: 'ERROR',
26
+ };
27
+
28
+ const VERB = {
29
+ READ: 'READ',
30
+ NOTIFY: 'NOTIFY',
31
+ PATCH: 'PATCH'
32
+ };
33
+
34
+ const ERROR = {
35
+ UNSUPPORTED_MESSAGE: 'UNSUPPORTED_MESSAGE',
36
+ UNSUPPORTED_VERB: 'UNSUPPORTED_VERB',
37
+ JSON_PARSE_ERROR: 'JSON_PARSE_ERROR',
38
+ };
39
+
40
+ /**************************************************************************
41
+ * Private convenience functions
42
+ *************************************************************************/
43
+ const serialise = JSON.stringify;
44
+ const deserialise = (msg) => {
45
+ //reviver function
46
+ return JSON.parse(msg.toString(), (k, v) => {
47
+ if (
48
+ v !== null &&
49
+ typeof v === 'object' &&
50
+ 'type' in v &&
51
+ v.type === 'Buffer' &&
52
+ 'data' in v &&
53
+ Array.isArray(v.data)) {
54
+ return new Buffer(v.data);
55
+ }
56
+ return v;
57
+ });
58
+ };
59
+
60
+
61
+ const buildMsg = (verb, msg, data, id = randomPhrase()) => serialise({
62
+ verb,
63
+ msg,
64
+ data,
65
+ id,
66
+ });
67
+
68
+ const buildPatchConfiguration = (oldConf, newConf, id) => {
69
+ const patches = jsonPatch.compare(oldConf, newConf);
70
+ return buildMsg(VERB.PATCH, MESSAGE.CONFIGURATION, patches, id);
71
+ };
72
+
73
+ /**************************************************************************
74
+ * build
75
+ *
76
+ * Public object exposing an API to build valid protocol messages.
77
+ * It is not the only way to build valid messages within the protocol.
78
+ *************************************************************************/
79
+ const build = {
80
+ CONFIGURATION: {
81
+ PATCH: buildPatchConfiguration,
82
+ READ: (id) => buildMsg(VERB.READ, MESSAGE.CONFIGURATION, {}, id),
83
+ NOTIFY: (config, id) => buildMsg(VERB.NOTIFY, MESSAGE.CONFIGURATION, config, id),
84
+ },
85
+ ERROR: {
86
+ NOTIFY: {
87
+ UNSUPPORTED_MESSAGE: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_MESSAGE, id),
88
+ UNSUPPORTED_VERB: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_VERB, id),
89
+ JSON_PARSE_ERROR: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.JSON_PARSE_ERROR, id),
90
+ }
91
+ },
92
+ };
93
+
94
+ /**************************************************************************
95
+ * Server
96
+ *
97
+ * The Control Server. Exposes a websocket control API.
98
+ * Used to hot-restart the SDK.
99
+ *
100
+ * logger - Logger- see SDK logger used elsewhere
101
+ * port - HTTP port to host on
102
+ * appConfig - The configuration for the entire application- supplied here as this class uses it to
103
+ * validate reconfiguration requests- it is not used for configuration here, however
104
+ * server - optional HTTP/S server on which to serve the websocket
105
+ *************************************************************************/
106
+ class Server extends ws.Server {
107
+ constructor({ logger, appConfig = {} }) {
108
+ super({ clientTracking: true, port: appConfig.control.port });
109
+
110
+ this._logger = logger;
111
+ this._port = appConfig.control.port;
112
+ this._appConfig = appConfig;
113
+ this._clientData = new Map();
114
+
115
+ this.on('error', err => {
116
+ this._logger.push({ err })
117
+ .log('Unhandled websocket error occurred. Shutting down.');
118
+ process.exit(1);
119
+ });
120
+
121
+ this.on('connection', (socket, req) => {
122
+ const logger = this._logger.push({
123
+ url: req.url,
124
+ ip: 'localhost',
125
+ remoteAddress: req.socket.remoteAddress,
126
+ });
127
+ logger.log('Websocket connection received');
128
+ this._clientData.set(socket, { ip: req.connection.remoteAddress, logger });
129
+
130
+ socket.on('close', (code, reason) => {
131
+ logger.push({ code, reason }).log('Websocket connection closed');
132
+ this._clientData.delete(socket);
133
+ });
134
+
135
+ socket.on('message', this._handle(socket, logger));
136
+ });
137
+ this._logger.push(this.address()).log('running on');
138
+ }
139
+
140
+ // Close the server then wait for all the client sockets to close
141
+ async stop() {
142
+ await new Promise(this.close.bind(this));
143
+ this._logger.log('Control server shutdown complete');
144
+ }
145
+
146
+ _handle(client, logger) {
147
+ return (data) => {
148
+ // TODO: json-schema validation of received message- should be pretty straight-forward
149
+ // and will allow better documentation of the API
150
+ let msg;
151
+ try {
152
+ msg = deserialise(data);
153
+ } catch (err) {
154
+ logger.push({ data }).log('Couldn\'t parse received message');
155
+ client.send(build.ERROR.NOTIFY.JSON_PARSE_ERROR());
156
+ }
157
+ logger.push({ msg }).log('Handling received message');
158
+ switch (msg.msg) {
159
+ case MESSAGE.CONFIGURATION:
160
+ switch (msg.verb) {
161
+ case VERB.READ:
162
+ (async () => {
163
+ const jwsCerts = await this.populateConfig();
164
+ client.send(build.CONFIGURATION.NOTIFY(jwsCerts, msg.id));
165
+ })();
166
+ break;
167
+ default:
168
+ client.send(build.ERROR.NOTIFY.UNSUPPORTED_VERB(msg.id));
169
+ break;
170
+ }
171
+ break;
172
+ default:
173
+ client.send(build.ERROR.NOTIFY.UNSUPPORTED_MESSAGE(msg.id));
174
+ break;
175
+ }
176
+ };
177
+ }
178
+
179
+ async populateConfig(){
180
+ return this._appConfig;
181
+ }
182
+
183
+
184
+ /**
185
+ * Register this server instance to receive internal server messages
186
+ * from other modules.
187
+ */
188
+ registerInternalEvents() {
189
+ ControlServerEventEmitter.on(INTERNAL_EVENTS.SERVER.BROADCAST_CONFIG_CHANGE, (params) => {
190
+ this.broadcastConfigChange(params);
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Broadcast configuration change to all connected clients.
196
+ *
197
+ * @param {object} params Updated configuration
198
+ */
199
+ async broadcastConfigChange(updatedConfig) {
200
+ const updateConfMsg = build.CONFIGURATION.PATCH({}, updatedConfig, randomPhrase());
201
+ const errorLogger = (socket, message) => (err) =>
202
+ this._logger
203
+ .push({ message, ip: this._clientData.get(socket).ip, err })
204
+ .log('Error sending JWS keys notification to client');
205
+ return await this.broadcast(updateConfMsg, errorLogger);
206
+ }
207
+
208
+ /**
209
+ * Broadcasts a protocol message to all connected clients.
210
+ *
211
+ * @param {string} msg
212
+ * @param {object} errorLogger
213
+ */
214
+ async broadcast(msg, errorLogger) {
215
+ const sendToAllClients = (msg, errorLogger) => Promise.all(
216
+ [...this.clients.values()].map((socket) =>
217
+ (new Promise((resolve) => socket.send(msg, resolve))).catch(errorLogger(socket, msg))
218
+ )
219
+ );
220
+ return await sendToAllClients(msg, errorLogger);
221
+ }
222
+ }
223
+
224
+
225
+ module.exports = {
226
+ Server,
227
+ build,
228
+ MESSAGE,
229
+ VERB,
230
+ ERROR,
231
+ };
@@ -0,0 +1,126 @@
1
+
2
+ const ControlServer = require('~/ControlServer');
3
+ const InboundServer = require('~/InboundServer');
4
+ const OutboundServer = require('~/OutboundServer');
5
+ const TestServer = require('~/TestServer');
6
+ const defaultConfig = require('./data/defaultConfig.json');
7
+ const { Logger } = require('@mojaloop/sdk-standard-components');
8
+
9
+ jest.mock('~/lib/cache');
10
+ const Cache = require('~/lib/cache');
11
+
12
+ // TODO:
13
+ // - diff against master to determine what else needs testing
14
+ // - especially look for assertions in the code
15
+ // - err.. grep the code for TODO
16
+
17
+ describe('ControlServer', () => {
18
+ it('exposes a valid message API', () => {
19
+ expect(Object.keys(ControlServer.build).sort()).toEqual(
20
+ Object.keys(ControlServer.MESSAGE).sort(),
21
+ 'The API exposed by the builder object must contain as top-level keys all of the message types exposed in the MESSAGE constant. Check that ControlServer.MESSAGE has the same keys as ControlServer.build.'
22
+ );
23
+ Object.entries(ControlServer.build).forEach(([messageType, builders]) => {
24
+ expect(Object.keys(ControlServer.VERB)).toEqual(
25
+ expect.arrayContaining(Object.keys(builders)),
26
+ `For message type '${messageType}' every builder must correspond to a verb. Check that ControlServer.build.${messageType} has the same keys as ControlServer.VERB.`
27
+ );
28
+ });
29
+ expect(Object.keys(ControlServer.build.ERROR.NOTIFY).sort()).toEqual(
30
+ Object.keys(ControlServer.ERROR).sort(),
31
+ 'ControlServer.ERROR.NOTIFY should contain the same keys as ControlServer.ERROR'
32
+ );
33
+ });
34
+
35
+ describe('API', () => {
36
+ let server, logger, client;
37
+ const appConfig = { what: 'ever' };
38
+ const changedConfig = { ...appConfig, some: 'thing' };
39
+
40
+ beforeEach(async () => {
41
+ logger = new Logger.Logger({ stringify: () => '' });
42
+ server = new ControlServer.Server({ logger, appConfig });
43
+ client = await ControlServer.Client.Create({
44
+ address: 'localhost',
45
+ port: server.address().port,
46
+ logger
47
+ });
48
+ });
49
+
50
+ afterEach(async () => {
51
+ await server.stop();
52
+ });
53
+
54
+ it('supplies config when requested', async () => {
55
+ await client.send(ControlServer.build.CONFIGURATION.READ());
56
+ const response = await client.receive();
57
+ expect(response).toEqual({
58
+ ...JSON.parse(ControlServer.build.CONFIGURATION.NOTIFY(appConfig, response.id)),
59
+ });
60
+ });
61
+
62
+ it('emits new config when received', async () => {
63
+ const newConfigEvent = new Promise(
64
+ (resolve) => server.on(ControlServer.EVENT.RECONFIGURE, resolve)
65
+ );
66
+ await client.send(ControlServer.build.CONFIGURATION.PATCH(appConfig, changedConfig));
67
+ const newConfEventData = await newConfigEvent;
68
+ expect(newConfEventData).toEqual(changedConfig);
69
+ });
70
+
71
+ it('sends new config to clients when instructed', async () => {
72
+ const client2 = await ControlServer.Client.Create({
73
+ address: 'localhost',
74
+ port: server.address().port,
75
+ logger
76
+ });
77
+ const changedConfig = { ...appConfig, some: 'thing' };
78
+ await client.send(ControlServer.build.CONFIGURATION.PATCH(appConfig, changedConfig));
79
+ const restart = server.reconfigure({ appConfig: changedConfig });
80
+ restart();
81
+ await server.notifyClientsOfCurrentConfig();
82
+ const [notification, notification2] =
83
+ await Promise.all([client.receive(), client2.receive()]);
84
+ const expected = ControlServer.build.CONFIGURATION.NOTIFY(changedConfig, notification.id);
85
+ expect(JSON.stringify(notification)).toEqual(expected);
86
+ expect(JSON.stringify(notification2)).toEqual(expected);
87
+ });
88
+ });
89
+ });
90
+
91
+ describe('Server reconfigure methods', () => {
92
+ let conf, logger, cache;
93
+
94
+ const isPromise = (o) => Promise.resolve(o) === o;
95
+
96
+ beforeEach(() => {
97
+ conf = JSON.parse(JSON.stringify(defaultConfig));
98
+ logger = new Logger.Logger({ stringify: () => '' });
99
+ cache = new Cache({ ...conf.cacheConfig, logger: logger.push({ component: 'cache' }) });
100
+ });
101
+
102
+ test('InboundServer reconfigure method returns sync function', async () => {
103
+ const server = new InboundServer(conf, logger, cache);
104
+ const res = await server.reconfigure(conf, logger, cache);
105
+ expect(isPromise(res)).toEqual(false);
106
+ });
107
+
108
+ test('OutboundServer reconfigure method returns sync function', async () => {
109
+ const server = new OutboundServer(conf, logger, cache);
110
+ const res = await server.reconfigure(conf, logger, cache);
111
+ expect(isPromise(res)).toEqual(false);
112
+ });
113
+
114
+ test('TestServer reconfigure method returns sync function', async () => {
115
+ const server = new TestServer({ logger, cache });
116
+ const res = await server.reconfigure({ logger, cache });
117
+ expect(isPromise(res)).toEqual(false);
118
+ });
119
+
120
+ test('ControlServer reconfigure method returns sync function', async () => {
121
+ const server = new ControlServer.Server({ logger, appConfig: {} });
122
+ const res = await server.reconfigure({ logger, appConfig: {} });
123
+ expect(isPromise(res)).toEqual(false);
124
+ await server.close();
125
+ });
126
+ });
@@ -28,6 +28,7 @@ describe('config', () => {
28
28
  process.env.BACKEND_ENDPOINT = '172.17.0.5:4000';
29
29
  process.env.CACHE_HOST = '172.17.0.2';
30
30
  process.env.CACHE_PORT = '6379';
31
+ process.env.MGMT_API_WS_URL = '0.0.0.0';
31
32
  certDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jest-'));
32
33
  });
33
34
 
@@ -21,6 +21,7 @@ process.env.PEER_ENDPOINT = '172.17.0.3:4000';
21
21
  process.env.BACKEND_ENDPOINT = '172.17.0.5:4000';
22
22
  process.env.CACHE_HOST = '172.17.0.2';
23
23
  process.env.CACHE_PORT = '6379';
24
+ process.env.MGMT_API_WS_URL = '0.0.0.0';
24
25
 
25
26
  const index = require('~/index.js');
26
27
 
@@ -354,6 +354,39 @@ describe('outboundModel', () => {
354
354
  expect(StateMachine.__instance.state).toBe('payeeResolved');
355
355
  });
356
356
 
357
+ test('uses payee party fspid as source header when supplied - resolving payee', async () => {
358
+ config.autoAcceptParty = false;
359
+
360
+ const model = new Model({
361
+ cache,
362
+ logger,
363
+ ...config,
364
+ });
365
+
366
+ let req = JSON.parse(JSON.stringify(transferRequest));
367
+ const testFspId = 'TESTDESTFSPID';
368
+ req.to.fspId = testFspId;
369
+
370
+ await model.initialize(req);
371
+
372
+ expect(StateMachine.__instance.state).toBe('start');
373
+
374
+ // start the model running
375
+ const resultPromise = model.run();
376
+
377
+ // now we started the model running we simulate a callback with the resolved party
378
+ emitPartyCacheMessage(cache, payeeParty);
379
+
380
+ // wait for the model to reach a terminal state
381
+ const result = await resultPromise;
382
+
383
+ // check we stopped at payeeResolved state
384
+ expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE');
385
+ expect(StateMachine.__instance.state).toBe('payeeResolved');
386
+
387
+ // check getParties mojaloop requests method was called with the correct arguments
388
+ expect(MojaloopRequests.__getParties).toHaveBeenCalledWith(req.to.idType, req.to.idValue, req.to.idSubValue, testFspId);
389
+ });
357
390
 
358
391
  test('halts after resolving payee, resumes and then halts after receiving quote response when AUTO_ACCEPT_PARTY is false and AUTO_ACCEPT_QUOTES is false', async () => {
359
392
  config.autoAcceptParty = false;