Skip to content

Commit

Permalink
feat(export): add export command (#1050)
Browse files Browse the repository at this point in the history
refs #468
- add export command & export taks
  • Loading branch information
acburdine authored Nov 7, 2019
1 parent b2629e5 commit f540af7
Show file tree
Hide file tree
Showing 6 changed files with 407 additions and 89 deletions.
30 changes: 30 additions & 0 deletions lib/commands/export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const Command = require('../command');

class ExportCommand extends Command {
async run(argv) {
const {exportTask} = require('../tasks/import');
const {SystemError} = require('../errors');

const instance = this.system.getInstance();
const isRunning = await instance.isRunning();

if (!isRunning) {
const shouldStart = await this.ui.confirm('Ghost instance is not currently running. Would you like to start it?', true);

if (!shouldStart) {
throw new SystemError('Ghost instance is not currently running');
}

instance.checkEnvironment();
await this.ui.run(() => instance.start(), 'Starting Ghost');
}

await this.ui.run(() => exportTask(this.ui, instance, argv.file), 'Exporting content');
this.ui.log(`Content exported to ${argv.file}`, 'green');
}
}

ExportCommand.description = 'Export content from a blog';
ExportCommand.params = '[file]';

module.exports = ExportCommand;
3 changes: 2 additions & 1 deletion lib/tasks/import/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const {importTask} = require('./tasks');
const {importTask, exportTask} = require('./tasks');
const parseExport = require('./parse-export');

module.exports = {
importTask,
exportTask,
parseExport
};
46 changes: 31 additions & 15 deletions lib/tasks/import/tasks.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
const validator = require('validator');

const {SystemError} = require('../../errors');
const parseExport = require('./parse-export');
const {isSetup, setup, runImport} = require('./api');
const {isSetup, setup, runImport, downloadExport} = require('./api');

const authPrompts = [{
type: 'string',
name: 'username',
message: 'Enter your Ghost administrator email address',
validate: val => validator.isEmail(`${val}`) || 'You must specify a valid email'
}, {
type: 'password',
name: 'password',
message: 'Enter your Ghost administrator password',
validate: val => validator.isLength(`${val}`, {min: 10}) || 'Password must be at least 10 characters long'
}];

async function importTask(ui, instance, exportFile) {
const {data} = parseExport(exportFile);
const url = instance.config.get('url');

const prompts = [{
type: 'password',
name: 'password',
message: 'Enter your Ghost administrator password',
validate: val => validator.isLength(`${val}`, {min: 10}) || 'Password must be at least 10 characters long'
}];
let prompts = authPrompts;

const blogIsSetup = await isSetup(instance.version, url);
if (blogIsSetup) {
prompts.unshift({
type: 'string',
name: 'username',
message: 'Enter your Ghost administrator email address',
validate: val => validator.isEmail(`${val}`) || 'You must specify a valid email'
});
if (!blogIsSetup) {
prompts = authPrompts.slice(1);
}

const {username, password} = await ui.prompt(prompts);
Expand All @@ -37,6 +40,19 @@ async function importTask(ui, instance, exportFile) {
}], false);
}

async function exportTask(ui, instance, exportFile) {
const url = instance.config.get('url');

const blogIsSetup = await isSetup(instance.version, url);
if (!blogIsSetup) {
throw new SystemError('Cannot export content from a blog that hasn\'t been set up.');
}

const authData = await ui.prompt(authPrompts);
await downloadExport(instance.version, url, authData, exportFile);
}

module.exports = {
importTask
importTask,
exportTask
};
91 changes: 91 additions & 0 deletions test/unit/commands/export-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const {expect} = require('chai');
const sinon = require('sinon');
const proxyquire = require('proxyquire').noCallThru();

const {SystemError} = require('../../../lib/errors');

const modulePath = '../../../lib/commands/export';

describe('Unit: Commands > export', function () {
it('runs export task if instance is running', async function () {
const exportTask = sinon.stub().resolves();
const instance = {
isRunning: sinon.stub().resolves(true)
};
const ui = {
run: sinon.stub().callsFake(fn => fn()),
log: sinon.stub()
};
const getInstance = sinon.stub().returns(instance);

const ExportCommand = proxyquire(modulePath, {'../tasks/import': {exportTask}});
const cmd = new ExportCommand(ui, {getInstance});

await cmd.run({file: 'test-output.json'});
expect(getInstance.calledOnce).to.be.true;
expect(instance.isRunning.calledOnce).to.be.true;
expect(ui.run.calledOnce).to.be.true;
expect(exportTask.calledOnceWithExactly(ui, instance, 'test-output.json')).to.be.true;
expect(ui.log.calledOnce).to.be.true;
});

it('prompts to start if not running and throws if not confirmed', async function () {
const exportTask = sinon.stub().resolves();
const instance = {
isRunning: sinon.stub().resolves(false)
};
const ui = {
confirm: sinon.stub().resolves(false),
run: sinon.stub().callsFake(fn => fn()),
log: sinon.stub()
};
const getInstance = sinon.stub().returns(instance);

const ExportCommand = proxyquire(modulePath, {'../tasks/import': {exportTask}});
const cmd = new ExportCommand(ui, {getInstance});

try {
await cmd.run({file: 'test-output.json'});
} catch (error) {
expect(error).to.be.an.instanceof(SystemError);
expect(error.message).to.include('not currently running');
expect(getInstance.calledOnce).to.be.true;
expect(instance.isRunning.calledOnce).to.be.true;
expect(ui.confirm.calledOnce).to.be.true;
expect(ui.run.called).to.be.false;
expect(exportTask.called).to.be.false;
expect(ui.log.called).to.be.false;
return;
}

expect.fail('run should have errored');
});

it('prompts to start if not running and starts if confirmed', async function () {
const exportTask = sinon.stub().resolves();
const instance = {
isRunning: sinon.stub().resolves(false),
start: sinon.stub().resolves(),
checkEnvironment: sinon.stub()
};
const ui = {
confirm: sinon.stub().resolves(true),
run: sinon.stub().callsFake(fn => fn()),
log: sinon.stub()
};
const getInstance = sinon.stub().returns(instance);

const ExportCommand = proxyquire(modulePath, {'../tasks/import': {exportTask}});
const cmd = new ExportCommand(ui, {getInstance});

await cmd.run({file: 'test-output.json'});
expect(getInstance.calledOnce).to.be.true;
expect(instance.isRunning.calledOnce).to.be.true;
expect(ui.confirm.calledOnce).to.be.true;
expect(instance.checkEnvironment.calledOnce).to.be.true;
expect(ui.run.calledTwice).to.be.true;
expect(instance.start.calledOnce).to.be.true;
expect(exportTask.calledOnceWithExactly(ui, instance, 'test-output.json')).to.be.true;
expect(ui.log.calledOnce).to.be.true;
});
});
132 changes: 132 additions & 0 deletions test/unit/commands/import-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const {expect} = require('chai');
const sinon = require('sinon');
const proxyquire = require('proxyquire').noCallThru();

