navy
Version:
Quick and powerful development environments using Docker and Docker Compose
243 lines (240 loc) • 9 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _chai = require("chai");
var _sinon = _interopRequireDefault(require("sinon"));
var _proxyquire = _interopRequireDefault(require("proxyquire"));
var _readline = _interopRequireDefault(require("readline"));
var _cliSpinners = require("cli-spinners");
var _stripAnsi = _interopRequireDefault(require("strip-ansi"));
/* eslint-env mocha */
// NOTE (potential source bug in updates.js, not locked in by these tests):
// The Promise.all body checks `if (!service || !service.raw || !service.raw.Image)`
// then immediately runs `updateStatus[service.id] = 'NO_IMAGE'`. If `service` were
// falsy the guard would let us through, but the very next assignment dereferences
// `service.id` and would throw. The `!service` part of the guard is therefore
// either dead defensive code or a latent bug.
describe('cli/updates', function () {
let sandbox;
let clock;
let getNavyStub;
let navyStub;
let hasUpdateStub;
let consoleLogStub;
let clearLineStub;
let cursorToStub;
let moveCursorStub;
let updatesCli;
function makeService(overrides = {}) {
return {
id: 'svc-1',
name: 'api',
image: 'navy/api:latest',
raw: {
Image: 'sha256:abcdef'
},
...overrides
};
}
beforeEach(function () {
sandbox = _sinon.default.createSandbox();
clock = sandbox.useFakeTimers();
navyStub = {
getNavyFile: sandbox.stub().resolves({}),
ps: sandbox.stub().resolves([])
};
getNavyStub = sandbox.stub().returns(navyStub);
hasUpdateStub = sandbox.stub().resolves(false);
consoleLogStub = sandbox.stub(console, 'log');
clearLineStub = sandbox.stub(_readline.default, 'clearLine');
cursorToStub = sandbox.stub(_readline.default, 'cursorTo');
moveCursorStub = sandbox.stub(_readline.default, 'moveCursor');
updatesCli = _proxyquire.default.noCallThru()('../updates', {
'../': {
getNavy: getNavyStub
},
'../util/has-update': hasUpdateStub
});
});
afterEach(function () {
sandbox.restore();
});
function combinedLog() {
return consoleLogStub.getCalls().map(c => (0, _stripAnsi.default)(String(c.args[0] || ''))).join('\n');
}
describe('default export', function () {
it('should resolve the navy instance using opts.navy', async function () {
await updatesCli({
navy: 'env-1'
});
(0, _chai.expect)(getNavyStub.calledOnce).to.equal(true);
(0, _chai.expect)(getNavyStub.firstCall.args[0]).to.equal('env-1');
});
it('should fetch the navy file and the running services list', async function () {
await updatesCli({
navy: 'env-1'
});
(0, _chai.expect)(navyStub.getNavyFile.calledOnce).to.equal(true);
(0, _chai.expect)(navyStub.ps.calledOnce).to.equal(true);
});
it('should print the table headers in the initial draw', async function () {
navyStub.ps.resolves([]);
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('NAME');
(0, _chai.expect)(text).to.contain('IMAGE');
(0, _chai.expect)(text).to.contain('UPDATES');
});
it('should clear the spinner interval before resolving', async function () {
navyStub.ps.resolves([makeService()]);
await updatesCli({
navy: 'env-1'
});
(0, _chai.expect)(clock.countTimers()).to.equal(0);
});
it('should call readline cursor/clear helpers when redrawing', async function () {
navyStub.ps.resolves([makeService()]);
await updatesCli({
navy: 'env-1'
});
(0, _chai.expect)(clearLineStub.called).to.equal(true);
(0, _chai.expect)(cursorToStub.called).to.equal(true);
(0, _chai.expect)(moveCursorStub.called).to.equal(true);
});
});
describe('renderStatus (via default export, final draw)', function () {
it('should mark a service as NO_IMAGE when raw is missing', async function () {
navyStub.ps.resolves([makeService({
raw: undefined
})]);
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('No image for service');
(0, _chai.expect)(hasUpdateStub.called).to.equal(false);
});
it('should mark a service as NO_IMAGE when raw.Image is missing', async function () {
navyStub.ps.resolves([makeService({
raw: {}
})]);
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('No image for service');
(0, _chai.expect)(hasUpdateStub.called).to.equal(false);
});
it('should mark a service as Update available when hasUpdate returns true', async function () {
navyStub.ps.resolves([makeService()]);
hasUpdateStub.resolves(true);
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('Update available');
});
it('should mark a service as Up to date when hasUpdate returns false', async function () {
navyStub.ps.resolves([makeService()]);
hasUpdateStub.resolves(false);
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('Up to date');
});
it('should mark a service as Not found when hasUpdate returns UNKNOWN_REMOTE', async function () {
navyStub.ps.resolves([makeService()]);
hasUpdateStub.resolves('UNKNOWN_REMOTE');
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('Not found');
});
it('should mark a service as Up to date when hasUpdate returns an unrecognised string', async function () {
navyStub.ps.resolves([makeService()]);
hasUpdateStub.resolves('SOME_OTHER_VALUE');
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('Up to date');
});
it('should mark a service as Internal error when hasUpdate throws', async function () {
navyStub.ps.resolves([makeService()]);
hasUpdateStub.rejects(new Error('boom'));
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('Internal error');
});
it('should mark a service as Internal error when hasUpdate throws an error with no stack', async function () {
navyStub.ps.resolves([makeService()]);
const errNoStack = {
message: 'no stack'
};
hasUpdateStub.callsFake(() => Promise.reject(errNoStack));
await updatesCli({
navy: 'env-1'
});
const text = combinedLog();
(0, _chai.expect)(text).to.contain('Internal error');
});
it('should pass image, raw image id, and navy file through to hasUpdate', async function () {
const navyFile = {
ignoreUnauthorizedRequestsForRegistries: ['r']
};
navyStub.getNavyFile.resolves(navyFile);
navyStub.ps.resolves([makeService({
image: 'navy/api:latest',
raw: {
Image: 'sha256:abc'
}
})]);
await updatesCli({
navy: 'env-1'
});
(0, _chai.expect)(hasUpdateStub.calledOnce).to.equal(true);
(0, _chai.expect)(hasUpdateStub.firstCall.args).to.eql(['navy/api:latest', 'sha256:abc', navyFile]);
});
});
describe('renderStatus null branch (initial Checking... state)', function () {
it('should print Checking... in the very first draw before any update results', async function () {
let resolveHasUpdate;
hasUpdateStub.callsFake(() => new Promise(resolve => {
resolveHasUpdate = resolve;
}));
navyStub.ps.resolves([makeService()]);
const cliPromise = updatesCli({
navy: 'env-1'
});
await clock.tickAsync(0);
const before = combinedLog();
(0, _chai.expect)(before).to.contain('Checking...');
resolveHasUpdate(false);
await cliPromise;
});
});
describe('spinner interval', function () {
it('should advance the spinner frame on each interval tick and wrap back to 0 after frames.length ticks', async function () {
let resolveHasUpdate;
hasUpdateStub.callsFake(() => new Promise(resolve => {
resolveHasUpdate = resolve;
}));
navyStub.ps.resolves([makeService()]);
const cliPromise = updatesCli({
navy: 'env-1'
});
await clock.tickAsync(0);
const drawsBefore = consoleLogStub.callCount;
await clock.tickAsync(_cliSpinners.dots.interval * (_cliSpinners.dots.frames.length + 1));
(0, _chai.expect)(consoleLogStub.callCount).to.be.greaterThan(drawsBefore);
resolveHasUpdate(false);
await cliPromise;
(0, _chai.expect)(clock.countTimers()).to.equal(0);
});
});
});