navy 6.0.0 → 7.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/lib/__tests__/config-provider.js +75 -0
  2. package/lib/__tests__/config.js +130 -0
  3. package/lib/__tests__/driver-logging.js +148 -0
  4. package/lib/__tests__/driver.js +19 -0
  5. package/lib/__tests__/errors.js +49 -0
  6. package/lib/__tests__/http-proxy.js +214 -0
  7. package/lib/__tests__/index.js +25 -0
  8. package/lib/__tests__/service.js +16 -0
  9. package/lib/cli/__tests__/develop.js +239 -0
  10. package/lib/cli/__tests__/external-ip.js +68 -0
  11. package/lib/cli/__tests__/health.js +257 -0
  12. package/lib/cli/__tests__/https.js +210 -0
  13. package/lib/cli/__tests__/import.js +110 -0
  14. package/lib/cli/__tests__/index.js +118 -0
  15. package/lib/cli/__tests__/lan-ip.js +90 -0
  16. package/lib/cli/__tests__/launch.js +179 -0
  17. package/lib/cli/__tests__/live.js +155 -0
  18. package/lib/cli/__tests__/local-ip.js +72 -0
  19. package/lib/cli/__tests__/logs.js +52 -0
  20. package/lib/cli/__tests__/open.js +65 -0
  21. package/lib/cli/__tests__/program.js +472 -0
  22. package/lib/cli/__tests__/ps.js +345 -0
  23. package/lib/cli/__tests__/refresh-config.js +95 -0
  24. package/lib/cli/__tests__/run.js +54 -0
  25. package/lib/cli/__tests__/status.js +204 -0
  26. package/lib/cli/__tests__/updates.js +243 -0
  27. package/lib/cli/__tests__/wait-for-healthy.js +134 -0
  28. package/lib/cli/config/__tests__/index.js +275 -0
  29. package/lib/cli/config/__tests__/wrapper.js +53 -0
  30. package/lib/cli/config/index.js +19 -37
  31. package/lib/cli/config/wrapper.js +0 -6
  32. package/lib/cli/develop.js +7 -21
  33. package/lib/cli/doctor/__tests__/clean-compose-files.js +78 -0
  34. package/lib/cli/doctor/__tests__/index.js +67 -0
  35. package/lib/cli/doctor/__tests__/invalid-compose-config.js +103 -0
  36. package/lib/cli/doctor/__tests__/invalid-state.js +83 -0
  37. package/lib/cli/doctor/__tests__/util.js +91 -0
  38. package/lib/cli/doctor/clean-compose-files.js +5 -13
  39. package/lib/cli/doctor/index.js +0 -12
  40. package/lib/cli/doctor/invalid-compose-config.js +0 -4
  41. package/lib/cli/doctor/invalid-state.js +0 -6
  42. package/lib/cli/doctor/util.js +0 -10
  43. package/lib/cli/external-ip.js +0 -4
  44. package/lib/cli/health.js +0 -12
  45. package/lib/cli/https.js +9 -25
  46. package/lib/cli/import.js +0 -12
  47. package/lib/cli/index.js +0 -9
  48. package/lib/cli/lan-ip.js +2 -8
  49. package/lib/cli/launch.js +0 -9
  50. package/lib/cli/live.js +6 -16
  51. package/lib/cli/local-ip.js +2 -7
  52. package/lib/cli/logs.js +0 -3
  53. package/lib/cli/open.js +2 -7
  54. package/lib/cli/program.js +73 -101
  55. package/lib/cli/ps.js +1 -21
  56. package/lib/cli/refresh-config.js +0 -7
  57. package/lib/cli/run.js +0 -3
  58. package/lib/cli/status.js +0 -11
  59. package/lib/cli/updates.js +0 -22
  60. package/lib/cli/util/__tests__/get-or-initialise-navy.js +66 -0
  61. package/lib/cli/util/__tests__/import.js +123 -0
  62. package/lib/cli/util/__tests__/index.js +17 -0
  63. package/lib/cli/util/__tests__/merge-action-options.js +47 -0
  64. package/lib/cli/util/__tests__/reconfigure.js +78 -0
  65. package/lib/cli/util/get-or-initialise-navy.js +0 -7
  66. package/lib/cli/util/import.js +0 -9
  67. package/lib/cli/util/index.js +0 -2
  68. package/lib/cli/util/merge-action-options.js +20 -0
  69. package/lib/cli/util/reconfigure.js +0 -4
  70. package/lib/cli/wait-for-healthy.js +0 -21
  71. package/lib/client/registry/__tests__/get-credentials.js +62 -0
  72. package/lib/client/registry/__tests__/get-endpoint.js +124 -0
  73. package/lib/client/registry/__tests__/get-fat-manifest.js +67 -0
  74. package/lib/client/registry/__tests__/get-token.js +66 -0
  75. package/lib/client/registry/__tests__/helpers.js +26 -0
  76. package/lib/client/registry/get-credentials.js +3 -9
  77. package/lib/client/registry/get-endpoint.js +29 -63
  78. package/lib/client/registry/get-fat-manifest.js +2 -9
  79. package/lib/client/registry/get-token.js +2 -13
  80. package/lib/client/registry/helpers.js +0 -4
  81. package/lib/config-provider.js +0 -12
  82. package/lib/config-providers/filesystem/__tests__/index.js +176 -0
  83. package/lib/config-providers/filesystem/index.js +5 -23
  84. package/lib/config-providers/npm/__tests__/index.js +226 -0
  85. package/lib/config-providers/npm/__tests__/util.js +1 -2
  86. package/lib/config-providers/npm/index.js +12 -35
  87. package/lib/config-providers/npm/util.js +0 -3
  88. package/lib/config.js +4 -19
  89. package/lib/domain/__tests__/container-image.js +81 -0
  90. package/lib/domain/__tests__/oci-api-specification.js +23 -0
  91. package/lib/domain/container-image.js +8 -21
  92. package/lib/domain/oci-api-specification.js +3 -5
  93. package/lib/driver-logging.js +0 -19
  94. package/lib/driver.js +0 -4
  95. package/lib/drivers/docker-compose/__tests__/client.js +249 -0
  96. package/lib/drivers/docker-compose/__tests__/index.js +430 -0
  97. package/lib/drivers/docker-compose/client.js +0 -16
  98. package/lib/drivers/docker-compose/index.js +7 -49
  99. package/lib/errors.js +0 -10
  100. package/lib/http-proxy.js +28 -23
  101. package/lib/index.js +1 -9
  102. package/lib/middleware/__tests__/add-service-proxy-config.js +258 -0
  103. package/lib/middleware/__tests__/develop.js +120 -0
  104. package/lib/middleware/__tests__/helpers.js +154 -0
  105. package/lib/middleware/__tests__/port-override.js +125 -0
  106. package/lib/middleware/__tests__/set-env-vars.js +94 -0
  107. package/lib/middleware/__tests__/set-image.js +76 -0
  108. package/lib/middleware/__tests__/set-logging-driver.js +94 -0
  109. package/lib/middleware/__tests__/tag-override.js +92 -0
  110. package/lib/middleware/add-service-proxy-config.js +8 -16
  111. package/lib/middleware/develop.js +2 -5
  112. package/lib/middleware/helpers.js +6 -12
  113. package/lib/middleware/port-override.js +5 -8
  114. package/lib/middleware/set-env-vars.js +6 -6
  115. package/lib/middleware/set-image.js +4 -5
  116. package/lib/middleware/set-logging-driver.js +6 -6
  117. package/lib/middleware/tag-override.js +4 -6
  118. package/lib/navy/__tests__/default-middleware.js +40 -0
  119. package/lib/navy/__tests__/index.js +1612 -0
  120. package/lib/navy/__tests__/middleware.js +71 -0
  121. package/lib/navy/__tests__/plugin-interface.js +121 -0
  122. package/lib/navy/__tests__/state.js +103 -0
  123. package/lib/navy/__tests__/util.js +24 -0
  124. package/lib/navy/default-middleware.js +0 -10
  125. package/lib/navy/index.js +83 -138
  126. package/lib/navy/middleware.js +0 -6
  127. package/lib/navy/plugin-interface.js +2 -10
  128. package/lib/navy/state.js +12 -24
  129. package/lib/navy/util.js +0 -1
  130. package/lib/service.js +2 -3
  131. package/lib/util/__tests__/exec-async.js +83 -0
  132. package/lib/util/__tests__/external-ip.js +97 -2
  133. package/lib/util/__tests__/get-lan-ip.js +46 -0
  134. package/lib/util/__tests__/has-update.js +136 -0
  135. package/lib/util/__tests__/https.js +301 -0
  136. package/lib/util/__tests__/navyrc.js +45 -0
  137. package/lib/util/__tests__/service-host.js +63 -5
  138. package/lib/util/__tests__/table.js +44 -0
  139. package/lib/util/docker-client.js +2 -10
  140. package/lib/util/exec-async.js +0 -4
  141. package/lib/util/external-ip.js +8 -12
  142. package/lib/util/fs.js +1 -6
  143. package/lib/util/get-lan-ip.js +0 -5
  144. package/lib/util/has-update.js +2 -14
  145. package/lib/util/https.js +11 -55
  146. package/lib/util/navyrc.js +0 -5
  147. package/lib/util/service-host.js +0 -17
  148. package/lib/util/table.js +0 -6
  149. package/package.json +14 -13
