'use strict';
const expect = require('chai').expect;
const sinon = require('sinon');
const proxyquire = require('proxyquire');
const configStub = require('../../../test/utils/config-stub');
const semver = require('semver');

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

describe('Unit: Mysql extension', function () {
    it('setup hook works', function () {
        const MysqlExtension = require(modulePath);
        const inst = new MysqlExtension({}, {}, {}, '/some/dir');
        const result = inst.setup();

        expect(result).to.have.length(1);
        const [task] = result;

        // Check static properties
        expect(task.id).to.equal('mysql');
        expect(task.name).to.equal('"ghost" mysql user');

        // Check task functions
        expect(task.task).to.be.a('function');
        expect(task.enabled).to.be.a('function');
        expect(task.skip).to.be.a('function');

        // Check task.task
        const stub = sinon.stub(inst, 'setupMySQL');
        task.task('a', 'set', 'of', 'args', true);
        expect(stub.calledOnce).to.be.true;
        expect(stub.calledWithExactly('a', 'set', 'of', 'args', true)).to.be.true;

        // Check task.enabled
        expect(task.enabled({argv: {local: true}})).to.be.false;
        expect(task.enabled({argv: {local: false, db: 'sqlite3'}})).to.be.false;
        expect(task.enabled({argv: {local: false}})).to.be.true;

        // Check task.skip
        const get = sinon.stub();
        get.onFirstCall().returns('not-root');
        get.onSecondCall().returns('root');

        expect(task.skip({instance: {config: {get}}})).to.be.true;
        expect(get.calledOnce).to.be.true;
        expect(task.skip({instance: {config: {get}}})).to.be.false;
        expect(get.calledTwice).to.be.true;
    });

    it('setupMySQL works', function () {
        const MysqlExtension = require(modulePath);
        const listr = sinon.stub();
        const config = configStub();
        const dbConfig = {host: 'localhost', user: 'root', password: 'password'};
        config.get.returns(dbConfig);

        const inst = new MysqlExtension({listr}, {}, {}, '/some/dir');
        const context = {instance: {config}};
        inst.setupMySQL(context);

        expect(config.get.calledOnceWithExactly('database.connection')).to.be.true;
        expect(listr.calledOnce).to.be.true;
        const [tasks, ctx] = listr.args[0];
        expect(tasks).to.have.length(5);
        expect(ctx).to.be.false;

        const canConnect = sinon.stub(inst, 'canConnect');
        expect(tasks[0].title).to.equal('Connecting to database');
        tasks[0].task();
        expect(canConnect.calledOnceWithExactly(context, dbConfig)).to.be.true;

        const createUser = sinon.stub(inst, 'createUser');
        expect(tasks[1].title).to.equal('Creating new MySQL user');
        tasks[1].task();
        expect(createUser.calledOnceWithExactly(context, dbConfig)).to.be.true;

        const grantPermissions = sinon.stub(inst, 'grantPermissions');
        expect(tasks[2].title).to.equal('Granting new user permissions');
        tasks[2].task();
        expect(grantPermissions.calledOnceWithExactly(context, dbConfig)).to.be.true;

        const setupDatabase = sinon.stub(inst, 'createMySQL8Database');
        expect(tasks[3].title).to.equal('Setting up database (MySQL 8)');
        tasks[3].task();
        expect(setupDatabase.calledOnceWithExactly(dbConfig)).to.be.true;

        const end = sinon.stub();
        inst.connection = {end};
        context.mysql = {username: 'new', password: 'new'};
        expect(tasks[4].title).to.equal('Saving new config');
        tasks[4].task();
        expect(config.set.calledTwice).to.be.true;
        expect(config.set.calledWithExactly('database.connection.user', 'new')).to.be.true;
        expect(config.set.calledWithExactly('database.connection.password', 'new')).to.be.true;
        expect(config.save.calledOnce).to.be.true;
        expect(end.calledOnce).to.be.true;
    });

    describe('canConnect', function () {
        it('creates connection and connects', async function () {
            const connectStub = sinon.stub().callsArg(0);
            const createConnectionStub = sinon.stub().returns({connect: connectStub});
            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({logVerbose: () => {}}, {}, {}, '/some/dir');
            const getServerVersion = sinon.stub(instance, 'getServerVersion').resolves(null);

            const ctx = {};

            await instance.canConnect(ctx, {user: 'someuser', password: 'somepass', database: 'testing'});
            expect(createConnectionStub.calledOnce).to.be.true;
            expect(createConnectionStub.calledWithExactly({user: 'someuser', password: 'somepass'})).to.be.true;
            expect(connectStub.calledOnce).to.be.true;
            expect(getServerVersion.calledOnce).to.be.true;
            expect(ctx.mysql).to.not.exist;
        });

        it('sets version if version found', async function () {
            const connectStub = sinon.stub().callsArg(0);
            const createConnectionStub = sinon.stub().returns({connect: connectStub});
            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({logVerbose: () => {}}, {}, {}, '/some/dir');

            const version = semver.parse('8.0.0');
            const getServerVersion = sinon.stub(instance, 'getServerVersion').resolves(version);

            const ctx = {};

            await instance.canConnect(ctx, {user: 'someuser', password: 'somepass', database: 'testing'});
            expect(createConnectionStub.calledOnce).to.be.true;
            expect(createConnectionStub.calledWithExactly({user: 'someuser', password: 'somepass'})).to.be.true;
            expect(connectStub.calledOnce).to.be.true;
            expect(getServerVersion.calledOnce).to.be.true;
            expect(ctx.mysql.version).to.exist;
            expect(semver.eq(ctx.mysql.version, version)).to.be.true;
        });

        it('throws error on MySQL 5.6', async function () {
            const connectStub = sinon.stub().callsArg(0);
            const createConnectionStub = sinon.stub().returns({connect: connectStub});
            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({logVerbose: () => {}}, {}, {}, '/some/dir');

            const version = semver.parse('5.6.47');
            const getServerVersion = sinon.stub(instance, 'getServerVersion').resolves(version);

            try {
                await instance.canConnect({}, {user: 'someuser', password: 'somepass', database: 'testing'});
            } catch (error) {
                expect(error).to.be.an.instanceof(errors.SystemError);
                expect(createConnectionStub.calledOnce).to.be.true;
                expect(createConnectionStub.calledWithExactly({user: 'someuser', password: 'somepass'})).to.be.true;
                expect(connectStub.calledOnce).to.be.true;
                expect(getServerVersion.calledOnce).to.be.true;
                return;
            }

            throw new Error('canConnect should have thrown');
        });

        it('throws configerror if error is ECONNREFUSED', function () {
            const connectStub = sinon.stub().callsFake((cb) => {
                const error = new Error('db connection failed');
                error.code = 'ECONNREFUSED';
                cb(error);
            });
            const createConnectionStub = sinon.stub().returns({connect: connectStub});
            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({}, {}, {}, '/some/dir');

            return instance.canConnect({}, {}).then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.ConfigError);
                expect(error.message).to.equal('db connection failed');
                expect(createConnectionStub.calledOnce).to.be.true;
                expect(connectStub.calledOnce).to.be.true;
            });
        });

        it('throws configerror if error is ER_ACCESS_DENIED_ERROR', function () {
            const connectStub = sinon.stub().callsFake((cb) => {
                const error = new Error('invalid username/password');
                error.code = 'ER_ACCESS_DENIED_ERROR';
                cb(error);
            });
            const createConnectionStub = sinon.stub().returns({connect: connectStub});
            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({}, {}, {}, '/some/dir');

            return instance.canConnect({}, {}).then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.ConfigError);
                expect(error.message).to.equal('invalid username/password');
                expect(createConnectionStub.calledOnce).to.be.true;
                expect(connectStub.calledOnce).to.be.true;
            });
        });

        it('throws error if error code does not match expected ones', function () {
            const connectStub = sinon.stub().callsFake((cb) => {
                cb(new Error('ack'));
            });
            const createConnectionStub = sinon.stub().returns({connect: connectStub});
            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({}, {}, {}, '/some/dir');

            return instance.canConnect({}, {}).then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.CliError);
                expect(error.message).to.equal('Error trying to connect to the MySQL database.');
                expect(error.options.help).to.equal('You can run `ghost config` to re-enter the correct credentials. Alternatively you can run `ghost setup` again.');
                expect(error.options.err.message).to.equal('ack');
                expect(createConnectionStub.calledOnce).to.be.true;
                expect(connectStub.calledOnce).to.be.true;
            });
        });
    });

    describe('isDeprecated', function () {
        it('returns that 5.7 is deprecated', async function () {
            const connectStub = sinon.stub().callsArg(0);
            const endStub = sinon.stub();
            const createConnectionStub = sinon.stub().returns({connect: connectStub, end: endStub});

            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({logVerbose: () => {}}, {}, {}, '/some/dir');

            const version = semver.parse('5.7.30');
            const getServerVersion = sinon.stub(instance, 'getServerVersion').resolves(version);

            const isDeprecated = await instance.isDeprecated({user: 'someuser', password: 'somepass', database: 'testing'});

            expect(createConnectionStub.calledOnce).to.be.true;
            expect(createConnectionStub.calledWithExactly({user: 'someuser', password: 'somepass'})).to.be.true;
            expect(connectStub.calledOnce).to.be.true;
            expect(endStub.calledOnce).to.be.true;
            expect(getServerVersion.calledOnce).to.be.true;
            expect(isDeprecated).to.be.true;
        });

        it('returns that 8.0 is not deprecated', async function () {
            const connectStub = sinon.stub().callsArg(0);
            const endStub = sinon.stub();
            const createConnectionStub = sinon.stub().returns({connect: connectStub, end: endStub});

            const MysqlExtension = proxyquire(modulePath, {
                mysql2: {createConnection: createConnectionStub}
            });
            const instance = new MysqlExtension({logVerbose: () => {}}, {}, {}, '/some/dir');

            const version = semver.parse('8.0.0');
            const getServerVersion = sinon.stub(instance, 'getServerVersion').resolves(version);

            const isDeprecated = await instance.isDeprecated({user: 'someuser', password: 'somepass', database: 'testing'});

            expect(createConnectionStub.calledOnce).to.be.true;
            expect(createConnectionStub.calledWithExactly({user: 'someuser', password: 'somepass'})).to.be.true;
            expect(connectStub.calledOnce).to.be.true;
            expect(endStub.calledOnce).to.be.true;
            expect(getServerVersion.calledOnce).to.be.true;
            expect(isDeprecated).to.be.false;
        });
    });

    describe('createUser', function () {
        const MysqlExtension = require(modulePath);

        it('runs correct queries and logs things', async function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query').resolves();
            queryStub.onSecondCall().resolves([{password: '*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19'}]);
            const ctx = {};

            await instance.createUser(ctx, {host: 'localhost'});
            expect(queryStub.calledThrice).to.be.true;
            expect(queryStub.args[0][0]).to.equal('SET old_passwords = 0;');
            expect(queryStub.args[1][0]).to.match(/^SELECT PASSWORD\('[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*'\) AS password;$/);
            expect(queryStub.args[2][0]).to.match(/^CREATE USER 'ghost-[0-9]{1,4}'@'localhost' IDENTIFIED WITH mysql_native_password AS '\*[0-9A-F]*';$/);
            expect(logStub.calledThrice).to.be.true;
            expect(logStub.args[0][0]).to.match(/disabled old_password/);
            expect(logStub.args[1][0]).to.match(/created password hash/);
            expect(logStub.args[2][0]).to.match(/successfully created new user/);
            expect(ctx.mysql).to.exist;
            expect(ctx.mysql.username).to.match(/^ghost-[0-9]{1,4}$/);
            expect(ctx.mysql.password).to.match(/^[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*$/);
        });

        it('uses % for user host if db host is not localhost or 127.0.0.1', async function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query').resolves();
            queryStub.onSecondCall().resolves([{password: '*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19'}]);
            const ctx = {};

            await instance.createUser(ctx, {host: '117.241.162.107'});
            expect(queryStub.calledThrice).to.be.true;
            expect(queryStub.args[0][0]).to.equal('SET old_passwords = 0;');
            expect(queryStub.args[1][0]).to.match(/^SELECT PASSWORD\('[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*'\) AS password;$/);
            expect(queryStub.args[2][0]).to.match(/^CREATE USER 'ghost-[0-9]{1,4}'@'%' IDENTIFIED WITH mysql_native_password AS '\*[0-9A-F]*';$/);
            expect(logStub.calledThrice).to.be.true;
            expect(logStub.args[0][0]).to.match(/disabled old_password/);
            expect(logStub.args[1][0]).to.match(/created password hash/);
            expect(logStub.args[2][0]).to.match(/successfully created new user/);
            expect(ctx.mysql).to.exist;
            expect(ctx.mysql.username).to.match(/^ghost-[0-9]{1,4}$/);
            expect(ctx.mysql.password).to.match(/^[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*$/);
        });

        it('retries creating user if username already exists', async function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query').resolves();
            queryStub.onSecondCall().resolves([{password: '*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19'}]);
            queryStub.onCall(4).resolves([{password: '*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19'}]);
            const err = new Error();
            err.errno = 1396;
            queryStub.onThirdCall().rejects(new errors.CliError({message: 'User exists already', err: err}));
            const ctx = {};

            await instance.createUser(ctx, {host: 'localhost'});
            expect(queryStub.callCount).to.equal(6);
            expect(queryStub.args[0][0]).to.equal('SET old_passwords = 0;');
            expect(queryStub.args[1][0]).to.match(/^SELECT PASSWORD\('[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*'\) AS password;$/);
            expect(queryStub.args[2][0]).to.match(/^CREATE USER 'ghost-[0-9]{1,4}'@'localhost' IDENTIFIED WITH mysql_native_password AS '\*[0-9A-F]*';$/);
            expect(queryStub.args[3][0]).to.equal('SET old_passwords = 0;');
            expect(queryStub.args[4][0]).to.match(/^SELECT PASSWORD\('[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*'\) AS password;$/);
            expect(queryStub.args[5][0]).to.match(/^CREATE USER 'ghost-[0-9]{1,4}'@'localhost' IDENTIFIED WITH mysql_native_password AS '\*[0-9A-F]*';$/);
            expect(logStub.callCount).to.equal(6);
            expect(logStub.args[0][0]).to.match(/disabled old_password/);
            expect(logStub.args[1][0]).to.match(/created password hash/);
            expect(logStub.args[2][0]).to.match(/user exists, re-trying user creation/);
            expect(logStub.args[3][0]).to.match(/disabled old_password/);
            expect(logStub.args[4][0]).to.match(/created password hash/);
            expect(logStub.args[5][0]).to.match(/successfully created new user/);
            expect(ctx.mysql).to.exist;
            expect(ctx.mysql.username).to.match(/^ghost-[0-9]{1,4}$/);
            expect(ctx.mysql.password).to.match(/^[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*$/);
        });

        it('rejects if error occurs during user creation', function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query').resolves();
            queryStub.onSecondCall().resolves([{password: '*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19'}]);
            const err = new Error('User exists already');
            err.errno = 9999;
            queryStub.onThirdCall().rejects(new errors.CliError({message: 'User exists already', err: err, context: 'SELECT PASSWORD'}));
            const endStub = sinon.stub();
            instance.connection = {end: endStub};

            return instance.createUser({}, {host: 'localhost'}).then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.CliError);
                expect(error.message).to.match(/Creating new MySQL user errored with message:/);
                expect(error.options.err).to.exist;
                expect(error.options.context).to.match(/^SELECT PASSWORD/);
                expect(error.options.err.message).to.equal('User exists already');
                expect(queryStub.callCount).to.equal(3);
                expect(queryStub.args[0][0]).to.equal('SET old_passwords = 0;');
                expect(queryStub.args[1][0]).to.match(/^SELECT PASSWORD\('[a-zA-Z0-9!@#$%^&*()+_\-=}{[\]|:;"/?.><,`~]*'\) AS password;$/);
                expect(queryStub.args[2][0]).to.match(/^CREATE USER 'ghost-[0-9]{1,4}'@'localhost' IDENTIFIED WITH mysql_native_password AS '\*[0-9A-F]*';$/);
                expect(logStub.callCount).to.equal(3);
                expect(logStub.args[0][0]).to.match(/disabled old_password/);
                expect(logStub.args[1][0]).to.match(/created password hash/);
                expect(logStub.args[2][0]).to.match(/Unable to create custom Ghost user/);
                expect(endStub.calledOnce).to.be.true;
            });
        });

        it('catches cli errors and ends connection if any query fails', function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query').resolves();
            const endStub = sinon.stub();
            instance.connection = {end: endStub};
            queryStub.onSecondCall().resolves([{password: '*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19'}]);
            queryStub.onThirdCall().rejects(new errors.CliError({message: 'something failed', err: new Error('something failed'), context: 'SET old_passwords = 0;'}));

            return instance.createUser({}, {host: 'localhost'}).then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.CliError);
                expect(error.message).to.match(/Creating new MySQL user errored with message: something failed/);
                expect(error.options.context).to.match(/SET old_passwords = 0/);
                expect(error.options.err.message).to.equal('something failed');
                expect(queryStub.calledThrice).to.be.true;
                expect(queryStub.args[2][0]).to.match(/CREATE USER/);
                expect(logStub.calledThrice).to.be.true;
                expect(logStub.args[2][0]).to.match(/MySQL: Unable to create custom Ghost user/);
                expect(endStub.calledOnce).to.be.true;
            });
        });

        it('rejects with CliError and ends connection if any query fails', function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query').rejects(new errors.CliError({message: 'Oopsi', err: new Error('something failed'), context: 'SET old_passwords = 0;'}));
            const endStub = sinon.stub();
            instance.connection = {end: endStub};

            return instance.createUser({}, {host: 'localhost'}).then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.CliError);
                expect(error.message).to.match(/Creating new MySQL user errored with message: Oopsi/);
                expect(error.options.err).to.exist;
                expect(queryStub.calledOnce).to.be.true;
                expect(queryStub.args[0][0]).to.equal('SET old_passwords = 0;');
                expect(logStub.calledOnce).to.be.true;
                expect(logStub.args[0][0]).to.match(/Unable to create custom Ghost user/);
                expect(endStub.calledOnce).to.be.true;
            });
        });
    });

    describe('grantPermissions', function () {
        const MysqlExtension = require(modulePath);

        it('runs correct queries and logs things', function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query').resolves();

            return instance.grantPermissions({mysql: {username: 'testuser', host: '%'}}, {host: 'localhost', database: 'ghost'}).then(() => {
                expect(queryStub.calledTwice).to.be.true;
                expect(queryStub.calledWithExactly('GRANT ALL PRIVILEGES ON `ghost`.* TO \'testuser\'@\'%\';')).to.be.true;
                expect(queryStub.calledWithExactly('FLUSH PRIVILEGES;')).to.be.true;
                expect(logStub.calledTwice).to.be.true;
                expect(logStub.args[0][0]).to.match(/Successfully granted privileges/);
                expect(logStub.args[1][0]).to.match(/flushed privileges/);
            });
        });

        it('catches any error, logs and ends connection', function () {
            const logStub = sinon.stub();
            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            const queryStub = sinon.stub(instance, '_query');
            const endStub = sinon.stub();
            instance.connection = {end: endStub};
            queryStub.onFirstCall().resolves();
            queryStub.onSecondCall().rejects(new errors.CliError({message: 'something failed', err: new Error('something failed'), context: 'FLUSH PRIVILEGES;'}));

            return instance.grantPermissions({mysql: {username: 'test'}}, {host: 'localhost', database: 'ghost'}).then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.CliError);
                expect(error.message).to.match(/Granting database permissions errored with message: something failed/);
                expect(error.options.err).to.exist;
                expect(error.options.context).to.equal('FLUSH PRIVILEGES;');
                expect(queryStub.calledTwice).to.be.true;
                expect(logStub.calledTwice).to.be.true;
                expect(logStub.args[0][0]).to.match(/Successfully granted privileges/);
                expect(logStub.args[1][0]).to.match(/Unable either to grant permissions or flush privileges/);
                expect(endStub.calledOnce).to.be.true;
            });
        });
    });

    describe('_query', function () {
        it('_query runs query and logs query in verbose mode', function () {
            const MysqlExtension = require(modulePath);
            const queryStub = sinon.stub().callsArg(1);
            const logStub = sinon.stub();

            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            instance.connection = {query: queryStub};

            return instance._query('SELECT * FROM table').then(() => {
                expect(queryStub.calledOnce).to.be.true;
                expect(queryStub.calledWith('SELECT * FROM table')).to.be.true;
                expect(logStub.calledOnce).to.be.true;
                expect(logStub.args[0][0]).to.match(/SELECT \* FROM table/);
            });
        });

        it('rejects with correct CliError', function () {
            const MysqlExtension = require(modulePath);
            const queryStub = sinon.stub().callsFake((_, cb) => cb(new Error('failed executing')));
            const logStub = sinon.stub();

            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            instance.connection = {query: queryStub};

            return instance._query('SELECT * FROM table').then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.CliError);
                expect(error.message).to.equal('failed executing');
                expect(error.options.context).to.equal('SELECT * FROM table');
                expect(error.options.err).to.exist;
                expect(error.options.err.message).to.equal('failed executing');
                expect(queryStub.calledOnce).to.be.true;
                expect(queryStub.calledWith('SELECT * FROM table')).to.be.true;
                expect(logStub.calledOnce).to.be.true;
                expect(logStub.args[0][0]).to.match(/SELECT \* FROM table/);
            });
        });

        it('passes through if error is already a CliError', function () {
            const MysqlExtension = require(modulePath);
            const err = new errors.CliError('failed executing');
            err.options.context = 'SELECT * FROM table';
            err.options.err = new Error('failed executing');
            const queryStub = sinon.stub().throws(err);
            const logStub = sinon.stub();

            const instance = new MysqlExtension({logVerbose: logStub}, {}, {}, '/some/dir');
            instance.connection = {query: queryStub};

            return instance._query('SELECT * FROM table').then(() => {
                expect(false, 'error should have been thrown').to.be.true;
            }).catch((error) => {
                expect(error).to.be.an.instanceof(errors.CliError);
                expect(error.message).to.equal('failed executing');
                expect(error.options.context).to.equal('SELECT * FROM table');
                expect(error.options.err).to.exist;
                expect(error.options.err.message).to.equal('failed executing');
                expect(queryStub.calledOnce).to.be.true;
                expect(queryStub.calledWith('SELECT * FROM table')).to.be.true;
                expect(logStub.calledOnce).to.be.true;
                expect(logStub.args[0][0]).to.match(/SELECT \* FROM table/);
            });
        });
    });
});