const {SystemError} = require('../../../lib/errors');

const modulePath = '../../../lib/commands/import';

describe('Unit: Commands > import', function () {
it('throws error if importing a 0.x import into a > 1.x blog', async function () {
const parseExport = sinon.stub().returns({version: '0.11.14'});
const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport}});
const getInstance = sinon.stub().returns({version: '3.0.0'});

const cmd = new ImportCommand({}, {getInstance});

try {
await cmd.run({file: 'test-output.json'});
} catch (error) {
expect(error).to.be.an.instanceof(SystemError);
expect(error.message).to.include('can only be imported by Ghost v1.x versions');
expect(getInstance.calledOnce).to.be.true;
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
return;
}

expect.fail('expected run to error');
});

it('runs import task from v0.x to 1.x if blog is running', async function () {
const parseExport = sinon.stub().returns({version: '0.11.14'});
const run = sinon.stub().resolves();
const importTask = sinon.stub().resolves({run});
const instance = {
isRunning: sinon.stub().resolves(true),
version: '1.0.0'
};

const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport, importTask}});
const getInstance = sinon.stub().returns(instance);

const cmd = new ImportCommand({ui: true}, {getInstance});

await cmd.run({file: 'test-output.json'});
expect(getInstance.calledOnce).to.be.true;
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
expect(instance.isRunning.calledOnce).to.be.true;
expect(importTask.calledOnceWithExactly({ui: true}, instance, 'test-output.json')).to.be.true;
expect(run.calledOnce).to.be.true;
});

it('runs import task from v1.x to any', async function () {
const parseExport = sinon.stub().returns({version: '1.0.0'});
const run = sinon.stub().resolves();
const importTask = sinon.stub().resolves({run});
const instance = {
isRunning: sinon.stub().resolves(true),
version: '3.0.0'
};

const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport, importTask}});
const getInstance = sinon.stub().returns(instance);

const cmd = new ImportCommand({ui: true}, {getInstance});

await cmd.run({file: 'test-output.json'});
expect(getInstance.calledOnce).to.be.true;
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
expect(instance.isRunning.calledOnce).to.be.true;
expect(importTask.calledOnceWithExactly({ui: true}, instance, 'test-output.json')).to.be.true;
expect(run.calledOnce).to.be.true;
});

it('prompts to start if not running and throws if not confirmed', async function () {
const parseExport = sinon.stub().returns({version: '1.0.0'});
const instance = {
isRunning: sinon.stub().resolves(false),
version: '3.0.0'
};
const confirm = sinon.stub().resolves(false);

const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport}});
const getInstance = sinon.stub().returns(instance);

const cmd = new ImportCommand({confirm}, {getInstance});

try {
await cmd.run({file: 'test-output.json'});
} catch (error) {
expect(error).to.be.an.instanceof(SystemError);
expect(error.message).to.include('not currently running');
expect(getInstance.calledOnce).to.be.true;
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
expect(instance.isRunning.calledOnce).to.be.true;
expect(confirm.calledOnce).to.be.true;
return;
}

expect.fail('run should have errored');
});

it('prompts to start if not running and starts if confirmed', async function () {
const parseExport = sinon.stub().returns({version: '1.0.0'});
const runImport = sinon.stub().resolves();
const importTask = sinon.stub().resolves({run: runImport});
const instance = {
isRunning: sinon.stub().resolves(false),
checkEnvironment: sinon.stub(),
start: sinon.stub().resolves(),
version: '3.0.0'
};
const confirm = sinon.stub().resolves(true);
const run = sinon.stub().callsFake(fn => fn());

const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport, importTask}});
const getInstance = sinon.stub().returns(instance);

const cmd = new ImportCommand({confirm, run}, {getInstance});

await cmd.run({file: 'test-output.json'});

expect(getInstance.calledOnce).to.be.true;
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
expect(instance.isRunning.calledOnce).to.be.true;
expect(confirm.calledOnce).to.be.true;
expect(instance.checkEnvironment.calledOnce).to.be.true;
expect(run.calledOnce).to.be.true;
expect(instance.start.calledOnce).to.be.true;
expect(importTask.calledOnceWithExactly({confirm, run}, instance, 'test-output.json')).to.be.true;
expect(runImport.calledOnce).to.be.true;
});
});
Loading

0 comments on commit f540af7

Please sign in to comment.