Skip to content

Commit e9e640f

Browse files
committed
feat(pre-checks): add ~/.config folder ownership to pre-run checks
closes #675 - convert update check to a listr task set - add ~/.config directory check to list of prechecks
1 parent 7cfbfb7 commit e9e640f

10 files changed

+214
-115
lines changed

lib/command.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ class Command {
172172

173173
let precheck = Promise.resolve();
174174

175-
if (this.checkVersion) {
176-
const updateCheck = require('./utils/update-check');
177-
precheck = updateCheck(ui);
175+
if (this.runPreChecks) {
176+
const preChecks = require('./utils/pre-checks');
177+
precheck = preChecks(ui, system);
178178
}
179179

180180
return precheck.then(() => {

lib/commands/install.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ InstallCommand.options = {
139139
default: false
140140
}
141141
};
142-
InstallCommand.checkVersion = true;
142+
InstallCommand.runPreChecks = true;
143143
InstallCommand.ensureDir = true;
144144

145145
module.exports = InstallCommand;

lib/commands/migrate.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ class MigrateCommand extends Command {
3434
}
3535

3636
MigrateCommand.description = 'Run system migrations on a Ghost instance';
37-
MigrateCommand.checkVersion = true;
37+
MigrateCommand.runPreChecks = true;
3838

3939
module.exports = MigrateCommand;

lib/commands/setup.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,6 @@ SetupCommand.options = Object.assign({
215215
type: 'string'
216216
}
217217
}, options);
218-
SetupCommand.checkVersion = true;
218+
SetupCommand.runPreChecks = true;
219219

220220
module.exports = SetupCommand;

lib/commands/update.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,6 @@ UpdateCommand.options = {
287287
default: false
288288
}
289289
};
290-
UpdateCommand.checkVersion = true;
290+
UpdateCommand.runPreChecks = true;
291291

292292
module.exports = UpdateCommand;

lib/utils/pre-checks.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
const fs = require('fs-extra');
3+
const os = require('os');
4+
const path = require('path');
5+
const semver = require('semver');
6+
const latestVersion = require('latest-version');
7+
const pkg = require('../../package.json');
8+
9+
/**
10+
* Checks if a version update is available
11+
* @param {UI} ui ui instance
12+
* @param {System} system System instance
13+
*/
14+
module.exports = function preChecks(ui, system) {
15+
const tasks = [{
16+
title: 'Checking for Ghost-CLI updates',
17+
task: () => latestVersion(pkg.name).then((latest) => {
18+
if (semver.lt(pkg.version, latest)) {
19+
const chalk = require('chalk');
20+
21+
ui.log(
22+
'You are running an outdated version of Ghost-CLI.\n' +
23+
'It is recommended that you upgrade before continuing.\n' +
24+
`Run ${chalk.cyan('`npm install -g ghost-cli@latest`')} to upgrade.\n`,
25+
'yellow'
26+
);
27+
}
28+
})
29+
}, {
30+
title: 'Ensuring correct ~/.config folder ownership',
31+
task: () => {
32+
const configstore = path.join(os.homedir(), '.config');
33+
34+
return fs.lstat(configstore).then((stats) => {
35+
if (stats.uid === process.getuid() && stats.gid === process.getgid()) {
36+
return;
37+
}
38+
39+
const {USER} = process.env;
40+
41+
return ui.sudo(`chown -R ${USER}:${USER} ${configstore}`);
42+
});
43+
},
44+
isEnabled: () => system.platform.linux
45+
}];
46+
47+
return ui.listr(tasks, {}, {clearOnSuccess: true});
48+
};

lib/utils/update-check.js

-27
This file was deleted.

test/unit/command-spec.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -361,17 +361,17 @@ describe('Unit: Command', function () {
361361
const uiStub = sinon.stub().returns({ui: true});
362362
const setEnvironmentStub = sinon.stub();
363363
const systemStub = sinon.stub().returns({setEnvironment: setEnvironmentStub});
364-
const updateCheckStub = sinon.stub().resolves();
364+
const preChecksStub = sinon.stub().resolves();
365365

366366
const Command = proxyquire(modulePath, {
367367
'./ui': uiStub,
368368
'./system': systemStub,
369-
'./utils/update-check': updateCheckStub
369+
'./utils/pre-checks': preChecksStub
370370
});
371371

372372
class TestCommand extends Command {}
373373
TestCommand.global = true;
374-
TestCommand.checkVersion = true;
374+
TestCommand.runPreChecks = true;
375375

376376
const runStub = sinon.stub(TestCommand.prototype, 'run');
377377
const oldEnv = process.env.NODE_ENV;
@@ -393,8 +393,8 @@ describe('Unit: Command', function () {
393393
expect(setEnvironmentStub.calledWithExactly(true, true)).to.be.true;
394394
expect(systemStub.calledOnce).to.be.true;
395395
expect(systemStub.calledWithExactly({ui: true}, [{extensiona: true}])).to.be.true;
396-
expect(updateCheckStub.calledOnce).to.be.true;
397-
expect(updateCheckStub.calledWithExactly({ui: true})).to.be.true;
396+
expect(preChecksStub.calledOnce).to.be.true;
397+
expect(preChecksStub.calledWithExactly({ui: true}, {setEnvironment: setEnvironmentStub})).to.be.true;
398398
expect(runStub.calledOnce).to.be.true;
399399
expect(runStub.calledWithExactly({verbose: false, prompt: false, development: false, auto: false})).to.be.true;
400400

test/unit/utils/pre-checks-spec.js

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
'use strict';
2+
const {expect} = require('chai');
3+
const proxyquire = require('proxyquire').noCallThru();
4+
const sinon = require('sinon');
5+
const stripAnsi = require('strip-ansi');
6+
const os = require('os');
7+
const fs = require('fs-extra');
8+
9+
function fake(stubs = {}) {
10+
return proxyquire('../../../lib/utils/pre-checks', stubs);
11+
}
12+
13+
function getTasks(stubs = {}, ui = {}, system = {}) {
14+
const preChecks = fake(stubs);
15+
const listr = sinon.stub().resolves();
16+
17+
return preChecks(Object.assign({listr}, ui), system).then(() => {
18+
expect(listr.calledOnce).to.be.true;
19+
return listr.args[0][0];
20+
});
21+
}
22+
23+
describe('Unit: Utils > pre-checks', function () {
24+
describe('update check', function () {
25+
it('rejects error if latestVersion has an error', function (done) {
26+
const pkg = {name: 'ghost', version: '1.0.0'};
27+
const testError = new Error('update check');
28+
const latestVersion = sinon.stub().rejects(testError);
29+
30+
getTasks({
31+
'../../package.json': pkg,
32+
'latest-version': latestVersion
33+
}).then(([task]) => {
34+
expect(task.title).to.equal('Checking for Ghost-CLI updates');
35+
36+
return task.task();
37+
}).catch((err) => {
38+
expect(err.message).to.equal(testError.message);
39+
expect(latestVersion.calledOnce).to.be.true;
40+
expect(latestVersion.calledWithExactly('ghost')).to.be.true;
41+
done();
42+
});
43+
});
44+
45+
it('doesn\'t do anything if there are no updates', function () {
46+
const pkg = {name: 'ghost', version: '1.0.0'};
47+
const latestVersion = sinon.stub().resolves('1.0.0');
48+
const log = sinon.stub();
49+
50+
return getTasks({
51+
'../../package.json': pkg,
52+
'latest-version': latestVersion
53+
}, {log}).then(([task]) => task.task()).then(() => {
54+
expect(log.called).to.be.false;
55+
expect(latestVersion.calledOnce).to.be.true;
56+
expect(latestVersion.calledWithExactly('ghost')).to.be.true;
57+
});
58+
});
59+
60+
it('logs a message if an update is available', function () {
61+
const pkg = {name: 'ghost', version: '1.0.0'};
62+
const latestVersion = sinon.stub().resolves('1.1.0');
63+
const log = sinon.stub();
64+
65+
return getTasks({
66+
'../../package.json': pkg,
67+
'latest-version': latestVersion
68+
}, {log}).then(([task]) => task.task()).then(() => {
69+
expect(log.calledOnce).to.be.true;
70+
const msg = log.args[0][0];
71+
72+
expect(stripAnsi(msg)).to.match(/You are running an outdated version of Ghost-CLI/);
73+
74+
expect(latestVersion.calledOnce).to.be.true;
75+
expect(latestVersion.calledWithExactly('ghost')).to.be.true;
76+
});
77+
});
78+
});
79+
80+
describe('~/.config folder ownership', function () {
81+
afterEach(() => {
82+
sinon.restore();
83+
delete process.env.USER;
84+
});
85+
86+
it('rejects error if fs.lstat errors', function (done) {
87+
const homedir = sinon.stub(os, 'homedir').returns('/home/ghost');
88+
const lstat = sinon.stub(fs, 'lstat').rejects(new Error('test error'));
89+
90+
getTasks({}, {}, {platform: {linux: true}}).then(([,task]) => {
91+
expect(task.title).to.equal('Ensuring correct ~/.config folder ownership');
92+
expect(task.isEnabled()).to.be.true;
93+
return task.task();
94+
}).catch((error) => {
95+
expect(error.message).to.equal('test error');
96+
expect(homedir.calledOnce).to.be.true;
97+
expect(lstat.calledOnce).to.be.true;
98+
expect(lstat.calledWithExactly('/home/ghost/.config')).to.be.true;
99+
done();
100+
});
101+
});
102+
103+
it('doesn\'t do anything if directory ownership if fine', function () {
104+
sinon.stub(os, 'homedir').returns('/home/ghost');
105+
sinon.stub(fs, 'lstat').resolves({uid: 1, gid: 1});
106+
const uid = sinon.stub(process, 'getuid').returns(1);
107+
const gid = sinon.stub(process, 'getgid').returns(1);
108+
const sudo = sinon.stub().resolves();
109+
110+
return getTasks({}, {sudo}, {platform: {linux: false}}).then(([,task]) => {
111+
expect(task.isEnabled()).to.be.false;
112+
return task.task();
113+
}).then(() => {
114+
expect(uid.calledOnce).to.be.true;
115+
expect(gid.calledOnce).to.be.true;
116+
expect(sudo.called).to.be.false;
117+
});
118+
});
119+
120+
it('calls chown if directory owner is not correct', function () {
121+
sinon.stub(os, 'homedir').returns('/home/ghost');
122+
sinon.stub(fs, 'lstat').resolves({uid: 1, gid: 1});
123+
const uid = sinon.stub(process, 'getuid').returns(2);
124+
const gid = sinon.stub(process, 'getgid').returns(2);
125+
const sudo = sinon.stub().resolves();
126+
process.env.USER = 'ghostuser';
127+
128+
return getTasks({}, {sudo}).then(([,task]) => task.task()).then(() => {
129+
expect(uid.calledOnce).to.be.true;
130+
expect(gid.called).to.be.false;
131+
expect(sudo.calledOnce).to.be.true;
132+
133+
expect(sudo.args[0][0]).to.equal('chown -R ghostuser:ghostuser /home/ghost/.config');
134+
});
135+
});
136+
137+
it('calls chown if directory group is not correct', function () {
138+
sinon.stub(os, 'homedir').returns('/home/ghost');
139+
sinon.stub(fs, 'lstat').resolves({uid: 2, gid: 1});
140+
const uid = sinon.stub(process, 'getuid').returns(2);
141+
const gid = sinon.stub(process, 'getgid').returns(2);
142+
const sudo = sinon.stub().resolves();
143+
process.env.USER = 'ghostuser';
144+
145+
return getTasks({}, {sudo}).then(([,task]) => task.task()).then(() => {
146+
expect(uid.calledOnce).to.be.true;
147+
expect(gid.calledOnce).to.be.true;
148+
expect(sudo.calledOnce).to.be.true;
149+
150+
expect(sudo.args[0][0]).to.equal('chown -R ghostuser:ghostuser /home/ghost/.config');
151+
});
152+
});
153+
});
154+
});

test/unit/utils/update-check-spec.js

-76
This file was deleted.

0 commit comments

Comments
 (0)