navy 7.0.0-alpha.4 → 7.1.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.
@@ -18,6 +18,8 @@ describe('cli/https', function () {
18
18
  let createCertStub;
19
19
  let generateRootCaStub;
20
20
  let removeCertStub;
21
+ let issueLeafCertForUrlStub;
22
+ let installRootCaToTrustStoreStub;
21
23
  let fsStub;
22
24
  let consoleLogStub;
23
25
  let httpsCli;
@@ -40,6 +42,12 @@ describe('cli/https', function () {
40
42
  createCertStub = sandbox.stub().resolves();
41
43
  generateRootCaStub = sandbox.stub().resolves();
42
44
  removeCertStub = sandbox.stub().resolves();
45
+ issueLeafCertForUrlStub = sandbox.stub().resolves({
46
+ certPath: '/out/host.crt',
47
+ keyPath: '/out/host.key',
48
+ caCopyPath: '/out/navy-root-ca.crt'
49
+ });
50
+ installRootCaToTrustStoreStub = sandbox.stub();
43
51
  fsStub = {
44
52
  existsSync: sandbox.stub().returns(true),
45
53
  readdirSync: sandbox.stub().returns(['web.local.crt', 'api.local.crt', 'something.txt'])
@@ -63,7 +71,11 @@ describe('cli/https', function () {
63
71
  '../util/https': {
64
72
  createCert: createCertStub,
65
73
  generateRootCa: generateRootCaStub,
66
- removeCert: removeCertStub
74
+ removeCert: removeCertStub,
75
+ issueLeafCertForUrl: issueLeafCertForUrlStub
76
+ },
77
+ '../util/install-root-ca': {
78
+ installRootCaToTrustStore: installRootCaToTrustStoreStub
67
79
  },
68
80
  fs: fsStub
69
81
  });
@@ -205,6 +217,186 @@ describe('cli/https', function () {
205
217
  const checkedPaths = fsStub.existsSync.getCalls().map(c => c.args[0]);
206
218
  (0, _chai.expect)(checkedPaths.some(p => p === '/custom-ca/ca.crt')).to.equal(true);
207
219
  });
220
+ it('should accept a single string service name', async function () {
221
+ await httpsCli('web', {
222
+ navy: 'env-1'
223
+ });
224
+ (0, _chai.expect)(createCertStub.callCount).to.equal(1);
225
+ });
226
+ it('should ignore non-string entries in a services array', async function () {
227
+ await httpsCli(['web', '', null, 42, 'api'], {
228
+ navy: 'env-1'
229
+ });
230
+ (0, _chai.expect)(createCertStub.callCount).to.equal(2);
231
+ });
232
+ it('should treat unsupported service arg types as an empty list', async function () {
233
+ fsStub.existsSync.returns(false);
234
+ await httpsCli(42, {
235
+ navy: 'env-1'
236
+ });
237
+ (0, _chai.expect)(createCertStub.called).to.equal(false);
238
+ (0, _chai.expect)(generateRootCaStub.called).to.equal(false);
239
+ });
240
+ it('should treat an empty string service arg as list mode', async function () {
241
+ fsStub.existsSync.returns(false);
242
+ await httpsCli('', {
243
+ navy: 'env-1'
244
+ });
245
+ (0, _chai.expect)(createCertStub.called).to.equal(false);
246
+ });
247
+ });
248
+ describe('validation', function () {
249
+ async function expectNavyError(promise, messagePattern) {
250
+ let caught;
251
+ try {
252
+ await promise;
253
+ } catch (err) {
254
+ caught = err;
255
+ }
256
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
257
+ (0, _chai.expect)(caught.message).to.match(messagePattern);
258
+ }
259
+ it('should reject combining disable with setup', async function () {
260
+ await expectNavyError(httpsCli([], {
261
+ disable: 'web',
262
+ setup: true
263
+ }), /cannot be combined/);
264
+ });
265
+ it('should reject combining disable with all', async function () {
266
+ await expectNavyError(httpsCli([], {
267
+ disable: 'web',
268
+ all: true
269
+ }), /cannot be combined/);
270
+ });
271
+ it('should reject combining disable with issue', async function () {
272
+ await expectNavyError(httpsCli([], {
273
+ disable: 'web',
274
+ issue: 'https://host'
275
+ }), /cannot be combined/);
276
+ });
277
+ it('should reject combining setup with all', async function () {
278
+ await expectNavyError(httpsCli([], {
279
+ setup: true,
280
+ all: true
281
+ }), /Use only one/);
282
+ });
283
+ it('should reject combining setup with issue', async function () {
284
+ await expectNavyError(httpsCli([], {
285
+ setup: true,
286
+ issue: 'https://host'
287
+ }), /Use only one/);
288
+ });
289
+ it('should reject combining all with issue', async function () {
290
+ await expectNavyError(httpsCli([], {
291
+ all: true,
292
+ issue: 'https://host'
293
+ }), /Use only one/);
294
+ });
295
+ });
296
+ describe('setup mode', function () {
297
+ it('should generate the root CA and install it in the trust store', async function () {
298
+ getConfigStub.returns({
299
+ tlsRootCaDir: '/custom-ca'
300
+ });
301
+ await httpsCli([], {
302
+ setup: true
303
+ });
304
+ (0, _chai.expect)(generateRootCaStub.calledOnce).to.equal(true);
305
+ (0, _chai.expect)(installRootCaToTrustStoreStub.calledOnce).to.equal(true);
306
+ (0, _chai.expect)(installRootCaToTrustStoreStub.firstCall.args[0]).to.equal('/custom-ca/ca.crt');
307
+ const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
308
+ (0, _chai.expect)(printed).to.contain('trust store');
309
+ });
310
+ it('should reject service names with setup', async function () {
311
+ let caught;
312
+ try {
313
+ await httpsCli(['web'], {
314
+ setup: true
315
+ });
316
+ } catch (err) {
317
+ caught = err;
318
+ }
319
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
320
+ (0, _chai.expect)(caught.message).to.contain('does not take service names');
321
+ });
322
+ });
323
+ describe('all mode', function () {
324
+ it('should enable HTTPS for every available service', async function () {
325
+ await httpsCli([], {
326
+ all: true,
327
+ navy: 'env-1'
328
+ });
329
+ (0, _chai.expect)(createCertStub.callCount).to.equal(2);
330
+ (0, _chai.expect)(reconfigureHTTPProxyStub.calledOnce).to.equal(true);
331
+ const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
332
+ (0, _chai.expect)(printed).to.contain('web');
333
+ (0, _chai.expect)(printed).to.contain('api');
334
+ });
335
+ it('should generate the root CA when cert files are missing', async function () {
336
+ fsStub.existsSync.callsFake(p => !p.endsWith('ca.crt') && !p.endsWith('ca.key'));
337
+ await httpsCli([], {
338
+ all: true,
339
+ navy: 'env-1'
340
+ });
341
+ (0, _chai.expect)(generateRootCaStub.calledOnce).to.equal(true);
342
+ });
343
+ it('should warn and return when no services are available', async function () {
344
+ navyStub.getAvailableServiceNames.resolves([]);
345
+ await httpsCli([], {
346
+ all: true,
347
+ navy: 'env-1'
348
+ });
349
+ (0, _chai.expect)(createCertStub.called).to.equal(false);
350
+ const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
351
+ (0, _chai.expect)(printed).to.contain('No services found');
352
+ });
353
+ it('should reject service names with all', async function () {
354
+ let caught;
355
+ try {
356
+ await httpsCli(['web'], {
357
+ all: true
358
+ });
359
+ } catch (err) {
360
+ caught = err;
361
+ }
362
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
363
+ (0, _chai.expect)(caught.message).to.contain('does not take service names');
364
+ });
365
+ });
366
+ describe('issue mode', function () {
367
+ it('should issue a leaf cert for an external URL', async function () {
368
+ const cwdStub = sandbox.stub(process, 'cwd').returns('/current');
369
+ await httpsCli([], {
370
+ issue: 'https://external.local',
371
+ navy: 'env-1'
372
+ });
373
+ (0, _chai.expect)(issueLeafCertForUrlStub.calledOnce).to.equal(true);
374
+ (0, _chai.expect)(issueLeafCertForUrlStub.firstCall.args).to.eql(['https://external.local', '/current']);
375
+ const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
376
+ (0, _chai.expect)(printed).to.contain('/out/host.crt');
377
+ (0, _chai.expect)(printed).to.contain('/out/host.key');
378
+ (0, _chai.expect)(printed).to.contain('/out/navy-root-ca.crt');
379
+ cwdStub.restore();
380
+ });
381
+ it('should honour issueOut when provided', async function () {
382
+ await httpsCli([], {
383
+ issue: 'https://external.local',
384
+ issueOut: '/custom/out'
385
+ });
386
+ (0, _chai.expect)(issueLeafCertForUrlStub.firstCall.args[1]).to.equal('/custom/out');
387
+ });
388
+ it('should reject service names with issue', async function () {
389
+ let caught;
390
+ try {
391
+ await httpsCli(['web'], {
392
+ issue: 'https://external.local'
393
+ });
394
+ } catch (err) {
395
+ caught = err;
396
+ }
397
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
398
+ (0, _chai.expect)(caught.message).to.contain('does not take service names');
399
+ });
208
400
  });
209
401
  });
210
402
  });
