@mojaloop/sdk-scheme-adapter 13.0.4 → 15.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/.nvmrc +1 -1
  3. package/CHANGELOG.md +24 -0
  4. package/Dockerfile +2 -2
  5. package/audit-resolve.json +48 -457
  6. package/docs/dfspInboundApi.yaml +11 -0
  7. package/package.json +27 -32
  8. package/src/ControlAgent/index.js +8 -11
  9. package/src/ControlServer/index.js +13 -13
  10. package/src/InboundServer/index.js +12 -61
  11. package/src/OAuthTestServer/index.js +0 -13
  12. package/src/OutboundServer/api.yaml +370 -44
  13. package/src/OutboundServer/index.js +11 -54
  14. package/src/TestServer/index.js +6 -33
  15. package/src/config.js +1 -4
  16. package/src/index.js +163 -146
  17. package/src/lib/cache.js +93 -186
  18. package/src/lib/metrics.js +1 -1
  19. package/src/lib/model/InboundTransfersModel.js +10 -6
  20. package/src/lib/model/OutboundTransfersModel.js +1 -1
  21. package/test/__mocks__/redis.js +51 -26
  22. package/test/config/integration.env +1 -2
  23. package/test/integration/lib/cache.test.js +1 -2
  24. package/test/integration/testEnv.js +1 -4
  25. package/test/unit/ControlClient.test.js +1 -45
  26. package/test/unit/ControlServer/index.js +18 -22
  27. package/test/unit/ControlServer.test.js +0 -60
  28. package/test/unit/InboundServer.test.js +8 -8
  29. package/test/unit/TestServer.test.js +1 -1
  30. package/test/unit/api/accounts/accounts.test.js +2 -2
  31. package/test/unit/api/transfers/transfers.test.js +1 -1
  32. package/test/unit/api/utils.js +12 -4
  33. package/test/unit/config.test.js +1 -2
  34. package/test/unit/data/defaultConfig.json +1 -5
  35. package/test/unit/index.test.js +5 -64
  36. package/test/unit/lib/cache.test.js +5 -6
  37. package/test/unit/lib/model/AccountsModel.test.js +3 -4
  38. package/test/unit/lib/model/InboundTransfersModel.test.js +55 -16
  39. package/test/unit/lib/model/OutboundBulkQuotesModel.test.js +3 -4
  40. package/test/unit/lib/model/OutboundBulkTransfersModel.test.js +1 -2
  41. package/test/unit/lib/model/OutboundRequestToPayModel.test.js +3 -4
  42. package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +3 -4
  43. package/test/unit/lib/model/OutboundTransfersModel.test.js +2 -3
  44. package/test/unit/lib/model/common/PersistentStateMachine.test.js +3 -4
  45. package/test/unit/lib/model/data/defaultConfig.json +1 -4
  46. package/test/unit/lib/model/data/notificationAbortedToPayee.json +10 -0
  47. package/test/unit/lib/model/data/notificationReservedToPayee.json +10 -0
@@ -11,7 +11,6 @@
11
11
  const Koa = require('koa');
12
12
  const ws = require('ws');
13
13
 
14
- const assert = require('assert').strict;
15
14
  const http = require('http');
16
15
  const yaml = require('js-yaml');
17
16
  const fs = require('fs').promises;