@@ -0,0 +1,1612 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _chai = require("chai");
5
+ var _sinon = _interopRequireDefault(require("sinon"));
6
+ var _proxyquire = _interopRequireDefault(require("proxyquire"));
7
+ var _errors = require("../../errors");
8
+ /* eslint-env mocha */
9
+
10
+ function loadNavyModule(stubs = {}) {
11
+ return _proxyquire.default.noCallThru()('../', {
12
+ '../util/fs': {
13
+ readdirAsync: _sinon.default.stub().resolves([]),
14
+ lstatSync: _sinon.default.stub().returns({
15
+ isDirectory: () => true
16
+ }),
17
+ ...stubs.fs
18
+ },
19
+ './state': {
20
+ getState: _sinon.default.stub().resolves(null),
21
+ saveState: _sinon.default.stub().resolves(),
22
+ deleteState: _sinon.default.stub().resolves(),
23
+ pathToNavys: _sinon.default.stub().returns('/home/test/.navy/navies'),
24
+ pathToNavy: _sinon.default.stub().callsFake(n => `/home/test/.navy/navies/${n}`),
25
+ ...stubs.state
26
+ },
27
+ '../driver': {
28
+ resolveDriverFromName: _sinon.default.stub().returns(null),
29
+ ...stubs.driver
30
+ },
31
+ '../config-provider': {
32
+ resolveConfigProviderFromName: _sinon.default.stub().returns(null),
33
+ ...stubs.configProvider
34
+ },
35
+ '../config': {
36
+ getConfig: _sinon.default.stub().returns({
37
+ externalIP: null
38
+ }),
39
+ ...stubs.config
40
+ },
41
+ './plugin-interface': {
42
+ loadPlugins: _sinon.default.stub().resolves([]),
43
+ ...stubs.pluginInterface
44
+ },
45
+ './middleware': {
46
+ middlewareRunner: _sinon.default.stub().resolves(),
47
+ ...stubs.middleware
48
+ },
49
+ '../http-proxy': {
50
+ reconfigureHTTPProxy: _sinon.default.stub().resolves(),
51
+ ...stubs.httpProxy
52
+ },
53
+ '../util/external-ip': {
54
+ getExternalIP: _sinon.default.stub().resolves('127.0.0.1'),
55
+ ...stubs.externalIP
56
+ },
57
+ '../util/service-host': {
58
+ createUrlForService: _sinon.default.stub().resolves('http://svc.local'),
59
+ getUrlFromService: _sinon.default.stub().returns(null),
60
+ ...stubs.serviceHost
61
+ }
62
+ });
63
+ }
64
+ function makeDriver(overrides = {}) {
65
+ return {
66
+ launch: _sinon.default.stub().resolves(),
67
+ destroy: _sinon.default.stub().resolves(),
68
+ ps: _sinon.default.stub().resolves([]),
69
+ start: _sinon.default.stub().resolves(),
70
+ stop: _sinon.default.stub().resolves(),
71
+ restart: _sinon.default.stub().resolves(),
72
+ kill: _sinon.default.stub().resolves(),
73
+ rm: _sinon.default.stub().resolves(),
74
+ update: _sinon.default.stub().resolves(),
75
+ spawnLogStream: _sinon.default.stub().resolves(),
76
+ port: _sinon.default.stub().resolves(null),
77
+ writeConfig: _sinon.default.stub().resolves(),
78
+ getConfig: _sinon.default.stub().resolves({
79
+ services: {}
80
+ }),
81
+ getLaunchedServiceNames: _sinon.default.stub().resolves([]),
82
+ getAvailableServiceNames: _sinon.default.stub().resolves([]),
83
+ ...overrides
84
+ };
85
+ }
86
+ describe('navy/index', function () {
87
+ describe('Navy class', function () {
88
+ it('should normalise the navy name and store it', function () {
89
+ const {
90
+ Navy
91
+ } = loadNavyModule();
92
+ const navy = new Navy('My Env-1');
93
+ (0, _chai.expect)(navy.name).to.equal('My Env-1');
94
+ (0, _chai.expect)(navy.normalisedName).to.equal('MyEnv1');
95
+ });
96
+ });
97
+ describe('getNavy', function () {
98
+ it('should return a Navy instance for the given name', function () {
99
+ const {
100
+ getNavy
101
+ } = loadNavyModule();
102
+ const navy = getNavy('myenv');
103
+ (0, _chai.expect)(navy.name).to.equal('myenv');
104
+ });
105
+ it('should throw when navyName is not provided', function () {
106
+ const {
107
+ getNavy
108
+ } = loadNavyModule();
109
+ (0, _chai.expect)(() => getNavy()).to.throw(/NO_NAVY_PROVIDED/);
110
+ (0, _chai.expect)(() => getNavy(null)).to.throw(/NO_NAVY_PROVIDED/);
111
+ (0, _chai.expect)(() => getNavy('')).to.throw(/NO_NAVY_PROVIDED/);
112
+ });
113
+ });
114
+ describe('isInitialised', function () {
115
+ it('should return true when state exists', async function () {
116
+ const {
117
+ Navy
118
+ } = loadNavyModule({
119
+ state: {
120
+ getState: _sinon.default.stub().resolves({
121
+ driver: 'docker-compose'
122
+ })
123
+ }
124
+ });
125
+ (0, _chai.expect)(await new Navy('env').isInitialised()).to.equal(true);
126
+ });
127
+ it('should return false when state is null', async function () {
128
+ const {
129
+ Navy
130
+ } = loadNavyModule({
131
+ state: {
132
+ getState: _sinon.default.stub().resolves(null)
133
+ }
134
+ });
135
+ (0, _chai.expect)(await new Navy('env').isInitialised()).to.equal(false);
136
+ });
137
+ });
138
+ describe('getState/saveState/initialise/delete', function () {
139
+ it('should delegate getState to state module', async function () {
140
+ const stub = _sinon.default.stub().resolves({
141
+ driver: 'docker-compose'
142
+ });
143
+ const {
144
+ Navy
145
+ } = loadNavyModule({
146
+ state: {
147
+ getState: stub
148
+ }
149
+ });
150
+ const result = await new Navy('env').getState();
151
+ (0, _chai.expect)(result).to.eql({
152
+ driver: 'docker-compose'
153
+ });
154
+ (0, _chai.expect)(stub.firstCall.args[0]).to.equal('env');
155
+ });
156
+ it('saveState should write state then run middleware', async function () {
157
+ const saveStub = _sinon.default.stub().resolves();
158
+ const middlewareStub = _sinon.default.stub().resolves();
159
+ const {
160
+ Navy
161
+ } = loadNavyModule({
162
+ state: {
163
+ saveState: saveStub
164
+ },
165
+ middleware: {
166
+ middlewareRunner: middlewareStub
167
+ }
168
+ });
169
+ const navy = new Navy('env');
170
+ const state = {
171
+ driver: 'docker-compose'
172
+ };
173
+ await navy.saveState(state);
174
+ (0, _chai.expect)(saveStub.calledWith('env', state)).to.equal(true);
175
+ (0, _chai.expect)(middlewareStub.calledOnce).to.equal(true);
176
+ });
177
+ it('initialise should write state with driver=docker-compose and provided opts', async function () {
178
+ const saveStub = _sinon.default.stub().resolves();
179
+ const {
180
+ Navy
181
+ } = loadNavyModule({
182
+ state: {
183
+ saveState: saveStub
184
+ }
185
+ });
186
+ await new Navy('env').initialise({
187
+ services: {
188
+ foo: {}
189
+ },
190
+ configProvider: 'filesystem'
191
+ });
192
+ const writtenState = saveStub.firstCall.args[1];
193
+ (0, _chai.expect)(writtenState).to.eql({
194
+ services: {
195
+ foo: {}
196
+ },
197
+ configProvider: 'filesystem',
198
+ driver: 'docker-compose'
199
+ });
200
+ });
201
+ it('delete should call deleteState', async function () {
202
+ const stub = _sinon.default.stub().resolves();
203
+ const {
204
+ Navy
205
+ } = loadNavyModule({
206
+ state: {
207
+ deleteState: stub
208
+ }
209
+ });
210
+ await new Navy('env').delete();
211
+ (0, _chai.expect)(stub.calledWith('env')).to.equal(true);
212
+ });
213
+ });
214
+ describe('getDriver', function () {
215
+ it('should return null when state is missing', async function () {
216
+ const {
217
+ Navy
218
+ } = loadNavyModule();
219
+ (0, _chai.expect)(await new Navy('env').getDriver()).to.equal(null);
220
+ });
221
+ it('should return null when state has no driver', async function () {
222
+ const {
223
+ Navy
224
+ } = loadNavyModule({
225
+ state: {
226
+ getState: _sinon.default.stub().resolves({})
227
+ }
228
+ });
229
+ (0, _chai.expect)(await new Navy('env').getDriver()).to.equal(null);
230
+ });
231
+ it('should return null when the driver name is unknown', async function () {
232
+ const {
233
+ Navy
234
+ } = loadNavyModule({
235
+ state: {
236
+ getState: _sinon.default.stub().resolves({
237
+ driver: 'unknown'
238
+ })
239
+ },
240
+ driver: {
241
+ resolveDriverFromName: _sinon.default.stub().returns(null)
242
+ }
243
+ });
244
+ (0, _chai.expect)(await new Navy('env').getDriver()).to.equal(null);
245
+ });
246
+ it('should instantiate and return the driver from resolveDriverFromName', async function () {
247
+ const driverInstance = makeDriver();
248
+ const factory = _sinon.default.stub().returns(driverInstance);
249
+ const {
250
+ Navy
251
+ } = loadNavyModule({
252
+ state: {
253
+ getState: _sinon.default.stub().resolves({
254
+ driver: 'docker-compose'
255
+ })
256
+ },
257
+ driver: {
258
+ resolveDriverFromName: _sinon.default.stub().returns(factory)
259
+ }
260
+ });
261
+ const navy = new Navy('env');
262
+ (0, _chai.expect)(await navy.getDriver()).to.equal(driverInstance);
263
+ (0, _chai.expect)(factory.firstCall.args[0]).to.equal(navy);
264
+ });
265
+ });
266
+ describe('safeGetDriver', function () {
267
+ it('should throw NavyNotInitialisedError when not initialised', async function () {
268
+ const {
269
+ Navy
270
+ } = loadNavyModule();
271
+ let caught;
272
+ try {
273
+ await new Navy('env').safeGetDriver();
274
+ } catch (e) {
275
+ caught = e;
276
+ }
277
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
278
+ });
279
+ it('should throw an invariant violation when state exists but no driver could be resolved', async function () {
280
+ const {
281
+ Navy
282
+ } = loadNavyModule({
283
+ state: {
284
+ getState: _sinon.default.stub().resolves({
285
+ driver: 'unknown'
286
+ })
287
+ },
288
+ driver: {
289
+ resolveDriverFromName: _sinon.default.stub().returns(null)
290
+ }
291
+ });
292
+ let caught;
293
+ try {
294
+ await new Navy('env').safeGetDriver();
295
+ } catch (e) {
296
+ caught = e;
297
+ }
298
+ (0, _chai.expect)(caught.message).to.match(/NO_DRIVER/);
299
+ });
300
+ it('should return the driver when initialised and resolvable', async function () {
301
+ const driverInstance = makeDriver();
302
+ const {
303
+ Navy
304
+ } = loadNavyModule({
305
+ state: {
306
+ getState: _sinon.default.stub().resolves({
307
+ driver: 'docker-compose'
308
+ })
309
+ },
310
+ driver: {
311
+ resolveDriverFromName: _sinon.default.stub().returns(() => driverInstance)
312
+ }
313
+ });
314
+ (0, _chai.expect)(await new Navy('env').safeGetDriver()).to.equal(driverInstance);
315
+ });
316
+ });
317
+ describe('getConfigProvider', function () {
318
+ it('should return null when state has no configProvider', async function () {
319
+ const {
320
+ Navy
321
+ } = loadNavyModule({
322
+ state: {
323
+ getState: _sinon.default.stub().resolves({})
324
+ }
325
+ });
326
+ (0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(null);
327
+ });
328
+ it('should return null when state is missing', async function () {
329
+ const {
330
+ Navy
331
+ } = loadNavyModule();
332
+ (0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(null);
333
+ });
334
+ it('should return null when configProvider is unknown', async function () {
335
+ const {
336
+ Navy
337
+ } = loadNavyModule({
338
+ state: {
339
+ getState: _sinon.default.stub().resolves({
340
+ configProvider: 'unknown'
341
+ })
342
+ },
343
+ configProvider: {
344
+ resolveConfigProviderFromName: _sinon.default.stub().returns(null)
345
+ }
346
+ });
347
+ (0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(null);
348
+ });
349
+ it('should instantiate the config provider from resolveConfigProviderFromName', async function () {
350
+ const provider = {
351
+ getNavyPath: async () => '/path'
352
+ };
353
+ const factory = _sinon.default.stub().returns(provider);
354
+ const {
355
+ Navy
356
+ } = loadNavyModule({
357
+ state: {
358
+ getState: _sinon.default.stub().resolves({
359
+ configProvider: 'filesystem'
360
+ })
361
+ },
362
+ configProvider: {
363
+ resolveConfigProviderFromName: _sinon.default.stub().returns(factory)
364
+ }
365
+ });
366
+ (0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(provider);
367
+ });
368
+ });
369
+ describe('getNavyFile', function () {
370
+ it('should throw NavyNotInitialisedError when not initialised', async function () {
371
+ const {
372
+ Navy
373
+ } = loadNavyModule();
374
+ let caught;
375
+ try {
376
+ await new Navy('env').getNavyFile();
377
+ } catch (e) {
378
+ caught = e;
379
+ }
380
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
381
+ });
382
+ it('should throw NO_CONFIG_PROVIDER when initialised but no provider', async function () {
383
+ const {
384
+ Navy
385
+ } = loadNavyModule({
386
+ state: {
387
+ getState: _sinon.default.stub().resolves({})
388
+ }
389
+ });
390
+ let caught;
391
+ try {
392
+ await new Navy('env').getNavyFile();
393
+ } catch (e) {
394
+ caught = e;
395
+ }
396
+ (0, _chai.expect)(caught.message).to.match(/NO_CONFIG_PROVIDER/);
397
+ });
398
+ it('should require the navy file path and return its module export', async function () {
399
+ const fakePath = '/abs/Navyfile.js';
400
+ const navyFile = {
401
+ plugins: ['p']
402
+ };
403
+ const provider = {
404
+ getNavyFilePath: async () => fakePath
405
+ };
406
+ const {
407
+ Navy
408
+ } = loadNavyModule({
409
+ state: {
410
+ getState: _sinon.default.stub().resolves({
411
+ configProvider: 'filesystem'
412
+ })
413
+ },
414
+ configProvider: {
415
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
416
+ }
417
+ });
418
+ const navy = new Navy('env');
419
+ const proxiedRequire = require('proxyquire').noCallThru();
420
+ const NavyMod = proxiedRequire('../', {
421
+ '../util/fs': {
422
+ readdirAsync: _sinon.default.stub().resolves([]),
423
+ lstatSync: _sinon.default.stub().returns({
424
+ isDirectory: () => true
425
+ })
426
+ },
427
+ './state': {
428
+ getState: _sinon.default.stub().resolves({
429
+ configProvider: 'filesystem'
430
+ }),
431
+ saveState: _sinon.default.stub(),
432
+ deleteState: _sinon.default.stub(),
433
+ pathToNavys: _sinon.default.stub().returns('/'),
434
+ pathToNavy: _sinon.default.stub()
435
+ },
436
+ '../driver': {
437
+ resolveDriverFromName: _sinon.default.stub().returns(null)
438
+ },
439
+ '../config-provider': {
440
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
441
+ },
442
+ '../config': {
443
+ getConfig: _sinon.default.stub().returns({
444
+ externalIP: null
445
+ })
446
+ },
447
+ './plugin-interface': {
448
+ loadPlugins: _sinon.default.stub().resolves([])
449
+ },
450
+ './middleware': {
451
+ middlewareRunner: _sinon.default.stub().resolves()
452
+ },
453
+ '../http-proxy': {
454
+ reconfigureHTTPProxy: _sinon.default.stub().resolves()
455
+ },
456
+ '../util/external-ip': {
457
+ getExternalIP: _sinon.default.stub().resolves('127.0.0.1')
458
+ },
459
+ '../util/service-host': {
460
+ createUrlForService: _sinon.default.stub(),
461
+ getUrlFromService: _sinon.default.stub()
462
+ },
463
+ [fakePath]: navyFile
464
+ });
465
+ const fresh = new NavyMod.Navy('env');
466
+ (0, _chai.expect)(await fresh.getNavyFile()).to.equal(navyFile);
467
+
468
+ // also check that not-required path returns null
469
+ provider.getNavyFilePath = async () => '/does-not-exist/Navyfile.js';
470
+ (0, _chai.expect)(await navy.getNavyFile()).to.equal(null);
471
+ });
472
+ });
473
+ describe('ensurePluginsLoaded', function () {
474
+ it('should be a no-op the second time when plugins are already loaded', async function () {
475
+ const loadStub = _sinon.default.stub().resolves([]);
476
+ const {
477
+ Navy
478
+ } = loadNavyModule({
479
+ pluginInterface: {
480
+ loadPlugins: loadStub
481
+ }
482
+ });
483
+ const navy = new Navy('env');
484
+ navy.getNavyFile = _sinon.default.stub().resolves({
485
+ plugins: []
486
+ });
487
+ await navy.ensurePluginsLoaded();
488
+ await navy.ensurePluginsLoaded();
489
+ (0, _chai.expect)(loadStub.calledOnce).to.equal(true);
490
+ (0, _chai.expect)(navy._pluginsLoaded).to.equal(true);
491
+ });
492
+ it('should not load plugins when navyFile is null', async function () {
493
+ const loadStub = _sinon.default.stub().resolves([]);
494
+ const provider = {
495
+ getNavyFilePath: async () => '/missing/Navyfile.js'
496
+ };
497
+ const {
498
+ Navy
499
+ } = loadNavyModule({
500
+ state: {
501
+ getState: _sinon.default.stub().resolves({
502
+ configProvider: 'filesystem'
503
+ })
504
+ },
505
+ configProvider: {
506
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
507
+ },
508
+ pluginInterface: {
509
+ loadPlugins: loadStub
510
+ }
511
+ });
512
+ await new Navy('env').ensurePluginsLoaded();
513
+ (0, _chai.expect)(loadStub.called).to.equal(false);
514
+ });
515
+ it('should call loadPlugins and mark _pluginsLoaded when navyFile is returned', async function () {
516
+ const loadStub = _sinon.default.stub().resolves([]);
517
+ const {
518
+ Navy
519
+ } = loadNavyModule({
520
+ pluginInterface: {
521
+ loadPlugins: loadStub
522
+ }
523
+ });
524
+ const navy = new Navy('env');
525
+ const navyFile = {
526
+ plugins: ['my-plugin']
527
+ };
528
+ navy.getNavyFile = _sinon.default.stub().resolves(navyFile);
529
+ await navy.ensurePluginsLoaded();
530
+ (0, _chai.expect)(loadStub.calledOnce).to.equal(true);
531
+ (0, _chai.expect)(loadStub.firstCall.args[0]).to.equal(navy);
532
+ (0, _chai.expect)(loadStub.firstCall.args[1]).to.equal(navyFile);
533
+ (0, _chai.expect)(navy._pluginsLoaded).to.equal(true);
534
+ });
535
+ });
536
+ describe('registerCommand / invokeCommand', function () {
537
+ it('should run the registered action when invoked', async function () {
538
+ const {
539
+ Navy
540
+ } = loadNavyModule();
541
+ const navy = new Navy('env');
542
+ const action = _sinon.default.stub().resolves();
543
+ navy.registerCommand('foo', action);
544
+ await navy.invokeCommand('foo', ['a', 'b']);
545
+ (0, _chai.expect)(action.calledOnce).to.equal(true);
546
+ (0, _chai.expect)(action.firstCall.args[0]).to.eql(['a', 'b']);
547
+ });
548
+ it('should throw a NavyError for unknown commands', async function () {
549
+ const {
550
+ Navy
551
+ } = loadNavyModule();
552
+ let caught;
553
+ try {
554
+ await new Navy('env').invokeCommand('missing', []);
555
+ } catch (e) {
556
+ caught = e;
557
+ }
558
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
559
+ (0, _chai.expect)(caught.message).to.contain('Unknown command "missing"');
560
+ });
561
+ });
562
+ describe('registerMiddleware', function () {
563
+ it('should append the middleware to _registeredMiddleware', function () {
564
+ const {
565
+ Navy
566
+ } = loadNavyModule();
567
+ const navy = new Navy('env');
568
+ const fn = () => null;
569
+ navy.registerMiddleware(fn);
570
+ (0, _chai.expect)(navy._registeredMiddleware).to.eql([fn]);
571
+ });
572
+ });
573
+ describe('launch', function () {
574
+ function setupForLaunch({
575
+ available = ['api', 'web'],
576
+ launched = []
577
+ } = {}) {
578
+ const driver = makeDriver({
579
+ getAvailableServiceNames: _sinon.default.stub().resolves(available),
580
+ getLaunchedServiceNames: _sinon.default.stub().resolves(launched)
581
+ });
582
+ const provider = {
583
+ getNavyFilePath: async () => '/missing'
584
+ };
585
+ const reconfigureHTTPProxy = _sinon.default.stub().resolves();
586
+ const {
587
+ Navy
588
+ } = loadNavyModule({
589
+ state: {
590
+ getState: _sinon.default.stub().resolves({
591
+ driver: 'docker-compose',
592
+ configProvider: 'filesystem'
593
+ })
594
+ },
595
+ driver: {
596
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
597
+ },
598
+ configProvider: {
599
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
600
+ },
601
+ httpProxy: {
602
+ reconfigureHTTPProxy
603
+ }
604
+ });
605
+ return {
606
+ driver,
607
+ Navy,
608
+ reconfigureHTTPProxy
609
+ };
610
+ }
611
+ it('should default to launching the currently launched services when no list is given', async function () {
612
+ const {
613
+ driver,
614
+ Navy
615
+ } = setupForLaunch({
616
+ available: ['api'],
617
+ launched: ['api']
618
+ });
619
+ await new Navy('env').launch();
620
+ (0, _chai.expect)(driver.launch.calledOnce).to.equal(true);
621
+ (0, _chai.expect)(driver.launch.firstCall.args[0]).to.eql(['api']);
622
+ });
623
+ it('should filter out unknown service names with a warning', async function () {
624
+ const log = _sinon.default.stub(console, 'log');
625
+ try {
626
+ const {
627
+ driver,
628
+ Navy
629
+ } = setupForLaunch({
630
+ available: ['api']
631
+ });
632
+ await new Navy('env').launch(['api', 'unknown']);
633
+ (0, _chai.expect)(driver.launch.firstCall.args[0]).to.eql(['api']);
634
+ (0, _chai.expect)(log.calledWith('Unknown service name: unknown')).to.equal(true);
635
+ } finally {
636
+ log.restore();
637
+ }
638
+ });
639
+ it('should return early without launching when all requested services are invalid', async function () {
640
+ const log = _sinon.default.stub(console, 'log');
641
+ try {
642
+ const {
643
+ driver,
644
+ Navy
645
+ } = setupForLaunch({
646
+ available: ['api']
647
+ });
648
+ await new Navy('env').launch(['unknown1', 'unknown2']);
649
+ (0, _chai.expect)(driver.launch.called).to.equal(false);
650
+ } finally {
651
+ log.restore();
652
+ }
653
+ });
654
+ it('should reconfigure the HTTP proxy after launch', async function () {
655
+ const {
656
+ Navy,
657
+ reconfigureHTTPProxy
658
+ } = setupForLaunch({
659
+ available: ['api'],
660
+ launched: ['api']
661
+ });
662
+ await new Navy('env').launch(['api']);
663
+ (0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
664
+ });
665
+ });
666
+ describe('reconfigure / relaunch', function () {
667
+ it('reconfigure should be a no-op when no services are launched', async function () {
668
+ const {
669
+ Navy
670
+ } = loadNavyModule({
671
+ state: {
672
+ getState: _sinon.default.stub().resolves({
673
+ driver: 'docker-compose'
674
+ })
675
+ },
676
+ driver: {
677
+ resolveDriverFromName: _sinon.default.stub().returns(() => makeDriver({
678
+ getLaunchedServiceNames: _sinon.default.stub().resolves([])
679
+ }))
680
+ }
681
+ });
682
+ await new Navy('env').reconfigure();
683
+ });
684
+ it('reconfigure should call launch when there are running services', async function () {
685
+ const driver = makeDriver({
686
+ getLaunchedServiceNames: _sinon.default.stub().resolves(['api']),
687
+ getAvailableServiceNames: _sinon.default.stub().resolves(['api'])
688
+ });
689
+ const provider = {
690
+ getNavyFilePath: async () => '/missing'
691
+ };
692
+ const {
693
+ Navy
694
+ } = loadNavyModule({
695
+ state: {
696
+ getState: _sinon.default.stub().resolves({
697
+ driver: 'docker-compose',
698
+ configProvider: 'filesystem'
699
+ })
700
+ },
701
+ driver: {
702
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
703
+ },
704
+ configProvider: {
705
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
706
+ }
707
+ });
708
+ await new Navy('env').reconfigure();
709
+ (0, _chai.expect)(driver.launch.calledOnce).to.equal(true);
710
+ });
711
+ it('relaunch should delegate to reconfigure', async function () {
712
+ const driver = makeDriver({
713
+ getLaunchedServiceNames: _sinon.default.stub().resolves([])
714
+ });
715
+ const {
716
+ Navy
717
+ } = loadNavyModule({
718
+ state: {
719
+ getState: _sinon.default.stub().resolves({
720
+ driver: 'docker-compose'
721
+ })
722
+ },
723
+ driver: {
724
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
725
+ }
726
+ });
727
+ await new Navy('env').relaunch();
728
+ });
729
+ });
730
+ describe('destroy', function () {
731
+ it('should throw NavyNotInitialisedError when not initialised', async function () {
732
+ const {
733
+ Navy
734
+ } = loadNavyModule();
735
+ let caught;
736
+ try {
737
+ await new Navy('env').destroy();
738
+ } catch (e) {
739
+ caught = e;
740
+ }
741
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
742
+ });
743
+ it('should reconfigure proxy excluding self, destroy the driver, and delete state', async function () {
744
+ const driver = makeDriver();
745
+ const provider = {
746
+ getNavyFilePath: async () => '/missing'
747
+ };
748
+ const reconfigureHTTPProxy = _sinon.default.stub().resolves();
749
+ const deleteState = _sinon.default.stub().resolves();
750
+ const {
751
+ Navy
752
+ } = loadNavyModule({
753
+ state: {
754
+ getState: _sinon.default.stub().resolves({
755
+ driver: 'docker-compose',
756
+ configProvider: 'filesystem'
757
+ }),
758
+ deleteState
759
+ },
760
+ driver: {
761
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
762
+ },
763
+ configProvider: {
764
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
765
+ },
766
+ httpProxy: {
767
+ reconfigureHTTPProxy
768
+ },
769
+ fs: {
770
+ readdirAsync: _sinon.default.stub().resolves(['env', 'other'])
771
+ }
772
+ });
773
+ await new Navy('env').destroy();
774
+ (0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
775
+ (0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navies).to.eql(['other']);
776
+ (0, _chai.expect)(driver.destroy.calledOnce).to.equal(true);
777
+ (0, _chai.expect)(deleteState.calledOnce).to.equal(true);
778
+ });
779
+ it('should still delete state even when driver.destroy throws', async function () {
780
+ const driver = makeDriver({
781
+ destroy: _sinon.default.stub().rejects(new Error('boom'))
782
+ });
783
+ const provider = {
784
+ getNavyFilePath: async () => '/missing'
785
+ };
786
+ const deleteState = _sinon.default.stub().resolves();
787
+ const {
788
+ Navy
789
+ } = loadNavyModule({
790
+ state: {
791
+ getState: _sinon.default.stub().resolves({
792
+ driver: 'docker-compose',
793
+ configProvider: 'filesystem'
794
+ }),
795
+ deleteState
796
+ },
797
+ driver: {
798
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
799
+ },
800
+ configProvider: {
801
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
802
+ },
803
+ httpProxy: {
804
+ reconfigureHTTPProxy: _sinon.default.stub().resolves()
805
+ }
806
+ });
807
+ await new Navy('env').destroy();
808
+ (0, _chai.expect)(deleteState.calledOnce).to.equal(true);
809
+ });
810
+ });
811
+ describe('start/stop/restart/kill/rm/update/spawnLogStream', function () {
812
+ function setup(driverOverrides = {}) {
813
+ const driver = makeDriver(driverOverrides);
814
+ const provider = {
815
+ getNavyFilePath: async () => '/missing'
816
+ };
817
+ const {
818
+ Navy
819
+ } = loadNavyModule({
820
+ state: {
821
+ getState: _sinon.default.stub().resolves({
822
+ driver: 'docker-compose',
823
+ configProvider: 'filesystem'
824
+ })
825
+ },
826
+ driver: {
827
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
828
+ },
829
+ configProvider: {
830
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
831
+ }
832
+ });
833
+ return {
834
+ driver,
835
+ navy: new Navy('env')
836
+ };
837
+ }
838
+ ;
839
+ [{
840
+ method: 'start',
841
+ explicit: ['api']
842
+ }, {
843
+ method: 'stop',
844
+ explicit: ['api']
845
+ }, {
846
+ method: 'restart',
847
+ explicit: ['api']
848
+ }, {
849
+ method: 'kill',
850
+ explicit: ['api']
851
+ }].forEach(({
852
+ method,
853
+ explicit
854
+ }) => {
855
+ it(`${method} should pass the explicit list through to the driver`, async function () {
856
+ const {
857
+ driver,
858
+ navy
859
+ } = setup();
860
+ await navy[method](explicit);
861
+ (0, _chai.expect)(driver[method].calledOnce).to.equal(true);
862
+ (0, _chai.expect)(driver[method].firstCall.args[0]).to.eql(explicit);
863
+ });
864
+ it(`${method} should default to currently launched services when none provided`, async function () {
865
+ const {
866
+ driver,
867
+ navy
868
+ } = setup({
869
+ getLaunchedServiceNames: _sinon.default.stub().resolves(['api', 'web'])
870
+ });
871
+ await navy[method]();
872
+ (0, _chai.expect)(driver[method].firstCall.args[0]).to.eql(['api', 'web']);
873
+ });
874
+ });
875
+ it('rm should default to currently launched services when none provided', async function () {
876
+ const {
877
+ driver,
878
+ navy
879
+ } = setup({
880
+ getLaunchedServiceNames: _sinon.default.stub().resolves(['x'])
881
+ });
882
+ await navy.rm();
883
+ (0, _chai.expect)(driver.rm.firstCall.args[0]).to.eql(['x']);
884
+ });
885
+ it('rm should accept an explicit list', async function () {
886
+ const {
887
+ driver,
888
+ navy
889
+ } = setup();
890
+ await navy.rm(['x']);
891
+ (0, _chai.expect)(driver.rm.firstCall.args[0]).to.eql(['x']);
892
+ });
893
+ it('update should default to all available services', async function () {
894
+ const {
895
+ driver,
896
+ navy
897
+ } = setup({
898
+ getAvailableServiceNames: _sinon.default.stub().resolves(['a', 'b'])
899
+ });
900
+ await navy.update();
901
+ (0, _chai.expect)(driver.update.firstCall.args[0]).to.eql(['a', 'b']);
902
+ });
903
+ it('update should accept an explicit list', async function () {
904
+ const {
905
+ driver,
906
+ navy
907
+ } = setup();
908
+ await navy.update(['x']);
909
+ (0, _chai.expect)(driver.update.firstCall.args[0]).to.eql(['x']);
910
+ });
911
+ it('spawnLogStream should default to currently launched services when none provided', async function () {
912
+ const {
913
+ driver,
914
+ navy
915
+ } = setup({
916
+ getLaunchedServiceNames: _sinon.default.stub().resolves(['l'])
917
+ });
918
+ await navy.spawnLogStream();
919
+ (0, _chai.expect)(driver.spawnLogStream.firstCall.args[0]).to.eql(['l']);
920
+ });
921
+ it('spawnLogStream should accept an explicit list', async function () {
922
+ const {
923
+ driver,
924
+ navy
925
+ } = setup();
926
+ await navy.spawnLogStream(['l']);
927
+ (0, _chai.expect)(driver.spawnLogStream.firstCall.args[0]).to.eql(['l']);
928
+ });
929
+ it('start should reconfigure the HTTP proxy', async function () {
930
+ const driver = makeDriver({
931
+ getLaunchedServiceNames: _sinon.default.stub().resolves(['api'])
932
+ });
933
+ const provider = {
934
+ getNavyFilePath: async () => '/missing'
935
+ };
936
+ const reconfigure = _sinon.default.stub().resolves();
937
+ const {
938
+ Navy
939
+ } = loadNavyModule({
940
+ state: {
941
+ getState: _sinon.default.stub().resolves({
942
+ driver: 'docker-compose',
943
+ configProvider: 'filesystem'
944
+ })
945
+ },
946
+ driver: {
947
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
948
+ },
949
+ configProvider: {
950
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
951
+ },
952
+ httpProxy: {
953
+ reconfigureHTTPProxy: reconfigure
954
+ }
955
+ });
956
+ await new Navy('env').start(['api']);
957
+ (0, _chai.expect)(reconfigure.calledOnce).to.equal(true);
958
+ });
959
+ });
960
+ describe('useTag/resetTag/usePort/resetPort', function () {
961
+ function setupTagPort({
962
+ initialState = {
963
+ driver: 'docker-compose',
964
+ configProvider: 'filesystem',
965
+ services: {}
966
+ }
967
+ } = {}) {
968
+ const driver = makeDriver({
969
+ getAvailableServiceNames: _sinon.default.stub().resolves(['api']),
970
+ getLaunchedServiceNames: _sinon.default.stub().resolves([])
971
+ });
972
+ let currentState = initialState;
973
+ const getStateStub = _sinon.default.stub().callsFake(async () => currentState);
974
+ const saveStub = _sinon.default.stub().callsFake(async (envName, state) => {
975
+ currentState = state;
976
+ });
977
+ const provider = {
978
+ getNavyFilePath: async () => '/missing'
979
+ };
980
+ const {
981
+ Navy
982
+ } = loadNavyModule({
983
+ state: {
984
+ getState: getStateStub,
985
+ saveState: saveStub
986
+ },
987
+ driver: {
988
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
989
+ },
990
+ configProvider: {
991
+ resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
992
+ }
993
+ });
994
+ return {
995
+ Navy,
996
+ driver,
997
+ saveStub
998
+ };
999
+ }
1000
+ it('useTag should record the tag in state, kill the service, and update it', async function () {
1001
+ const {
1002
+ Navy,
1003
+ driver,
1004
+ saveStub
1005
+ } = setupTagPort();
1006
+ await new Navy('env').useTag('api', 'v2');
1007
+ const written = saveStub.firstCall.args[1];
1008
+ (0, _chai.expect)(written.services.api._tag).to.equal('v2');
1009
+ (0, _chai.expect)(driver.kill.firstCall.args[0]).to.eql(['api']);
1010
+ (0, _chai.expect)(driver.update.firstCall.args[0]).to.eql(['api']);
1011
+ });
1012
+
1013
+ // NOTE: useTag/resetTag/usePort/resetPort all start with
1014
+ // `(await this.getState()) || {}` and write the result back as the new
1015
+ // state. If state was never initialised, the saved state has no `driver`
1016
+ // key, and the subsequent kill/launch then fails with a NO_DRIVER invariant.
1017
+ // We therefore exercise these methods only with a properly initialised state.
1018
+ it('useTag should preserve other services in state.services when modifying one', async function () {
1019
+ const initial = {
1020
+ driver: 'docker-compose',
1021
+ configProvider: 'filesystem',
1022
+ services: {
1023
+ other: {
1024
+ _tag: 'keep-me'
1025
+ }
1026
+ }
1027
+ };
1028
+ const {
1029
+ Navy,
1030
+ saveStub
1031
+ } = setupTagPort({
1032
+ initialState: initial
1033
+ });
1034
+ await new Navy('env').useTag('api', 'v2');
1035
+ const written = saveStub.firstCall.args[1];
1036
+ (0, _chai.expect)(written.services.other).to.eql({
1037
+ _tag: 'keep-me'
1038
+ });
1039
+ (0, _chai.expect)(written.services.api._tag).to.equal('v2');
1040
+ });
1041
+ it('resetTag should clear the tag and relaunch the service with noDeps', async function () {
1042
+ const {
1043
+ Navy,
1044
+ driver,
1045
+ saveStub
1046
+ } = setupTagPort({
1047
+ initialState: {
1048
+ driver: 'docker-compose',
1049
+ configProvider: 'filesystem',
1050
+ services: {
1051
+ api: {
1052
+ _tag: 'old'
1053
+ }
1054
+ }
1055
+ }
1056
+ });
1057
+ await new Navy('env').resetTag('api');
1058
+ (0, _chai.expect)(saveStub.firstCall.args[1].services.api._tag).to.equal(undefined);
1059
+ (0, _chai.expect)(driver.launch.firstCall.args[1]).to.eql({
1060
+ noDeps: true
1061
+ });
1062
+ });
1063
+ it('resetTag should preserve services other than the one being reset', async function () {
1064
+ const {
1065
+ Navy,
1066
+ saveStub
1067
+ } = setupTagPort({
1068
+ initialState: {
1069
+ driver: 'docker-compose',
1070
+ configProvider: 'filesystem',
1071
+ services: {
1072
+ api: {
1073
+ _tag: 'old'
1074
+ },
1075
+ other: {
1076
+ _tag: 'keep'
1077
+ }
1078
+ }
1079
+ }
1080
+ });
1081
+ await new Navy('env').resetTag('api');
1082
+ const written = saveStub.firstCall.args[1];
1083
+ (0, _chai.expect)(written.services.other).to.eql({
1084
+ _tag: 'keep'
1085
+ });
1086
+ (0, _chai.expect)(written.services.api._tag).to.equal(undefined);
1087
+ });
1088
+ it('usePort should record port mapping in state', async function () {
1089
+ const {
1090
+ Navy,
1091
+ saveStub
1092
+ } = setupTagPort();
1093
+ await new Navy('env').usePort('api', 80, 8080);
1094
+ (0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports).to.eql({
1095
+ 80: 8080
1096
+ });
1097
+ });
1098
+ it('usePort should merge the new port mapping with existing ports for the service', async function () {
1099
+ const {
1100
+ Navy,
1101
+ saveStub
1102
+ } = setupTagPort({
1103
+ initialState: {
1104
+ driver: 'docker-compose',
1105
+ configProvider: 'filesystem',
1106
+ services: {
1107
+ api: {
1108
+ _ports: {
1109
+ 443: 4433
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ });
1115
+ await new Navy('env').usePort('api', 80, 8080);
1116
+ const ports = saveStub.firstCall.args[1].services.api._ports;
1117
+ (0, _chai.expect)(ports).to.eql({
1118
+ 80: 8080,
1119
+ 443: 4433
1120
+ });
1121
+ });
1122
+ it('resetPort should set the port to undefined', async function () {
1123
+ const {
1124
+ Navy,
1125
+ saveStub
1126
+ } = setupTagPort({
1127
+ initialState: {
1128
+ driver: 'docker-compose',
1129
+ configProvider: 'filesystem',
1130
+ services: {
1131
+ api: {
1132
+ _ports: {
1133
+ 80: 8080
1134
+ }
1135
+ }
1136
+ }
1137
+ }
1138
+ });
1139
+ await new Navy('env').resetPort('api', 80);
1140
+ (0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports[80]).to.equal(undefined);
1141
+ });
1142
+ it('resetPort should preserve other ports on the same service', async function () {
1143
+ const {
1144
+ Navy,
1145
+ saveStub
1146
+ } = setupTagPort({
1147
+ initialState: {
1148
+ driver: 'docker-compose',
1149
+ configProvider: 'filesystem',
1150
+ services: {
1151
+ api: {
1152
+ _ports: {
1153
+ 80: 8080,
1154
+ 443: 4433
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ });
1160
+ await new Navy('env').resetPort('api', 80);
1161
+ const ports = saveStub.firstCall.args[1].services.api._ports;
1162
+ (0, _chai.expect)(ports[80]).to.equal(undefined);
1163
+ (0, _chai.expect)(ports[443]).to.equal(4433);
1164
+ });
1165
+ it('useTag should throw NavyNotInitialisedError when state is missing', async function () {
1166
+ const {
1167
+ Navy
1168
+ } = loadNavyModule();
1169
+ let caught;
1170
+ try {
1171
+ await new Navy('env').useTag('api', 'v1');
1172
+ } catch (e) {
1173
+ caught = e;
1174
+ }
1175
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
1176
+ });
1177
+ it('resetTag should throw NavyNotInitialisedError when state is missing', async function () {
1178
+ const {
1179
+ Navy
1180
+ } = loadNavyModule();
1181
+ let caught;
1182
+ try {
1183
+ await new Navy('env').resetTag('api');
1184
+ } catch (e) {
1185
+ caught = e;
1186
+ }
1187
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
1188
+ });
1189
+ it('usePort should throw NavyNotInitialisedError when state is missing', async function () {
1190
+ const {
1191
+ Navy
1192
+ } = loadNavyModule();
1193
+ let caught;
1194
+ try {
1195
+ await new Navy('env').usePort('api', 80, 8080);
1196
+ } catch (e) {
1197
+ caught = e;
1198
+ }
1199
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
1200
+ });
1201
+ it('resetPort should throw NavyNotInitialisedError when state is missing', async function () {
1202
+ const {
1203
+ Navy
1204
+ } = loadNavyModule();
1205
+ let caught;
1206
+ try {
1207
+ await new Navy('env').resetPort('api', 80);
1208
+ } catch (e) {
1209
+ caught = e;
1210
+ }
1211
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
1212
+ });
1213
+ it('useTag should treat missing state.services as an empty services map', async function () {
1214
+ const {
1215
+ Navy,
1216
+ saveStub
1217
+ } = setupTagPort({
1218
+ initialState: {
1219
+ driver: 'docker-compose',
1220
+ configProvider: 'filesystem'
1221
+ }
1222
+ });
1223
+ await new Navy('env').useTag('api', 'v2');
1224
+ (0, _chai.expect)(saveStub.firstCall.args[1].services.api._tag).to.equal('v2');
1225
+ });
1226
+ it('resetTag should treat missing state.services as an empty services map', async function () {
1227
+ const {
1228
+ Navy,
1229
+ saveStub
1230
+ } = setupTagPort({
1231
+ initialState: {
1232
+ driver: 'docker-compose',
1233
+ configProvider: 'filesystem'
1234
+ }
1235
+ });
1236
+ await new Navy('env').resetTag('api');
1237
+ (0, _chai.expect)(saveStub.firstCall.args[1].services.api._tag).to.equal(undefined);
1238
+ });
1239
+ it('usePort should treat missing state.services as an empty services map', async function () {
1240
+ const {
1241
+ Navy,
1242
+ saveStub
1243
+ } = setupTagPort({
1244
+ initialState: {
1245
+ driver: 'docker-compose',
1246
+ configProvider: 'filesystem'
1247
+ }
1248
+ });
1249
+ await new Navy('env').usePort('api', 80, 8080);
1250
+ (0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports).to.eql({
1251
+ 80: 8080
1252
+ });
1253
+ });
1254
+ it('resetPort should treat missing state.services as an empty services map', async function () {
1255
+ const {
1256
+ Navy,
1257
+ saveStub
1258
+ } = setupTagPort({
1259
+ initialState: {
1260
+ driver: 'docker-compose',
1261
+ configProvider: 'filesystem'
1262
+ }
1263
+ });
1264
+ await new Navy('env').resetPort('api', 80);
1265
+ (0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports[80]).to.equal(undefined);
1266
+ });
1267
+ });
1268
+ describe('externalIP', function () {
1269
+ it('should return the IP from getExternalIP using config.externalIP', async function () {
1270
+ const {
1271
+ Navy
1272
+ } = loadNavyModule({
1273
+ config: {
1274
+ getConfig: _sinon.default.stub().returns({
1275
+ externalIP: 'configured.host'
1276
+ })
1277
+ },
1278
+ externalIP: {
1279
+ getExternalIP: _sinon.default.stub().resolves('1.2.3.4')
1280
+ }
1281
+ });
1282
+ (0, _chai.expect)(await new Navy('env').externalIP()).to.equal('1.2.3.4');
1283
+ });
1284
+ });
1285
+ describe('port', function () {
1286
+ it('should delegate to driver.port with the privatePort and index defaulting to 1', async function () {
1287
+ const driver = makeDriver({
1288
+ port: _sinon.default.stub().resolves(8080)
1289
+ });
1290
+ const {
1291
+ Navy
1292
+ } = loadNavyModule({
1293
+ state: {
1294
+ getState: _sinon.default.stub().resolves({
1295
+ driver: 'docker-compose'
1296
+ })
1297
+ },
1298
+ driver: {
1299
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
1300
+ }
1301
+ });
1302
+ (0, _chai.expect)(await new Navy('env').port('api', 80)).to.equal(8080);
1303
+ (0, _chai.expect)(driver.port.firstCall.args).to.eql(['api', 80, 1]);
1304
+ });
1305
+ });
1306
+ describe('url', function () {
1307
+ it('should return getUrlFromService when there is a running service URL', async function () {
1308
+ const driver = makeDriver({
1309
+ ps: _sinon.default.stub().resolves([{
1310
+ name: 'api'
1311
+ }])
1312
+ });
1313
+ const {
1314
+ Navy
1315
+ } = loadNavyModule({
1316
+ state: {
1317
+ getState: _sinon.default.stub().resolves({
1318
+ driver: 'docker-compose'
1319
+ })
1320
+ },
1321
+ driver: {
1322
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
1323
+ },
1324
+ serviceHost: {
1325
+ getUrlFromService: _sinon.default.stub().returns('http://from-service'),
1326
+ createUrlForService: _sinon.default.stub().resolves('http://fallback')
1327
+ }
1328
+ });
1329
+ (0, _chai.expect)(await new Navy('env').url('api')).to.equal('http://from-service');
1330
+ });
1331
+ it('should fall back to createUrlForService when getUrlFromService returns null', async function () {
1332
+ const driver = makeDriver({
1333
+ ps: _sinon.default.stub().resolves([{
1334
+ name: 'api'
1335
+ }])
1336
+ });
1337
+ const {
1338
+ Navy
1339
+ } = loadNavyModule({
1340
+ state: {
1341
+ getState: _sinon.default.stub().resolves({
1342
+ driver: 'docker-compose'
1343
+ })
1344
+ },
1345
+ driver: {
1346
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
1347
+ },
1348
+ serviceHost: {
1349
+ getUrlFromService: _sinon.default.stub().returns(null),
1350
+ createUrlForService: _sinon.default.stub().resolves('http://fallback')
1351
+ },
1352
+ externalIP: {
1353
+ getExternalIP: _sinon.default.stub().resolves('1.2.3.4')
1354
+ }
1355
+ });
1356
+ (0, _chai.expect)(await new Navy('env').url('api')).to.equal('http://fallback');
1357
+ });
1358
+ });
1359
+ describe('ps / getLaunchedServiceNames / getAvailableServiceNames', function () {
1360
+ it('ps should delegate to the driver', async function () {
1361
+ const driver = makeDriver({
1362
+ ps: _sinon.default.stub().resolves([{
1363
+ name: 'api'
1364
+ }])
1365
+ });
1366
+ const {
1367
+ Navy
1368
+ } = loadNavyModule({
1369
+ state: {
1370
+ getState: _sinon.default.stub().resolves({
1371
+ driver: 'docker-compose'
1372
+ })
1373
+ },
1374
+ driver: {
1375
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
1376
+ }
1377
+ });
1378
+ (0, _chai.expect)(await new Navy('env').ps()).to.eql([{
1379
+ name: 'api'
1380
+ }]);
1381
+ });
1382
+ it('getLaunchedServiceNames should delegate to driver', async function () {
1383
+ const driver = makeDriver({
1384
+ getLaunchedServiceNames: _sinon.default.stub().resolves(['x'])
1385
+ });
1386
+ const {
1387
+ Navy
1388
+ } = loadNavyModule({
1389
+ state: {
1390
+ getState: _sinon.default.stub().resolves({
1391
+ driver: 'docker-compose'
1392
+ })
1393
+ },
1394
+ driver: {
1395
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
1396
+ }
1397
+ });
1398
+ (0, _chai.expect)(await new Navy('env').getLaunchedServiceNames()).to.eql(['x']);
1399
+ });
1400
+ it('getAvailableServiceNames should delegate to driver', async function () {
1401
+ const driver = makeDriver({
1402
+ getAvailableServiceNames: _sinon.default.stub().resolves(['x', 'y'])
1403
+ });
1404
+ const {
1405
+ Navy
1406
+ } = loadNavyModule({
1407
+ state: {
1408
+ getState: _sinon.default.stub().resolves({
1409
+ driver: 'docker-compose'
1410
+ })
1411
+ },
1412
+ driver: {
1413
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
1414
+ }
1415
+ });
1416
+ (0, _chai.expect)(await new Navy('env').getAvailableServiceNames()).to.eql(['x', 'y']);
1417
+ });
1418
+ });
1419
+ describe('waitForHealthy', function () {
1420
+ function setupHealth({
1421
+ ps = []
1422
+ } = {}) {
1423
+ const driver = makeDriver({
1424
+ ps: _sinon.default.stub().resolves(ps)
1425
+ });
1426
+ const {
1427
+ Navy
1428
+ } = loadNavyModule({
1429
+ state: {
1430
+ getState: _sinon.default.stub().resolves({
1431
+ driver: 'docker-compose'
1432
+ })
1433
+ },
1434
+ driver: {
1435
+ resolveDriverFromName: _sinon.default.stub().returns(() => driver)
1436
+ }
1437
+ });
1438
+ return new Navy('env');
1439
+ }
1440
+ it('should resolve true when all specified services are healthy', async function () {
1441
+ const navy = setupHealth({
1442
+ ps: [{
1443
+ name: 'api',
1444
+ raw: {
1445
+ State: {
1446
+ Health: {
1447
+ Status: 'healthy'
1448
+ }
1449
+ }
1450
+ }
1451
+ }]
1452
+ });
1453
+ const result = await navy.waitForHealthy(['api'], null, {
1454
+ factor: 1,
1455
+ retries: 1,
1456
+ minTimeout: 1
1457
+ });
1458
+ (0, _chai.expect)(result).to.equal(true);
1459
+ });
1460
+ it('should default services to all containers with health checks when none provided', async function () {
1461
+ const navy = setupHealth({
1462
+ ps: [{
1463
+ name: 'api',
1464
+ raw: {
1465
+ State: {
1466
+ Health: {
1467
+ Status: 'healthy'
1468
+ }
1469
+ }
1470
+ }
1471
+ }, {
1472
+ name: 'noh',
1473
+ raw: {
1474
+ State: {}
1475
+ }
1476
+ }]
1477
+ });
1478
+ const result = await navy.waitForHealthy(undefined, null, {
1479
+ factor: 1,
1480
+ retries: 1,
1481
+ minTimeout: 1
1482
+ });
1483
+ (0, _chai.expect)(result).to.equal(true);
1484
+ });
1485
+ it('should call progressCallback with health status of each service', async function () {
1486
+ const navy = setupHealth({
1487
+ ps: [{
1488
+ name: 'api',
1489
+ raw: {
1490
+ State: {
1491
+ Health: {
1492
+ Status: 'healthy'
1493
+ }
1494
+ }
1495
+ }
1496
+ }]
1497
+ });
1498
+ const cb = _sinon.default.spy();
1499
+ await navy.waitForHealthy(['api'], cb, {
1500
+ factor: 1,
1501
+ retries: 1,
1502
+ minTimeout: 1
1503
+ });
1504
+ (0, _chai.expect)(cb.calledOnce).to.equal(true);
1505
+ (0, _chai.expect)(cb.firstCall.args[0]).to.eql([{
1506
+ service: 'api',
1507
+ health: 'healthy'
1508
+ }]);
1509
+ });
1510
+ it('should throw a NavyError describing services missing health checks', async function () {
1511
+ const navy = setupHealth({
1512
+ ps: [{
1513
+ name: 'api',
1514
+ raw: {
1515
+ State: {}
1516
+ }
1517
+ }]
1518
+ });
1519
+ let caught;
1520
+ try {
1521
+ await navy.waitForHealthy(['api'], null, {
1522
+ factor: 1,
1523
+ retries: 1,
1524
+ minTimeout: 1
1525
+ });
1526
+ } catch (e) {
1527
+ caught = e;
1528
+ }
1529
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
1530
+ (0, _chai.expect)(caught.message).to.contain("don't have health checks");
1531
+ (0, _chai.expect)(caught.message).to.contain('api');
1532
+ });
1533
+
1534
+ // NOTE: Currently `waitForHealthy` calls `retry('Unhealthy services: ...')`
1535
+ // with a string. After retries exhaust, that string is what is thrown, then
1536
+ // `catch (e) { throw new NavyError(e.message) }` reads `.message` on the
1537
+ // string, which is undefined. The NavyError that surfaces therefore has no
1538
+ // message. This test asserts the type only and documents the lost message.
1539
+ it('should throw a NavyError after retries exhausted with unhealthy services', async function () {
1540
+ this.timeout(10000);
1541
+ const navy = setupHealth({
1542
+ ps: [{
1543
+ name: 'api',
1544
+ raw: {
1545
+ State: {
1546
+ Health: {
1547
+ Status: 'starting'
1548
+ }
1549
+ }
1550
+ }
1551
+ }]
1552
+ });
1553
+ let caught;
1554
+ try {
1555
+ await navy.waitForHealthy(['api'], null, {
1556
+ factor: 1,
1557
+ retries: 2,
1558
+ minTimeout: 1
1559
+ });
1560
+ } catch (e) {
1561
+ caught = e;
1562
+ }
1563
+ (0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
1564
+ });
1565
+ });
1566
+ describe('getLaunchedNavies / getLaunchedNavyNames', function () {
1567
+ it('getLaunchedNavyNames should return readdir entries', async function () {
1568
+ const {
1569
+ getLaunchedNavyNames
1570
+ } = loadNavyModule({
1571
+ fs: {
1572
+ readdirAsync: _sinon.default.stub().resolves(['env1', 'env2'])
1573
+ }
1574
+ });
1575
+ (0, _chai.expect)(await getLaunchedNavyNames()).to.eql(['env1', 'env2']);
1576
+ });
1577
+ it('getLaunchedNavyNames should return [] when readdir fails', async function () {
1578
+ const {
1579
+ getLaunchedNavyNames
1580
+ } = loadNavyModule({
1581
+ fs: {
1582
+ readdirAsync: _sinon.default.stub().rejects(new Error('ENOENT'))
1583
+ }
1584
+ });
1585
+ (0, _chai.expect)(await getLaunchedNavyNames()).to.eql([]);
1586
+ });
1587
+ it('getLaunchedNavies should return Navy instances for each navy directory', async function () {
1588
+ const {
1589
+ getLaunchedNavies
1590
+ } = loadNavyModule({
1591
+ fs: {
1592
+ readdirAsync: _sinon.default.stub().resolves(['env1', 'env2', 'file.txt']),
1593
+ lstatSync: _sinon.default.stub().callsFake(p => ({
1594
+ isDirectory: () => !p.endsWith('file.txt')
1595
+ }))
1596
+ }
1597
+ });
1598
+ const navies = await getLaunchedNavies();
1599
+ (0, _chai.expect)(navies.map(n => n.name)).to.eql(['env1', 'env2']);
1600
+ });
1601
+ it('getLaunchedNavies should return [] when readdir fails', async function () {
1602
+ const {
1603
+ getLaunchedNavies
1604
+ } = loadNavyModule({
1605
+ fs: {
1606
+ readdirAsync: _sinon.default.stub().rejects(new Error('ENOENT'))
1607
+ }
1608
+ });
1609
+ (0, _chai.expect)(await getLaunchedNavies()).to.eql([]);
1610
+ });
1611
+ });
1612
+ });