package/lib/cli/https.js CHANGED
@@ -5,16 +5,64 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = _default;
8
+ var _path = _interopRequireDefault(require("path"));
8
9
  var _chalk = _interopRequireDefault(require("chalk"));
9
10
  var _errors = require("../errors");
10
11
  var _navy = require("../navy");
11
12
  var _config = require("../config");
12
13
  var _httpProxy = require("../http-proxy");
13
14
  var _https = require("../util/https");
15
+ var _installRootCa = require("../util/install-root-ca");
14
16
  var _fs = _interopRequireDefault(require("fs"));
17
+ function normaliseServiceArgs(services) {
18
+ if (services == null) {
19
+ return [];
20
+ }
21
+ if (Array.isArray(services)) {
22
+ const result = [];
23
+ for (const s of services) {
24
+ if (typeof s === 'string' && s !== '') {
25
+ result.push(s);
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ if (typeof services === 'string') {
31
+ return services ? [services] : [];
32
+ }
33
+ return [];
34
+ }
35
+ async function issueHttpsForServices(navy, serviceNames, availableServices) {
36
+ const httpsReadyServices = [];
37
+ for (const service of serviceNames) {
38
+ if (!availableServices.includes(service)) {
39
+ console.log(`❌ ${service} not found, skipping`);
40
+ continue;
41
+ }
42
+ const serviceUrl = await navy.url(service);
43
+ try {
44
+ await (0, _https.createCert)({
45
+ serviceUrl
46
+ });
47
+ httpsReadyServices.push(service);
48
+ } catch (error) {
49
+ console.log(error);
50
+ throw new _errors.NavyError(`Could not generate TLS cert for ${service}`);
51
+ }
52
+ }
53
+ return httpsReadyServices;
54
+ }
15
55
  async function _default(services, opts) {
16
56
  const tlsRootCaDir = (0, _config.getConfig)().tlsRootCaDir || _config.DEFAULT_TLS_ROOT_CA_DIR;
17
57
  const configDir = (0, _config.getConfigDir)();
58
+ const serviceList = normaliseServiceArgs(services);
59
+ const hasIssue = Boolean(opts.issue);
60
+ if (opts.disable && (opts.setup || opts.all || hasIssue)) {
61
+ throw new _errors.NavyError('`--disable` cannot be combined with `--setup`, `--all`, or `--issue`.');
62
+ }
63
+ if ([opts.setup, opts.all, hasIssue].filter(Boolean).length > 1) {
64
+ throw new _errors.NavyError('Use only one of `--setup`, `--all`, or `--issue`.');
65
+ }
18
66
  if (opts.disable) {
19
67
  await (0, _https.removeCert)(opts);
20
68
 
@@ -31,7 +79,60 @@ async function _default(services, opts) {
31
79
  console.log();
32
80
  return;
33
81
  }
34
- if (!services || services.length === 0) {
82
+ if (opts.setup) {
83
+ if (serviceList.length > 0) {
84
+ throw new _errors.NavyError('`navy https --setup` does not take service names. Run `navy https <service>…` separately to issue service certificates.');
85
+ }
86
+ await (0, _https.generateRootCa)();
87
+ const caCrt = _path.default.join(tlsRootCaDir, 'ca.crt');
88
+ (0, _installRootCa.installRootCaToTrustStore)(caCrt);
89
+ console.log();
90
+ console.log(_chalk.default.green('✅ Navy root CA is installed in your trust store (where supported).'));
91
+ console.log(_chalk.default.dim('Browsers that use their own NSS store (e.g. Firefox) may still need the CA imported there.'));
92
+ console.log();
93
+ return;
94
+ }
95
+ if (opts.all) {
96
+ if (serviceList.length > 0) {
97
+ throw new _errors.NavyError('`navy https --all` does not take service names. Omit them, or name services without `--all`.');
98
+ }
99
+ if (!_fs.default.existsSync(`${tlsRootCaDir}/ca.crt`) || !_fs.default.existsSync(`${tlsRootCaDir}/ca.key`)) {
100
+ await (0, _https.generateRootCa)();
101
+ }
102
+ const navy = await (0, _navy.getNavy)(opts.navy);
103
+ await navy.ensurePluginsLoaded();
104
+ const availableServices = await navy.getAvailableServiceNames();
105
+ if (availableServices.length === 0) {
106
+ console.log();
107
+ console.log(_chalk.default.yellow('No services found in this navy; nothing to enable HTTPS for.'));
108
+ console.log();
109
+ return;
110
+ }
111
+ const httpsReadyServices = await issueHttpsForServices(navy, availableServices, availableServices);
112
+ await (0, _httpProxy.reconfigureHTTPProxy)({
113
+ restart: true,
114
+ navyFile: await navy.getNavyFile()
115
+ });
116
+ console.log();
117
+ console.log(_chalk.default.green(`✅ Service(s) ${httpsReadyServices.join(', ')} now accessible via HTTPS🔒`));
118
+ console.log();
119
+ return;
120
+ }
121
+ if (hasIssue) {
122
+ if (serviceList.length > 0) {
123
+ throw new _errors.NavyError('`navy https --issue` does not take service names.');
124
+ }
125
+ const outDir = opts.issueOut ? _path.default.resolve(opts.issueOut) : process.cwd();
126
+ const paths = await (0, _https.issueLeafCertForUrl)(String(opts.issue), outDir);
127
+ console.log();
128
+ console.log(_chalk.default.green('✅ TLS certificate issued for use outside Navy:'));
129
+ console.log(` ${paths.certPath}`);
130
+ console.log(` ${paths.keyPath}`);
131
+ console.log(` ${paths.caCopyPath} (root CA; configure clients to trust this or append as a chain file)`);
132
+ console.log();
133
+ return;
134
+ }
135
+ if (serviceList.length === 0) {
35
136
  if (!_fs.default.existsSync(`${configDir}/tls-certs`)) return;
36
137
  const files = _fs.default.readdirSync(`${configDir}/tls-certs`);
37
138
  const urls = files.filter(file => file.endsWith('.crt')).map(crt => {
@@ -48,23 +149,7 @@ async function _default(services, opts) {
48
149
  const navy = await (0, _navy.getNavy)(opts.navy);
49
150
  await navy.ensurePluginsLoaded();
50
151
  const availableServices = await navy.getAvailableServiceNames();
51
- const httpsReadyServices = [];
52
- for (const service of services) {
53
- if (!availableServices.includes(service)) {
54
- console.log(`❌ ${service} not found, skipping`);
55
- continue;
56
- }
57
- const serviceUrl = await navy.url(service);
58
- try {
59
- await (0, _https.createCert)({
60
- serviceUrl
61
- });
62
- httpsReadyServices.push(service);
63
- } catch (error) {
64
- console.log(error);
65
- throw new _errors.NavyError(`Could not generate TLS cert for ${service}`);
66
- }
67
- }
152
+ const httpsReadyServices = await issueHttpsForServices(navy, serviceList, availableServices);
68
153
  await (0, _httpProxy.reconfigureHTTPProxy)({
69
154
  restart: true,
70
155
  navyFile: await navy.getNavyFile()
@@ -171,7 +171,7 @@ _commander.program.command('health').option('-e, --navy [env]', `set the navy na
171
171
  _commander.program.command('wait-for-healthy [services...]').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Waits for the given services to be healthy').action(lazyRequire('./wait-for-healthy'));
172
172
  _commander.program.command('use-tag <service> <tag>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Uses a specific tag for the given service').action(basicCliWrapper('useTag'));
173
173
  _commander.program.command('reset-tag <service>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Resets any tag override on the given service').action(basicCliWrapper('resetTag'));
174
- _commander.program.command('https [services...]').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).option('-d, --disable <service>', 'disable https (deletes cert) for a given service', null).description('Prints or enables HTTPS services').action(lazyRequire('./https')).on('--help', () => console.log(`
174
+ _commander.program.command('https [services...]').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).option('-d, --disable <service>', 'disable https (deletes cert) for a given service', null).option('-s, --setup', 'generate the Navy root CA (if needed) and install it in the system trust store').option('-a, --all', 'enable HTTPS for every service in this navy').option('-i, --issue <url>', 'issue a TLS certificate and key for a URL (non-Navy service), signed with the Navy root CA').option('-o, --issue-out <dir>', 'output directory for --issue (default: current working directory)').description('Prints or enables HTTPS services').action(lazyRequire('./https')).on('--help', () => console.log(`
175
175
  Examples:
176
176
  List urls of services that listen on https:
177
177
  $ navy https
@@ -179,8 +179,18 @@ _commander.program.command('https [services...]').option('-e, --navy [env]', `se
179
179
  Enable https for mywebservice and anotherwebservice services
180
180
  $ navy https mywebservice anotherwebservice
181
181
 
182
+ Enable https for all services
183
+ $ navy https --all
184
+
182
185
  Disable https for mywebservice
183
186
  $ navy https -d mywebservice
187
+
188
+ Generate the Navy root CA and install it for this user (no service names)
189
+ $ navy https --setup
190
+
191
+ Issue a cert for an external host (writes leaf key/cert + navy-root-ca.crt)
192
+ $ navy https --issue https://api.example.test
193
+ $ navy https --issue https://api.example.test -o ./tls
184
194
  `));
185
195
  _commander.program.command('use-port <service> <internal> <external>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Uses a specific external port for the given service and internal port').action(basicCliWrapper('usePort'));
186
196
  _commander.program.command('reset-port <service> <internal>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Resets a specific external port mapping set by use-port').action(basicCliWrapper('resetPort'));
@@ -776,6 +776,148 @@ describe('navy/index', function () {
776
776
  (0, _chai.expect)(driver.destroy.calledOnce).to.equal(true);
777
777
  (0, _chai.expect)(deleteState.calledOnce).to.equal(true);
778
778
  });
779
+ it('should pass navyFile from a remaining navy when reconfiguring the proxy', async function () {
780
+ const driver = makeDriver();
781
+ const envPath = '/abs/env/Navyfile.js';
782
+ const otherPath = '/abs/other/Navyfile.js';
783
+ const destroyedNavyFile = {
784
+ httpProxyImage: 'destroyed/proxy:1'
785
+ };
786
+ const remainingNavyFile = {
787
+ httpProxyImage: 'remaining/proxy:1',
788
+ httpProxyEnv: {
789
+ FOO: 'bar'
790
+ }
791
+ };
792
+ const createProvider = path => ({
793
+ getNavyFilePath: async () => path
794
+ });
795
+ const resolveConfigProvider = _sinon.default.stub().callsFake(() => navy => createProvider(navy.normalisedName === 'other' ? otherPath : envPath));
796
+ const reconfigureHTTPProxy = _sinon.default.stub().resolves();
797
+ const deleteState = _sinon.default.stub().resolves();
798
+ const {
799
+ Navy
800
+ } = _proxyquire.default.noCallThru()('../', {
801
+ '../util/fs': {
802
+ readdirAsync: _sinon.default.stub().resolves(['env', 'other']),
803
+ lstatSync: _sinon.default.stub().returns({
804
+ isDirectory: () => true
805
+ })
806
+ },
807
+ './state': {
808
+ getState: _sinon.default.stub().resolves({
809
+ driver: 'docker-compose',
810
+ configProvider: 'filesystem'
811
+ }),
812
+ saveState: _sinon.default.stub().resolves(),
813
+ deleteState,
814
+ pathToNavys: _sinon.default.stub().returns('/home/test/.navy/navies'),
815
+ pathToNavy: _sinon.default.stub().callsFake(n => `/home/test/.navy/navies/${n}`)
816
+ },
817
+ '../driver': {
818
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
819
+ },
820
+ '../config-provider': {
821
+ resolveConfigProviderFromName: resolveConfigProvider
822
+ },
823
+ '../config': {
824
+ getConfig: _sinon.default.stub().returns({
825
+ externalIP: null
826
+ })
827
+ },
828
+ './plugin-interface': {
829
+ loadPlugins: _sinon.default.stub().resolves([])
830
+ },
831
+ './middleware': {
832
+ middlewareRunner: _sinon.default.stub().resolves()
833
+ },
834
+ '../http-proxy': {
835
+ reconfigureHTTPProxy
836
+ },
837
+ '../util/external-ip': {
838
+ getExternalIP: _sinon.default.stub().resolves('127.0.0.1')
839
+ },
840
+ '../util/service-host': {
841
+ createUrlForService: _sinon.default.stub().resolves('http://svc.local'),
842
+ getUrlFromService: _sinon.default.stub().returns(null)
843
+ },
844
+ [envPath]: destroyedNavyFile,
845
+ [otherPath]: remainingNavyFile
846
+ });
847
+ await new Navy('env').destroy();
848
+ (0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
849
+ (0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navies).to.eql(['other']);
850
+ (0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navyFile).to.eql(remainingNavyFile);
851
+ });
852
+ it('should skip uninitialised remaining navies when resolving navyFile for the proxy', async function () {
853
+ const driver = makeDriver();
854
+ const otherPath = '/abs/other/Navyfile.js';
855
+ const remainingNavyFile = {
856
+ httpProxyImage: 'remaining/proxy:1'
857
+ };
858
+ const createProvider = path => ({
859
+ getNavyFilePath: async () => path
860
+ });
861
+ const resolveConfigProvider = _sinon.default.stub().callsFake(() => navy => createProvider(navy.normalisedName === 'other' ? otherPath : '/abs/env/Navyfile.js'));
862
+ const reconfigureHTTPProxy = _sinon.default.stub().resolves();
863
+ const deleteState = _sinon.default.stub().resolves();
864
+ const getState = _sinon.default.stub().callsFake(name => {
865
+ if (name === 'uninit') return Promise.resolve(null);
866
+ return Promise.resolve({
867
+ driver: 'docker-compose',
868
+ configProvider: 'filesystem'
869
+ });
870
+ });
871
+ const {
872
+ Navy
873
+ } = _proxyquire.default.noCallThru()('../', {
874
+ '../util/fs': {
875
+ readdirAsync: _sinon.default.stub().resolves(['env', 'uninit', 'other']),
876
+ lstatSync: _sinon.default.stub().returns({
877
+ isDirectory: () => true
878
+ })
879
+ },
880
+ './state': {
881
+ getState,
882
+ saveState: _sinon.default.stub().resolves(),
883
+ deleteState,
884
+ pathToNavys: _sinon.default.stub().returns('/home/test/.navy/navies'),
885
+ pathToNavy: _sinon.default.stub().callsFake(n => `/home/test/.navy/navies/${n}`)
886
+ },
887
+ '../driver': {
888
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
889
+ },
890
+ '../config-provider': {
891
+ resolveConfigProviderFromName: resolveConfigProvider
892
+ },
893
+ '../config': {
894
+ getConfig: _sinon.default.stub().returns({
895
+ externalIP: null
896
+ })
897
+ },
898
+ './plugin-interface': {
899
+ loadPlugins: _sinon.default.stub().resolves([])
900
+ },
901
+ './middleware': {
902
+ middlewareRunner: _sinon.default.stub().resolves()
903
+ },
904
+ '../http-proxy': {
905
+ reconfigureHTTPProxy
906
+ },
907
+ '../util/external-ip': {
908
+ getExternalIP: _sinon.default.stub().resolves('127.0.0.1')
909
+ },
910
+ '../util/service-host': {
911
+ createUrlForService: _sinon.default.stub().resolves('http://svc.local'),
912
+ getUrlFromService: _sinon.default.stub().returns(null)
913
+ },
914
+ [otherPath]: remainingNavyFile
915
+ });
916
+ await new Navy('env').destroy();
917
+ (0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
918
+ (0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navies).to.eql(['uninit', 'other']);
919
+ (0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navyFile).to.eql(remainingNavyFile);
920
+ });
779
921
  it('should still delete state even when driver.destroy throws', async function () {
780
922
  const driver = makeDriver({
781
923
  destroy: _sinon.default.stub().rejects(new Error('boom'))
package/lib/navy/index.js CHANGED
@@ -237,9 +237,10 @@ class Navy extends _eventemitter.EventEmitter2 {
237
237
  if (!(await this.isInitialised())) {
238
238
  throw new _errors.NavyNotInitialisedError(this.name);
239
239
  }
240
+ const remainingNavyNames = (await getLaunchedNavyNames()).filter(navy => navy !== this.normalisedName);
240
241
  await (0, _httpProxy.reconfigureHTTPProxy)({
241
- navies: (await getLaunchedNavyNames()).filter(navy => navy !== this.normalisedName),
242
- navyFile: await this.getNavyFile()
242
+ navies: remainingNavyNames,
243
+ navyFile: await getNavyFileFromNavies(remainingNavyNames)
243
244
  });
244
245
  try {
245
246
  await (await this.safeGetDriver()).destroy();
@@ -499,6 +500,15 @@ function getNavy(navyName) {
499
500
  (0, _invariant.default)(navyName, "NO_NAVY_PROVIDED: No Navy provided");
500
501
  return new Navy(navyName);
501
502
  }
503
+ async function getNavyFileFromNavies(navyNames) {
504
+ for (const navyName of navyNames) {
505
+ const navy = getNavy(navyName);
506
+ if (!(await navy.isInitialised())) continue;
507
+ const navyFile = await navy.getNavyFile();
508
+ if (navyFile) return navyFile;
509
+ }
510
+ return null;
511
+ }
502
512
 
503
513
  /**
504
514
  * Returns an array of `Navy` instances which are currently imported and launched.