2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Move activation to themes endpoint (#8093)

no issue
- browse will now include the correct activated theme again
- PUT /theme/:name/activate will activate a theme
- tests now read from a temp directory not content/themes
- all tests check errors and responses
This commit is contained in:
Hannah Wolfe 2017-03-08 10:46:03 +00:00 committed by Kevin Ansfield
parent 27e659a21e
commit 94d53cf5fb
5 changed files with 244 additions and 109 deletions

View file

@ -149,6 +149,11 @@ function apiRoutes() {
api.http(api.themes.upload)
);
apiRouter.put('/themes/:name/activate',
authenticatePrivate,
api.http(api.themes.activate)
);
apiRouter.del('/themes/:name',
authenticatePrivate,
api.http(api.themes.destroy)

View file

@ -13,9 +13,10 @@ var debug = require('debug')('ghost:api:themes'),
apiUtils = require('./utils'),
utils = require('./../utils'),
i18n = require('../i18n'),
settings = require('./settings'),
settingsCache = require('../settings/cache'),
themeUtils = require('../themes'),
themeList = themeUtils.list,
packageUtils = require('../utils/packages'),
themes;
/**
@ -26,11 +27,24 @@ var debug = require('debug')('ghost:api:themes'),
themes = {
browse: function browse() {
debug('browsing');
var result = packageUtils.filterPackages(themeList.getAll());
var result = themeList.toAPI(themeList.getAll(), settingsCache.get('activeTheme'));
debug('got result');
return Promise.resolve({themes: result});
},
activate: function activate(options) {
var themeName = options.name,
newSettings = [{
key: 'activeTheme',
value: themeName
}];
return settings.edit({settings: newSettings}, options).then(function () {
var result = themeList.toAPI(themeList.getAll(), themeName);
return Promise.resolve({themes: result});
});
},
upload: function upload(options) {
options = options || {};
@ -87,14 +101,12 @@ themes = {
return themeUtils.loadOne(zip.shortName);
})
.then(function (themeObject) {
// @TODO fix this craziness
var toFilter = {};
toFilter[zip.shortName] = themeObject;
themeObject = packageUtils.filterPackages(toFilter);
themeObject = themeList.toAPI(themeObject, settingsCache.get('activeTheme'));
// gscan theme structure !== ghost theme structure
if (theme.results.warning.length > 0) {
themeObject.warnings = _.cloneDeep(theme.results.warning);
}
return {themes: themeObject};
})
.finally(function () {
@ -158,6 +170,7 @@ themes = {
.then(function () {
themeList.del(name);
events.emit('theme.deleted', name);
// Delete returns an empty 204 response
});
}
};

View file

@ -2,6 +2,7 @@
* Store themes after loading them from the file system
*/
var _ = require('lodash'),
packages = require('../utils/packages'),
themeListCache = {};
module.exports = {
@ -32,5 +33,17 @@ module.exports = {
});
return themeListCache;
},
toAPI: function toAPI(themes, active) {
var toFilter;
if (themes.hasOwnProperty('name')) {
toFilter = {};
toFilter[themes.name] = themes;
} else {
toFilter = themes;
}
return packages.filterPackages(toFilter, active);
}
};

View file