@@ -84,7 +83,11 @@ class WsServer extends ws.Server {
84
83
 
85
84
  // Close the server then wait for all the client sockets to close
86
85
  async stop() {
87
- await new Promise(resolve => this.close(resolve));
86
+ const closing = new Promise(resolve => this.close(resolve));
87
+ for (const client of this.clients) {
88
+ client.terminate();
89
+ }
90
+ await closing;
88
91
  // If we don't wait for all clients to close before shutting down, the socket close
89
92
  // handlers will be called after we return from this function, resulting in behaviour
90
93
  // occurring after the server tells the user it has shutdown.
@@ -201,7 +204,7 @@ class TestServer {
201
204
  async stop() {
202
205
  if (this._wsapi) {
203
206
  this._logger.log('Shutting down websocket server');
204
- this._wsapi.stop();
207
+ await this._wsapi.stop();
205
208
  this._wsapi = null;
206
209
  }
207
210
  if (this._server) {
@@ -211,36 +214,6 @@ class TestServer {
211
214
  }
212
215
  this._logger.log('Test server shutdown complete');
213
216
  }
214
-
215
- async reconfigure({ port, logger, cache }) {
216
- assert(port === this._port, 'Cannot reconfigure running port');
217
- const newApi = new TestApi(logger, cache, this._validator);
218
- const newWsApi = new WsServer(logger.push({ component: 'websocket-server' }), cache);
219
- await newWsApi.start();
220
-
221
- return () => {
222
- const oldWsApi = this._wsapi;
223
- this._logger = logger;
224
- this._cache = cache;
225
- this._wsapi = newWsApi;
226
- this._api = newApi;
227
- this._server.removeAllListeners('upgrade');
228
- this._server.on('upgrade', (req, socket, head) => {
229
- this._wsapi.handleUpgrade(req, socket, head, (ws) =>
230
- this._wsapi.emit('connection', ws, req));
231
- });
232
- this._server.removeAllListeners('request');
233
- this._server.on('request', newApi.callback());
234
- // TODO: we can't guarantee client implementations. Therefore we can't guarantee
235
- // reconnect logic/behaviour. Therefore instead of closing all websocket client
236
- // connections as we do below, we should replace handlers.
237
- oldWsApi.stop().catch((err) => {
238
- this._logger.push({ err }).log('Error stopping websocket server during reconfigure');
239
- });
240
-
241
- this._logger.log('restarted');
242
- };
243
- }
244
217
  }
245
218
 
246
219
  module.exports = TestServer;
package/src/config.js CHANGED
@@ -129,10 +129,7 @@ module.exports = {
129
129
  jwsSignPutParties: env.get('JWS_SIGN_PUT_PARTIES').default('false').asBool(),
130
130
  jwsSigningKey: env.get('JWS_SIGNING_KEY_PATH').asFileContent(),
131
131
  jwsVerificationKeysDirectory: env.get('JWS_VERIFICATION_KEYS_DIRECTORY').asString(),
132
- cacheConfig: {
133
- host: env.get('CACHE_HOST').required().asString(),
134
- port: env.get('CACHE_PORT').required().asPortNumber(),
135
- },
132
+ cacheUrl: env.get('CACHE_URL').default('redis://redis:6379').asUrlString(),
136
133
  enableTestFeatures: env.get('ENABLE_TEST_FEATURES').default('false').asBool(),
137
134
  oauthTestServer: {
138
135
  enabled: env.get('ENABLE_OAUTH_TOKEN_ENDPOINT').default('false').asBool(),
package/src/index.js CHANGED
@@ -10,11 +10,10 @@
10
10
 
11
11
  'use strict';
12
12
 
13
- const assert = require('assert/strict');
14
13
  const { hostname } = require('os');
14
+ const _ = require('lodash');
15
15
  const config = require('./config');
16
16
  const EventEmitter = require('events');
17
- const _ = require('lodash');
18
17
 
19
18
  const InboundServer = require('./InboundServer');
20
19
  const OutboundServer = require('./OutboundServer');
@@ -30,8 +29,7 @@ const OutboundServerMiddleware = require('./OutboundServer/middlewares.js');
30
29
  const Router = require('./lib/router');
31
30
  const Validate = require('./lib/validate');
32
31
  const Cache = require('./lib/cache');
33
- const check = require('./lib/check');
34
- const { Logger } = require('@mojaloop/sdk-standard-components');
32
+ const { Logger, WSO2Auth } = require('@mojaloop/sdk-standard-components');
35
33
 
36
34
  const LOG_ID = {
37
35
  INBOUND: { app: 'mojaloop-connector-inbound-api' },
@@ -52,7 +50,7 @@ class Server extends EventEmitter {
52
50
  this.conf = conf;
53
51
  this.logger = logger;
54
52
  this.cache = new Cache({
55
- ...conf.cacheConfig,
53
+ cacheUrl: conf.cacheUrl,
56
54
  logger: this.logger.push(LOG_ID.CACHE),
57
55
  enableTestFeatures: conf.enableTestFeatures,
58
56
  });
@@ -64,11 +62,24 @@ class Server extends EventEmitter {
64
62
  logger: this.logger.push(LOG_ID.METRICS)
65
63
  });
66
64
 
65
+ this.wso2 = {
66
+ auth: new WSO2Auth({
67
+ ...conf.wso2.auth,
68
+ logger,
69
+ tlsCreds: conf.outbound.tls.mutualTLS.enabled && conf.outbound.tls.creds,
70
+ }),
71
+ retryWso2AuthFailureTimes: conf.wso2.requestAuthFailureRetryTimes,
72
+ };
73
+ this.wso2.auth.on('error', (msg) => {
74
+ this.emit('error', 'WSO2 auth error in InboundApi', msg);
75
+ });
76
+
67
77
  this.inboundServer = new InboundServer(
68
78
  this.conf,
69
79
  this.logger.push(LOG_ID.INBOUND),
70
80
  this.cache,
71
- this.metricsClient
81
+ this.metricsClient,
82
+ this.wso2,
72
83
  );
73
84
  this.inboundServer.on('error', (...args) => {
74
85
  this.logger.push({ args }).log('Unhandled error in Inbound Server');
@@ -79,76 +90,63 @@ class Server extends EventEmitter {
79
90
  this.conf,
80
91
  this.logger.push(LOG_ID.OUTBOUND),
81
92
  this.cache,
82
- this.metricsClient
93
+ this.metricsClient,
94
+ this.wso2,
83
95
  );
84
96
  this.outboundServer.on('error', (...args) => {
85
97
  this.logger.push({ args }).log('Unhandled error in Outbound Server');
86
98
  this.emit('error', 'Unhandled error in Outbound Server');
87
99
  });
88
100
 
89
- this.oauthTestServer = new OAuthTestServer({
90
- clientKey: this.conf.oauthTestServer.clientKey,
91
- clientSecret: this.conf.oauthTestServer.clientSecret,
92
- port: this.conf.oauthTestServer.listenPort,
93
- logger: this.logger.push(LOG_ID.OAUTHTEST),
94
- });
101
+ if (this.conf.oauthTestServer.enabled) {
102
+ this.oauthTestServer = new OAuthTestServer({
103
+ clientKey: this.conf.oauthTestServer.clientKey,
104
+ clientSecret: this.conf.oauthTestServer.clientSecret,
105
+ port: this.conf.oauthTestServer.listenPort,
106
+ logger: this.logger.push(LOG_ID.OAUTHTEST),
107
+ });
108
+ }
95
109
 
96
- this.testServer = new TestServer({
97
- port: this.conf.test.port,
98
- logger: this.logger.push(LOG_ID.TEST),
99
- cache: this.cache,
100
- });
110
+ if (this.conf.enableTestFeatures) {
111
+ this.testServer = new TestServer({
112
+ port: this.conf.test.port,
113
+ logger: this.logger.push(LOG_ID.TEST),
114
+ cache: this.cache,
115
+ });
116
+ }
117
+ }
118
+
119
+ async start() {
120
+ await this.cache.connect();
121
+ await this.wso2.auth.start();
122
+
123
+ // We only start the control client if we're running within Mojaloop Payment Manager.
124
+ // The control server is the Payment Manager Management API Service.
125
+ // We only start the client to connect to and listen to the Management API service for
126
+ // management protocol messages e.g configuration changes, certicate updates etc.
127
+ if (this.conf.pm4mlEnabled) {
128
+ const RESTART_INTERVAL_MS = 10000;
129
+ this.controlClient = await ControlAgent.Client.Create({
130
+ address: this.conf.control.mgmtAPIWsUrl,
131
+ port: this.conf.control.mgmtAPIWsPort,
132
+ logger: this.logger.push(LOG_ID.CONTROL),
133
+ appConfig: this.conf,
134
+ });
135
+ this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
136
+ this.controlClient.on('close', () => setTimeout(() => this.restart(_.merge({}, this.conf, { control: { stopped: Date.now() } })), RESTART_INTERVAL_MS));
137
+ }
138
+
139
+ await Promise.all([
140
+ this.inboundServer.start(),
141
+ this.outboundServer.start(),
142
+ this.metricsServer.start(),
143
+ this.testServer?.start(),
144
+ this.oauthTestServer?.start(),
145
+ ]);
101
146
  }
102
147
 
103
148
  async restart(newConf) {
104
- // Figuring out what config is necessary in each server and component is a pretty big job
105
- // that we'll have to save for later. For now, when the config changes, we'll restart
106
- // more than we might have to.
107
- // We'll do this by:
108
- // 0. creating a new instance of the logger, if necessary
109
- // 1. creating a new instance of the cache, if necessary
110
- // 2. calling the async reconfigure method of each of the servers as necessary- this will
111
- // return a synchronous function that we can call to swap over the server events and
112
- // object properties to the new ones. It will:
113
- // 1. remove the `request` listener for each of the HTTP servers
114
- // 2. add the new appropriate `request` listener
115
- // This results in a completely synchronous listener changeover to the new config and
116
- // therefore hopefully avoids any concurrency issues arising from restarting different
117
- // servers or components concurrently.
118
- // TODO: in the sense of being able to reason about the code, it would make some sense to
119
- // turn the config items or object passed to each server into an event emitter, or pass an
120
- // additional event emitter to the server constructor for the server to listen to and act
121
- // on changes. Before this, however, it's probably necessary to ensure each server gets
122
- // _only_ the config it needs, not the entire config object.
123
- // Further: it might be possible to use Object.observe for this functionality.
124
- // TODO: what happens if this is run concurrently? I.e. if it is called twice in rapid
125
- // succession. This question probably needs to be asked of the reconfigure message on every
126
- // server.
127
- // Note that it should be possible to reconfigure ports on a running server by reassigning
128
- // servers, e.g.
129
- // this.inboundServer._server = createHttpServer();
130
- // this.inboundServer._server.listen(newPort);
131
- // If there are conflicts, for example if the new configuration specifies the new inbound
132
- // port to be the same value as the old outbound port, this will require either
133
- // 1. some juggling of HTTP servers, e.g.
134
- // const oldInboundServer = this.inboundServer._server;
135
- // this.inboundServer._server = this.outboundServer._server;
136
- // .. etc.
137
- // 2. some juggling of sockets between servers, if possible
138
- // 3. rearchitecting of the servers, perhaps splitting the .start() method on the servers
139
- // to an .init() and .listen() methods, with the latter optionally taking an HTTP server
140
- // as argument
141
- // This _might_ introduce some confusion/complexity for existing websocket clients, but as
142
- // the event handlers _should_ not be modified this shouldn't be a problem. A careful
143
- // analysis of this will be necessary.
144
- assert(newConf.inbound.port === this.conf.inbound.port
145
- && newConf.outbound.port === this.conf.outbound.port
146
- && newConf.test.port === this.conf.test.port
147
- && newConf.oauthTestServer.listenPort === this.conf.oauthTestServer.listenPort
148
- && newConf.control.mgmtAPIWsPort === this.conf.control.mgmtAPIWsPort,
149
- 'Cannot reconfigure ports on running server');
150
- const doNothing = () => {};
151
- const updateLogger = check.notDeepEqual(newConf.logIndent, this.conf.logIndent);
149
+ const updateLogger = !_.isEqual(newConf.logIndent, this.conf.logIndent);
152
150
  if (updateLogger) {
153
151
  this.logger = new Logger.Logger({
154
152
  context: {
@@ -159,99 +157,118 @@ class Server extends EventEmitter {
159
157
  stringify: Logger.buildStringify({ space: this.conf.logIndent }),
160
158
  });
161
159
  }
160
+
162
161
  let oldCache;
163
- const updateCache = (
164
- updateLogger ||
165
- check.notDeepEqual(this.conf.cacheConfig, newConf.cacheConfig) ||
166
- check.notDeepEqual(this.conf.enableTestFeatures, newConf.enableTestFeatures)
167
- );
162
+ const updateCache = !_.isEqual(this.conf.cacheUrl, newConf.cacheUrl)
163
+ || !_.isEqual(this.conf.enableTestFeatures, newConf.enableTestFeatures);
168
164
  if (updateCache) {
169
165
  oldCache = this.cache;
166
+ await this.cache.disconnect();
170
167
  this.cache = new Cache({
171
- ...newConf.cacheConfig,
168
+ cacheUrl: newConf.cacheUrl,
172
169
  logger: this.logger.push(LOG_ID.CACHE),
173
170
  enableTestFeatures: newConf.enableTestFeatures,
174
171
  });
175
172
  await this.cache.connect();
176
173
  }
177
- const confChanged = !check.deepEqual(newConf, this.conf);
178
- // TODO: find better naming than "restart", because that's not really what's happening.
179
- const [restartInboundServer, restartOutboundServer, restartControlClient] = confChanged
180
- ? await Promise.all([
181
- this.inboundServer.reconfigure(newConf, this.logger.push(LOG_ID.INBOUND), this.cache),
182
- this.outboundServer.reconfigure(newConf, this.logger.push(LOG_ID.OUTBOUND), this.cache, this.metricsClient),
183
- this.controlClient.reconfigure({
184
- logger: this.logger.push(LOG_ID.CONTROL),
185
- port: newConf.control.mgmtAPIWsPort,
186
- appConfig: newConf
187
- }),
188
- ])
189
- : [doNothing, doNothing, doNothing];
190
- const updateOAuthTestServer = (
191
- updateLogger || check.notDeepEqual(newConf.oauthTestServer, this.conf.oauthTestServer)
192
- );
193
- const restartOAuthTestServer = updateOAuthTestServer
194
- ? await this.oauthTestServer.reconfigure({
195
- clientKey: this.conf.oauthTestServer.clientKey,
196
- clientSecret: this.conf.oauthTestServer.clientSecret,
197
- port: this.conf.oauthTestServer.listenPort,
198
- logger: this.logger.push(LOG_ID.OAUTHTEST),
199
- })
200
- : doNothing;
201
- const updateTestServer = (
202
- updateLogger || updateCache || check.notDeepEqual(newConf.test.port, this.conf.test.port)
203
- );
204
- const restartTestServer = updateTestServer
205
- ? await this.testServer.reconfigure({
206
- port: newConf.test.port,
207
- logger: this.logger.push(LOG_ID.TEST),
208
- cache: this.cache,
209
- })
210
- : doNothing;
211
- // You may not return an async restart function. Perform any required async activity in the
212
- // reconfigure function and return a sync restart function. See the note at the top of this
213
- // file.
214
- [restartTestServer, restartOAuthTestServer, restartInboundServer, restartOutboundServer, restartControlClient]
215
- .map(f => assert(Promise.resolve(f) !== f, 'Restart functions must be synchronous'));
216
- restartTestServer();
217
- restartOAuthTestServer();
218
- restartInboundServer();
219
- restartOutboundServer();
220
- restartControlClient();
221
- this.conf = newConf;
222
- await Promise.all([
223
- oldCache && oldCache.disconnect(),
224
- ]);
225
- }
226
174
 
227
- async start() {
228
- await this.cache.connect();
175
+ const updateWSO2 = !_.isEqual(this.conf.wso2, newConf.wso2)
176
+ || !_.isEqual(this.conf.outbound.tls, newConf.outbound.tls);
177
+ if (updateWSO2) {
178
+ this.wso2.auth.stop();
179
+ this.wso2.auth = new WSO2Auth({
180
+ ...newConf.wso2.auth,
181
+ logger: this.logger,
182
+ tlsCreds: newConf.outbound.tls.mutualTLS.enabled && newConf.outbound.tls.creds,
183
+ });
184
+ this.wso2.retryWso2AuthFailureTimes = newConf.wso2.requestAuthFailureRetryTimes;
185
+ this.wso2.auth.on('error', (msg) => {
186
+ this.emit('error', 'WSO2 auth error in InboundApi', msg);
187
+ });
188
+ await this.wso2.auth.start();
189
+ }
229
190
 
230
- const startTestServer = this.conf.enableTestFeatures ? this.testServer.start() : null;
231
- const startOauthTestServer = this.conf.oauthTestServer.enabled
232
- ? this.oauthTestServer.start()
233
- : null;
191
+ const updateInboundServer = !_.isEqual(this.conf.inbound, newConf.inbound);
192
+ if (updateInboundServer) {
193
+ await this.inboundServer.stop();
194
+ this.inboundServer = new InboundServer(
195
+ newConf,
196
+ this.logger.push(LOG_ID.INBOUND),
197
+ this.cache,
198
+ this.metricsClient,
199
+ this.wso2,
200
+ );
201
+ this.inboundServer.on('error', (...args) => {
202
+ this.logger.push({ args }).log('Unhandled error in Inbound Server');
203
+ this.emit('error', 'Unhandled error in Inbound Server');
204
+ });
205
+ await this.inboundServer.start();
206
+ }
234
207
 
235
- // We only start the control client if we're running within Mojaloop Payment Manager.
236
- // The control server is the Payment Manager Management API Service.
237
- // We only start the client to connect to and listen to the Management API service for
238
- // management protocol messages e.g configuration changes, certificate updates etc.
239
- if (this.conf.pm4mlEnabled) {
240
- this.controlClient = await ControlAgent.Client.Create({
241
- address: this.conf.control.mgmtAPIWsUrl,
242
- port: this.conf.control.mgmtAPIWsPort,
243
- logger: this.logger.push(LOG_ID.CONTROL),
244
- appConfig: this.conf,
208
+ const updateOutboundServer = !_.isEqual(this.conf.outbound, newConf.outbound);
209
+ if (updateOutboundServer) {
210
+ await this.outboundServer.stop();
211
+ this.outboundServer = new OutboundServer(
212
+ newConf,
213
+ this.logger.push(LOG_ID.OUTBOUND),
214
+ this.cache,
215
+ this.metricsClient,
216
+ this.wso2,
217
+ );
218
+ this.outboundServer.on('error', (...args) => {
219
+ this.logger.push({ args }).log('Unhandled error in Outbound Server');
220
+ this.emit('error', 'Unhandled error in Outbound Server');
245
221
  });
246
- this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
222
+ await this.outboundServer.start();
247
223
  }
248
224
 
225
+ const updateControlClient = !_.isEqual(this.conf.control, newConf.control);
226
+ if (updateControlClient) {
227
+ await this.controlClient?.stop();
228
+ if (this.conf.pm4mlEnabled) {
229
+ const RESTART_INTERVAL_MS = 10000;
230
+ this.controlClient = await ControlAgent.Client.Create({
231
+ address: newConf.control.mgmtAPIWsUrl,
232
+ port: newConf.control.mgmtAPIWsPort,
233
+ logger: this.logger.push(LOG_ID.CONTROL),
234
+ appConfig: newConf,
235
+ });
236
+ this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
237
+ this.controlClient.on('close', () => setTimeout(() => this.restart(_.merge({}, newConf, { control: { stopped: Date.now() } })), RESTART_INTERVAL_MS));
238
+ }
239
+ }
240
+
241
+ const updateOAuthTestServer = !_.isEqual(newConf.oauthTestServer, this.conf.oauthTestServer);
242
+ if (updateOAuthTestServer) {
243
+ await this.oauthTestServer?.stop();
244
+ if (this.conf.oauthTestServer.enabled) {
245
+ this.oauthTestServer = new OAuthTestServer({
246
+ clientKey: newConf.oauthTestServer.clientKey,
247
+ clientSecret: newConf.oauthTestServer.clientSecret,
248
+ port: newConf.oauthTestServer.listenPort,
249
+ logger: this.logger.push(LOG_ID.OAUTHTEST),
250
+ });
251
+ await this.oauthTestServer.start();
252
+ }
253
+ }
254
+
255
+ const updateTestServer = !_.isEqual(newConf.test.port, this.conf.test.port);
256
+ if (updateTestServer) {
257
+ await this.testServer?.stop();
258
+ if (this.conf.enableTestFeatures) {
259
+ this.testServer = new TestServer({
260
+ port: newConf.test.port,
261
+ logger: this.logger.push(LOG_ID.TEST),
262
+ cache: this.cache,
263
+ });
264
+ await this.testServer.start();
265
+ }
266
+ }
267
+
268
+ this.conf = newConf;
269
+
249
270
  await Promise.all([
250
- this.inboundServer.start(),
251
- this.outboundServer.start(),
252
- this.metricsServer.start(),
253
- startTestServer,
254
- startOauthTestServer,
271
+ oldCache?.disconnect(),
255
272
  ]);
256
273
  }
257
274
 
@@ -259,9 +276,9 @@ class Server extends EventEmitter {
259
276
  return Promise.all([
260
277
  this.inboundServer.stop(),
261
278
  this.outboundServer.stop(),
262
- this.oauthTestServer.stop(),
263
- this.testServer.stop(),
264
- this.controlClient.stop(),
279
+ this.oauthTestServer?.stop(),
280
+ this.testServer?.stop(),
281
+ this.controlClient?.stop(),
265
282
  this.metricsServer.stop(),
266
283
  ]);
267
284
  }