Skip to content

Commit 9cb3138

Browse files
committed
feat(setup): refactor setup command
closes #594, #588 - cleanup setup command code & improve testability - rework how extensions register setup steps - make it so an errored setup step doesn't break the entire flow
1 parent 13a6160 commit 9cb3138

File tree

9 files changed

+991
-1263
lines changed

9 files changed

+991
-1263
lines changed

extensions/mysql/index.js

+21-23
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,29 @@
33
const Promise = require('bluebird');
44
const mysql = require('mysql');
55
const omit = require('lodash/omit');
6-
const cli = require('../../lib');
76
const generator = require('generate-password');
7+
const {Extension, errors} = require('../../lib');
88

99
const localhostAliases = ['localhost', '127.0.0.1'];
10-
11-
class MySQLExtension extends cli.Extension {
12-
setup(cmd, argv) {
13-
// Case 1: ghost install local OR ghost setup --local
14-
// Case 2: ghost install --db sqlite3
15-
// Skip in both cases
16-
if (argv.local || argv.db === 'sqlite3') {
17-
return;
18-
}
19-
20-
cmd.addStage('mysql', this.setupMySQL.bind(this), [], '"ghost" mysql user');
10+
const {ConfigError, CliError} = errors;
11+
12+
class MySQLExtension extends Extension {
13+
setup() {
14+
return [{
15+
id: 'mysql',
16+
name: '"ghost" mysql user',
17+
task: (...args) => this.setupMySQL(...args),
18+
// Case 1: ghost install local OR ghost setup --local
19+
// Case 2: ghost install --db sqlite3
20+
// Skip in both cases
21+
enabled: ({argv}) => !(argv.local || argv.db === 'sqlite3'),
22+
skip: ({instance}) => instance.config.get('database.connection.user') !== 'root'
23+
}];
2124
}
2225

23-
setupMySQL(argv, ctx, task) {
26+
setupMySQL(ctx) {
2427
const dbconfig = ctx.instance.config.get('database.connection');
2528

26-
if (dbconfig.user !== 'root') {
27-
this.ui.log('MySQL user is not "root", skipping additional user setup', 'yellow');
28-
return task.skip();
29-
}
30-
3129
return this.ui.listr([{
3230
title: 'Connecting to database',
3331
task: () => this.canConnect(ctx, dbconfig)
@@ -53,7 +51,7 @@ class MySQLExtension extends cli.Extension {
5351

5452
return Promise.fromCallback(cb => this.connection.connect(cb)).catch((error) => {
5553
if (error.code === 'ECONNREFUSED') {
56-
return Promise.reject(new cli.errors.ConfigError({
54+
return Promise.reject(new ConfigError({
5755
message: error.message,
5856
config: {
5957
'database.connection.host': dbconfig.host,
@@ -63,7 +61,7 @@ class MySQLExtension extends cli.Extension {
6361
help: 'Ensure that MySQL is installed and reachable. You can always re-run `ghost setup` to try again.'
6462
}));
6563
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
66-
return Promise.reject(new cli.errors.ConfigError({
64+
return Promise.reject(new ConfigError({
6765
message: error.message,
6866
config: {
6967
'database.connection.user': dbconfig.user,
@@ -74,7 +72,7 @@ class MySQLExtension extends cli.Extension {
7472
}));
7573
}
7674

77-
return Promise.reject(new cli.errors.CliError({
75+
return Promise.reject(new CliError({
7876
message: 'Error trying to connect to the MySQL database.',
7977
help: 'You can run `ghost config` to re-enter the correct credentials. Alternatively you can run `ghost setup` again.',
8078
err: error
@@ -168,11 +166,11 @@ class MySQLExtension extends cli.Extension {
168166
this.ui.logVerbose(`MySQL: running query > ${queryString}`, 'gray');
169167
return Promise.fromCallback(cb => this.connection.query(queryString, cb))
170168
.catch((error) => {
171-
if (error instanceof (cli.errors.CliError)) {
169+
if (error instanceof CliError) {
172170
return Promise.reject(error);
173171
}
174172

175-
return Promise.reject(new cli.errors.CliError({
173+
return Promise.reject(new CliError({
176174
message: error.message,
177175
context: queryString,
178176
err: error

extensions/mysql/test/extension-spec.js

+82-85
Original file line numberDiff line numberDiff line change
@@ -2,100 +2,97 @@
22
const expect = require('chai').expect;
33
const sinon = require('sinon');
44
const proxyquire = require('proxyquire');
5+
const configStub = require('../../../test/utils/config-stub');
56

67
const modulePath = '../index';
78
const errors = require('../../../lib/errors');
89

910
describe('Unit: Mysql extension', function () {
10-
describe('setup hook', function () {
11+
it('setup hook works', function () {
1112
const MysqlExtension = require(modulePath);
12-
13-
it('does not add stage if --local is true', function () {
14-
const instance = new MysqlExtension({}, {}, {}, '/some/dir');
15-
const addStageStub = sinon.stub();
16-
17-
instance.setup({addStage: addStageStub}, {local: true});
18-
expect(addStageStub.called).to.be.false;
19-
});
20-
21-
it('does not add stage if db is sqlite3', function () {
22-
const instance = new MysqlExtension({}, {}, {}, '/some/dir');
23-
const addStageStub = sinon.stub();
24-
25-
instance.setup({addStage: addStageStub}, {local: false, db: 'sqlite3'});
26-
expect(addStageStub.called).to.be.false;
27-
});
28-
29-
it('adds stage if not local and db is not sqlite3', function () {
30-
const instance = new MysqlExtension({}, {}, {}, '/some/dir');
31-
const addStageStub = sinon.stub();
32-
33-
instance.setup({addStage: addStageStub}, {local: false, db: 'mysql'});
34-
expect(addStageStub.calledOnce).to.be.true;
35-
expect(addStageStub.calledWith('mysql')).to.be.true;
36-
});
13+
const inst = new MysqlExtension({}, {}, {}, '/some/dir');
14+
const result = inst.setup();
15+
16+
expect(result).to.have.length(1);
17+
const [task] = result;
18+
19+
// Check static properties
20+
expect(task.id).to.equal('mysql');
21+
expect(task.name).to.equal('"ghost" mysql user');
22+
23+
// Check task functions
24+
expect(task.task).to.be.a('function');
25+
expect(task.enabled).to.be.a('function');
26+
expect(task.skip).to.be.a('function');
27+
28+
// Check task.task
29+
const stub = sinon.stub(inst, 'setupMySQL');
30+
task.task('a', 'set', 'of', 'args', true);
31+
expect(stub.calledOnce).to.be.true;
32+
expect(stub.calledWithExactly('a', 'set', 'of', 'args', true)).to.be.true;
33+
34+
// Check task.enabled
35+
expect(task.enabled({argv: {local: true}})).to.be.false;
36+
expect(task.enabled({argv: {local: false, db: 'sqlite3'}})).to.be.false;
37+
expect(task.enabled({argv: {local: false}})).to.be.true;
38+
39+
// Check task.skip
40+
const get = sinon.stub();
41+
get.onFirstCall().returns('not-root');
42+
get.onSecondCall().returns('root');
43+
44+
expect(task.skip({instance: {config: {get}}})).to.be.true;
45+
expect(get.calledOnce).to.be.true;
46+
expect(task.skip({instance: {config: {get}}})).to.be.false;
47+
expect(get.calledTwice).to.be.true;
3748
});
3849

39-
describe('setupMySQL', function () {
50+
it('setupMySQL works', function () {
4051
const MysqlExtension = require(modulePath);
41-
42-
it('skips if db user is not root', function () {
43-
const logStub = sinon.stub();
44-
const instance = new MysqlExtension({log: logStub}, {}, {}, '/some/dir');
45-
const getStub = sinon.stub().returns({user: 'notroot'});
46-
const skipStub = sinon.stub().resolves();
47-
48-
return instance.setupMySQL({}, {instance: {config: {get: getStub}}}, {skip: skipStub}).then(() => {
49-
expect(getStub.calledOnce).to.be.true;
50-
expect(getStub.calledWithExactly('database.connection')).to.be.true;
51-
expect(logStub.calledOnce).to.be.true;
52-
expect(logStub.args[0][0]).to.match(/user is not "root"/);
53-
expect(skipStub.calledOnce).to.be.true;
54-
});
55-
});
56-
57-
it('returns tasks that call the helpers and cleanup', function () {
58-
const listrStub = sinon.stub().callsFake(function (tasks) {
59-
expect(tasks).to.be.an.instanceof(Array);
60-
expect(tasks).to.have.length(4);
61-
62-
// Run each task step
63-
tasks.forEach(task => task.task());
64-
return Promise.resolve();
65-
});
66-
const instance = new MysqlExtension({listr: listrStub}, {}, {}, '/some/dir');
67-
const getStub = sinon.stub().returns({user: 'root'});
68-
const saveStub = sinon.stub();
69-
const setStub = sinon.stub();
70-
setStub.returns({set: setStub, save: saveStub});
71-
const endStub = sinon.stub();
72-
const skipStub = sinon.stub().resolves();
73-
const canConnectStub = sinon.stub(instance, 'canConnect');
74-
const createUserStub = sinon.stub(instance, 'createUser').callsFake((ctx) => {
75-
ctx.mysql = {username: 'testuser', password: 'testpass'};
76-
});
77-
const grantPermissionsStub = sinon.stub(instance, 'grantPermissions');
78-
79-
instance.connection = {end: endStub};
80-
81-
return instance.setupMySQL({}, {
82-
instance: {config: {get: getStub, set: setStub, save: saveStub}}
83-
}, {skip: skipStub}).then(() => {
84-
expect(skipStub.called).to.be.false;
85-
expect(listrStub.calledOnce).to.be.true;
86-
expect(listrStub.args[0][1]).to.be.false;
87-
88-
expect(getStub.calledOnce).to.be.true;
89-
expect(canConnectStub.calledOnce).to.be.true;
90-
expect(createUserStub.calledOnce).to.be.true;
91-
expect(grantPermissionsStub.calledOnce).to.be.true;
92-
expect(setStub.calledTwice).to.be.true;
93-
expect(setStub.calledWithExactly('database.connection.user', 'testuser')).to.be.true;
94-
expect(setStub.calledWithExactly('database.connection.password', 'testpass')).to.be.true;
95-
expect(saveStub.calledOnce).to.be.true;
96-
expect(endStub.calledOnce).to.be.true;
97-
});
98-
});
52+
const listr = sinon.stub();
53+
const config = configStub();
54+
const dbConfig = {host: 'localhost', user: 'root', password: 'password'};
55+
config.get.returns(dbConfig);
56+
57+
const inst = new MysqlExtension({listr}, {}, {}, '/some/dir');
58+
const context = {instance: {config}};
59+
inst.setupMySQL(context);
60+
61+
expect(config.get.calledOnce).to.be.true;
62+
expect(config.get.calledWithExactly('database.connection')).to.be.true;
63+
expect(listr.calledOnce).to.be.true;
64+
const [tasks, ctx] = listr.args[0];
65+
expect(tasks).to.have.length(4);
66+
expect(ctx).to.be.false;
67+
68+
const canConnect = sinon.stub(inst, 'canConnect');
69+
expect(tasks[0].title).to.equal('Connecting to database');
70+
tasks[0].task();
71+
expect(canConnect.calledOnce).to.be.true;
72+
expect(canConnect.calledWithExactly(context, dbConfig)).to.be.true;
73+
74+
const createUser = sinon.stub(inst, 'createUser');
75+
expect(tasks[1].title).to.equal('Creating new MySQL user');
76+
tasks[1].task();
77+
expect(createUser.calledOnce).to.be.true;
78+
expect(createUser.calledWithExactly(context, dbConfig)).to.be.true;
79+
80+
const grantPermissions = sinon.stub(inst, 'grantPermissions');
81+
expect(tasks[2].title).to.equal('Granting new user permissions');
82+
tasks[2].task();
83+
expect(grantPermissions.calledOnce).to.be.true;
84+
expect(grantPermissions.calledWithExactly(context, dbConfig)).to.be.true;
85+
86+
const end = sinon.stub();
87+
inst.connection = {end};
88+
context.mysql = {username: 'new', password: 'new'};
89+
expect(tasks[3].title).to.equal('Saving new config');
90+
tasks[3].task();
91+
expect(config.set.calledTwice).to.be.true;
92+
expect(config.set.calledWithExactly('database.connection.user', 'new')).to.be.true;
93+
expect(config.set.calledWithExactly('database.connection.password', 'new')).to.be.true;
94+
expect(config.save.calledOnce).to.be.true;
95+
expect(end.calledOnce).to.be.true;
9996
});
10097

10198
describe('canConnect', function () {

0 commit comments

Comments
 (0)