mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-27 12:53:13 +03:00
d541a14826
- Currently theme uploads delete the existing theme before copying the new files into place - If something goes wrong with the delete action, you will end up in a bad state - Some or all of the files may be deleted, but now Ghost won't try to put the new theme in place, instead returning an error - This leaves you with an invalid active theme and a broken site - Unlike delete, move is a one-hit operation that succeeds or fails, there moving a theme is safer than deleting - This updated code moves the old theme to a folder with the name [theme-name]-[uuid] before copying the new theme into place - Even if this fails, the files should not be gone - There's a cleanup operation to remove the theme backup at the end, but we don't care too much if this fails
288 lines
12 KiB
JavaScript
288 lines
12 KiB
JavaScript
const should = require('should');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const _ = require('lodash');
|
|
const supertest = require('supertest');
|
|
const testUtils = require('../../utils');
|
|
const config = require('../../../core/shared/config');
|
|
const localUtils = require('./utils');
|
|
|
|
const ghost = testUtils.startGhost;
|
|
|
|
describe('Themes API', function () {
|
|
let ghostServer;
|
|
let ownerRequest;
|
|
|
|
const uploadTheme = (options) => {
|
|
const themePath = options.themePath;
|
|
const fieldName = 'file';
|
|
const request = options.request || ownerRequest;
|
|
|
|
return request
|
|
.post(localUtils.API.getApiQuery('themes/upload'))
|
|
.set('Origin', config.get('url'))
|
|
.attach(fieldName, themePath);
|
|
};
|
|
|
|
before(function () {
|
|
return ghost()
|
|
.then((_ghostServer) => {
|
|
ghostServer = _ghostServer;
|
|
});
|
|
});
|
|
|
|
before(function () {
|
|
ownerRequest = supertest.agent(config.get('url'));
|
|
return localUtils.doAuth(ownerRequest);
|
|
});
|
|
|
|
it('Can request all available themes', function () {
|
|
return ownerRequest
|
|
.get(localUtils.API.getApiQuery('themes/'))
|
|
.set('Origin', config.get('url'))
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(5);
|
|
|
|
localUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
|
|
jsonResponse.themes[0].name.should.eql('broken-theme');
|
|
jsonResponse.themes[0].package.should.be.an.Object().with.properties('name', 'version');
|
|
jsonResponse.themes[0].active.should.be.false();
|
|
|
|
localUtils.API.checkResponse(jsonResponse.themes[1], 'theme', 'templates');
|
|
jsonResponse.themes[1].name.should.eql('casper');
|
|
jsonResponse.themes[1].package.should.be.an.Object().with.properties('name', 'version');
|
|
jsonResponse.themes[1].active.should.be.true();
|
|
|
|
localUtils.API.checkResponse(jsonResponse.themes[2], 'theme');
|
|
jsonResponse.themes[2].name.should.eql('casper-1.4');
|
|
jsonResponse.themes[2].package.should.be.an.Object().with.properties('name', 'version');
|
|
jsonResponse.themes[2].active.should.be.false();
|
|
|
|
localUtils.API.checkResponse(jsonResponse.themes[3], 'theme');
|
|
jsonResponse.themes[3].name.should.eql('test-theme');
|
|
jsonResponse.themes[3].package.should.be.an.Object().with.properties('name', 'version');
|
|
jsonResponse.themes[3].active.should.be.false();
|
|
|
|
localUtils.API.checkResponse(jsonResponse.themes[4], 'theme');
|
|
jsonResponse.themes[4].name.should.eql('test-theme-channels');
|
|
jsonResponse.themes[4].package.should.be.false();
|
|
jsonResponse.themes[4].active.should.be.false();
|
|
});
|
|
});
|
|
|
|
it('Can download a theme', function () {
|
|
return ownerRequest
|
|
.get(localUtils.API.getApiQuery('themes/casper/download/'))
|
|
.set('Origin', config.get('url'))
|
|
.expect('Content-Type', /application\/zip/)
|
|
.expect('Content-Disposition', 'attachment; filename=casper.zip')
|
|
.expect(200);
|
|
});
|
|
|
|
it('Can upload a valid theme', function () {
|
|
return uploadTheme({themePath: path.join(__dirname, '..', '..', 'utils', 'fixtures', 'themes', 'valid.zip')})
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(1);
|
|
localUtils.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
|
|
return uploadTheme({themePath: path.join(__dirname, '..', '..', 'utils', 'fixtures', 'themes', 'valid.zip')});
|
|
})
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(1);
|
|
localUtils.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
|
|
const tmpFolderContents = fs.readdirSync(config.getContentPath('themes'));
|
|
tmpFolderContents.forEach((theme, index) => {
|
|
if (theme.match(/^\./)) {
|
|
tmpFolderContents.splice(index, 1);
|
|
}
|
|
});
|
|
|
|
// Note: at this point, the tmpFolder can legitimately still contain a valid_34324324 backup
|
|
// As it is deleted asynchronously
|
|
tmpFolderContents.should.containEql('valid');
|
|
tmpFolderContents.should.containEql('valid.zip');
|
|
|
|
// Check the Themes API returns the correct result
|
|
return ownerRequest
|
|
.get(localUtils.API.getApiQuery('themes/'))
|
|
.set('Origin', config.get('url'))
|
|
.expect(200);
|
|
})
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(6);
|
|
|
|
// Casper should be present and still active
|
|
const casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
|
|
should.exist(casperTheme);
|
|
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
|
|
casperTheme.active.should.be.true();
|
|
|
|
// The added theme should be here
|
|
const addedTheme = _.find(jsonResponse.themes, {name: 'valid'});
|
|
should.exist(addedTheme);
|
|
localUtils.API.checkResponse(addedTheme, 'theme');
|
|
addedTheme.active.should.be.false();
|
|
|
|
// Note: at this point, the API should not return a valid_34324324 backup folder as a theme
|
|
_.map(jsonResponse.themes, 'name').should.eql([
|
|
'broken-theme',
|
|
'casper',
|
|
'casper-1.4',
|
|
'test-theme',
|
|
'test-theme-channels',
|
|
'valid'
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('Can delete a theme', function () {
|
|
return ownerRequest
|
|
.del(localUtils.API.getApiQuery('themes/valid'))
|
|
.set('Origin', config.get('url'))
|
|
.expect(204)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
// Delete requests have empty bodies
|
|
jsonResponse.should.eql({});
|
|
|
|
// ensure tmp theme folder contains one theme again now
|
|
const tmpFolderContents = fs.readdirSync(config.getContentPath('themes'));
|
|
tmpFolderContents.forEach((theme, index) => {
|
|
if (theme.match(/^\./)) {
|
|
tmpFolderContents.splice(index, 1);
|
|
}
|
|
});
|
|
tmpFolderContents.should.be.an.Array().with.lengthOf(9);
|
|
|
|
tmpFolderContents.should.eql([
|
|
'broken-theme',
|
|
'casper',
|
|
'casper-1.4',
|
|
'casper.zip',
|
|
'invalid.zip',
|
|
'test-theme',
|
|
'test-theme-channels',
|
|
'valid.zip',
|
|
'warnings.zip'
|
|
]);
|
|
|
|
// Check the themes API returns the correct result after deletion
|
|
return ownerRequest
|
|
.get(localUtils.API.getApiQuery('themes/'))
|
|
.set('Origin', config.get('url'))
|
|
.expect(200);
|
|
})
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(5);
|
|
|
|
// Casper should be present and still active
|
|
const casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
|
|
should.exist(casperTheme);
|
|
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
|
|
casperTheme.active.should.be.true();
|
|
|
|
// The deleted theme should not be here
|
|
const deletedTheme = _.find(jsonResponse.themes, {name: 'valid'});
|
|
should.not.exist(deletedTheme);
|
|
});
|
|
});
|
|
|
|
it('Can upload a theme, which has warnings', function () {
|
|
return uploadTheme({themePath: path.join(__dirname, '/../../utils/fixtures/themes/warnings.zip')})
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(1);
|
|
localUtils.API.checkResponse(jsonResponse.themes[0], 'theme', ['warnings']);
|
|
jsonResponse.themes[0].name.should.eql('warnings');
|
|
jsonResponse.themes[0].active.should.be.false();
|
|
jsonResponse.themes[0].warnings.should.be.an.Array();
|
|
|
|
// Delete the theme to clean up after the test
|
|
return ownerRequest
|
|
.del(localUtils.API.getApiQuery('themes/warnings'))
|
|
.set('Origin', config.get('url'))
|
|
.expect(204);
|
|
});
|
|
});
|
|
|
|
it('Can activate a theme', function () {
|
|
return ownerRequest
|
|
.get(localUtils.API.getApiQuery('themes/'))
|
|
.set('Origin', config.get('url'))
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(5);
|
|
|
|
const casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
|
|
should.exist(casperTheme);
|
|
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
|
|
casperTheme.active.should.be.true();
|
|
|
|
const testTheme = _.find(jsonResponse.themes, {name: 'test-theme'});
|
|
should.exist(testTheme);
|
|
localUtils.API.checkResponse(testTheme, 'theme');
|
|
testTheme.active.should.be.false();
|
|
|
|
// Finally activate the new theme
|
|
return ownerRequest
|
|
.put(localUtils.API.getApiQuery('themes/test-theme/activate'))
|
|
.set('Origin', config.get('url'))
|
|
.expect(200);
|
|
})
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
should.exist(res.headers['x-cache-invalidate']);
|
|
should.exist(jsonResponse.themes);
|
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
|
jsonResponse.themes.length.should.eql(1);
|
|
|
|
const casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
|
|
should.not.exist(casperTheme);
|
|
|
|
const testTheme = _.find(jsonResponse.themes, {name: 'test-theme'});
|
|
should.exist(testTheme);
|
|
localUtils.API.checkResponse(testTheme, 'theme', ['warnings', 'templates']);
|
|
testTheme.active.should.be.true();
|
|
testTheme.warnings.should.be.an.Array();
|
|
});
|
|
});
|
|
});
|