@ -2,13 +2,12 @@ var testUtils = require('../../../utils'),
should = require('should'),
supertest = require('supertest'),
fs = require('fs-extra'),
path = require('path'),
join = require('path').join,
tmp = require('tmp'),
_ = require('lodash'),
ghost = testUtils.startGhost,
config = require('../../../../../core/server/config'),
request;
describe('Themes API', function () {
describe('Themes API (Forked)', function () {
var scope = {
ownerAccessToken: '',
editorAccessToken: '',
@ -22,46 +21,68 @@ describe('Themes API', function () {
.attach(fieldName, themePath);
},
editor: null
}, ghostServer;
}, forkedGhost, tmpContentPath;
function setupThemesFolder() {
tmpContentPath = tmp.dirSync({unsafeCleanup: true});
fs.mkdirSync(join(tmpContentPath.name, 'themes'));
fs.mkdirSync(join(tmpContentPath.name, 'themes', 'casper'));
fs.writeFileSync(
join(tmpContentPath.name, 'themes', 'casper', 'package.json'),
JSON.stringify({name: 'casper', version: '0.1.2'})
);
}
function teardownThemesFolder() {
return tmpContentPath.removeCallback();
}
before(function (done) {
ghost().then(function (_ghostServer) {
ghostServer = _ghostServer;
return ghostServer.start();
}).then(function () {
request = supertest.agent(config.get('url'));
}).then(function () {
return testUtils.doAuth(request);
}).then(function (token) {
scope.ownerAccessToken = token;
// Setup a temporary themes directory
setupThemesFolder();
// Fork Ghost to read from the temp directory, not the developer's themes
testUtils.fork.ghost({
paths: {
contentPath: tmpContentPath.name
}
}, 'themetests')
.then(function (child) {
forkedGhost = child;
request = supertest('http://127.0.0.1:' + child.port);
})
.then(function () {
return testUtils.doAuth(request);
})
.then(function (token) {
scope.ownerAccessToken = token;
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'test+1@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[1]
});
}).then(function (user) {
scope.editor = user;
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'test+1@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[1]
});
})
.then(function (user) {
scope.editor = user;
request.user = scope.editor;
return testUtils.doAuth(request);
}).then(function (token) {
scope.editorAccessToken = token;
done();
}).catch(done);
request.user = scope.editor;
return testUtils.doAuth(request);
})
.then(function (token) {
scope.editorAccessToken = token;
done();
})
.catch(done);
});
after(function () {
// clean successful uploaded themes
fs.removeSync(config.getContentPath('themes') + '/valid');
fs.removeSync(config.getContentPath('themes') + '/casper.zip');
after(function (done) {
teardownThemesFolder();
// gscan creates /test/tmp in test mode
fs.removeSync(config.get('paths').appRoot + '/test');
return testUtils.clearData()
.then(function () {
return ghostServer.stop();
});
if (forkedGhost) {
forkedGhost.kill(done);
} else {
done(new Error('No forked ghost process exists, test setup must have failed.'));
}
});
describe('success cases', function () {
@ -76,59 +97,18 @@ describe('Themes API', function () {
var jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.be.above(0);
jsonResponse.themes.length.should.eql(1);
testUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
jsonResponse.themes[0].name.should.eql('casper');
jsonResponse.themes[0].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[0].active.should.be.true();
done();
});
});
it('upload theme', function (done) {
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/themes/valid.zip')})
.end(function (err, res) {
if (err) {
return done(err);
}
res.statusCode.should.eql(200);
should.exist(res.body.themes);
res.body.themes.length.should.eql(1);
should.exist(res.body.themes[0].name);
should.exist(res.body.themes[0].package);
// upload same theme again to force override
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/themes/valid.zip')})
.end(function (err) {
if (err) {
return done(err);
}
// ensure contains two files (zip and extracted theme)
fs.readdirSync(config.getContentPath('themes')).join().match(/valid/gi).length.should.eql(1);
done();
});
});
});
it('get all themes + new theme', function (done) {
request.get(testUtils.API.getApiQuery('themes/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.be.above(0);
// ensure the new 'valid' theme is available
should.exist(_.find(jsonResponse.themes, {name: 'valid'}));
done();
});
});
it('download theme uuid', function (done) {
it('download theme', function (done) {
request.get(testUtils.API.getApiQuery('themes/casper/download/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect('Content-Type', /application\/zip/)
@ -143,25 +123,137 @@ describe('Themes API', function () {
});
});
it('delete theme uuid', function (done) {
request.del(testUtils.API.getApiQuery('themes/valid'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(204)
.end(function (err) {
it('upload theme', function (done) {
var jsonResponse;
scope.uploadTheme({themePath: join(__dirname, '/../../../utils/fixtures/themes/valid.zip')})
.end(function (err, res) {
if (err) {
return done(err);
}
fs.existsSync(config.getContentPath('themes') + '/valid').should.eql(false);
fs.existsSync(config.getContentPath('themes') + '/valid.zip').should.eql(false);
done();
jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(1);
testUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
jsonResponse.themes[0].name.should.eql('valid');
jsonResponse.themes[0].active.should.be.false();
// upload same theme again to force override
scope.uploadTheme({themePath: join(__dirname, '/../../../utils/fixtures/themes/valid.zip')})
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(1);
testUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
jsonResponse.themes[0].name.should.eql('valid');
jsonResponse.themes[0].active.should.be.false();
// ensure tmp theme folder contains two themes now
var tmpFolderContents = fs.readdirSync(join(tmpContentPath.name, 'themes'));
tmpFolderContents.should.be.an.Array().with.lengthOf(2);
tmpFolderContents[0].should.eql('casper');
tmpFolderContents[1].should.eql('valid');
// Check the Themes API returns the correct result
request.get(testUtils.API.getApiQuery('themes/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var addedTheme, casperTheme;
jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(2);
// Casper should be present and still active
casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
should.exist(casperTheme);
testUtils.API.checkResponse(casperTheme, 'theme');
casperTheme.active.should.be.true();
// The added theme should be here
addedTheme = _.find(jsonResponse.themes, {name: 'valid'});
should.exist(addedTheme);
testUtils.API.checkResponse(addedTheme, 'theme');
addedTheme.active.should.be.false();
done();
});
});
});
});
// NOTE: This test requires the previous upload test
// @TODO make this test independent
it('delete theme', function (done) {
var jsonResponse;
request.del(testUtils.API.getApiQuery('themes/valid'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(204)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
// Delete requests have empty bodies
jsonResponse.should.eql({});
// ensure tmp theme folder contains one theme again now
var tmpFolderContents = fs.readdirSync(join(tmpContentPath.name, 'themes'));
tmpFolderContents.should.be.an.Array().with.lengthOf(1);
tmpFolderContents[0].should.eql('casper');
// Check the settings API returns the correct result
request.get(testUtils.API.getApiQuery('themes/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var deletedTheme, casperTheme;
jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(1);
// Casper should be present and still active
casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
should.exist(casperTheme);
testUtils.API.checkResponse(casperTheme, 'theme');
casperTheme.active.should.be.true();
// The deleted theme should not be here
deletedTheme = _.find(jsonResponse.themes, {name: 'valid'});
should.not.exist(deletedTheme);
done();
});
});
});
});
describe('error cases', function () {
it('upload invalid theme', function (done) {
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/themes/invalid.zip')})
scope.uploadTheme({themePath: join(__dirname, '/../../../utils/fixtures/themes/invalid.zip')})
.end(function (err, res) {
if (err) {
return done(err);
@ -176,7 +268,7 @@ describe('Themes API', function () {
});
it('upload casper.zip', function (done) {
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/themes/casper.zip')})
scope.uploadTheme({themePath: join(__dirname, '/../../../utils/fixtures/themes/casper.zip')})
.end(function (err, res) {
if (err) {
return done(err);
@ -194,11 +286,15 @@ describe('Themes API', function () {
request.del(testUtils.API.getApiQuery('themes/casper'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(422)
.end(function (err) {
.end(function (err, res) {
if (err) {
return done(err);
}
res.body.errors.length.should.eql(1);
res.body.errors[0].errorType.should.eql('ValidationError');
res.body.errors[0].message.should.eql('Deleting the default casper theme is not allowed.');
done();
});
});
@ -207,31 +303,39 @@ describe('Themes API', function () {
request.del(testUtils.API.getApiQuery('themes/not-existent'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(404)
.end(function (err) {
.end(function (err, res) {
if (err) {
return done(err);
}
res.body.errors.length.should.eql(1);
res.body.errors[0].errorType.should.eql('NotFoundError');
res.body.errors[0].message.should.eql('Theme does not exist.');
done();
});
});
it('upload non application/zip', function (done) {
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/csv/single-column-with-header.csv')})
scope.uploadTheme({themePath: join(__dirname, '/../../../utils/fixtures/csv/single-column-with-header.csv')})
.end(function (err, res) {
if (err) {
return done(err);
}
res.statusCode.should.eql(415);
res.body.errors.length.should.eql(1);
res.body.errors[0].errorType.should.eql('UnsupportedMediaTypeError');
res.body.errors[0].message.should.eql('Please select a valid zip file.');
done();
});
});
// @TODO: does not pass in travis with 0.10.x, but local it works
// @TODO: make this a nicer error!
it.skip('upload different field name', function (done) {
scope.uploadTheme({
themePath: path.join(__dirname, '/../../../utils/fixtures/csv/single-column-with-header.csv'),
themePath: join(__dirname, '/../../../utils/fixtures/csv/single-column-with-header.csv'),
fieldName: 'wrong'
}).end(function (err, res) {
if (err) {
@ -247,7 +351,7 @@ describe('Themes API', function () {
describe('As Editor', function () {
it('no permissions to upload theme', function (done) {
scope.uploadTheme({
themePath: path.join(__dirname, '/../../../utils/fixtures/themes/valid.zip'),
themePath: join(__dirname, '/../../../utils/fixtures/themes/valid.zip'),
accessToken: scope.editorAccessToken
}).end(function (err, res) {
if (err) {

View file

@ -31,7 +31,7 @@ var _ = require('lodash'),
role: _.keys(schema.roles),
permission: _.keys(schema.permissions),
notification: ['type', 'message', 'status', 'id', 'dismissible', 'location'],
theme: ['uuid', 'name', 'version', 'active'],
theme: ['name', 'package', 'active'],
themes: ['themes'],
invites: _(schema.invites).keys().without('token').value()
};