feature: theme upload/download/delete (#7209)

refs #7204

- added 3 new themes permissions
- change core/client
- add theme upload/download logic
- extended local file storage to serve zips
- added gscan dependency
- add ability to handle the express response within the api layer
- restrict theme upload to local file storage
- added 007 migration
This commit is contained in:
Katharina Irrgang 2016-08-23 14:07:25 +02:00 committed by Hannah Wolfe
parent f546a5ce1d
commit a91e54cf1a
22 changed files with 773 additions and 94 deletions

@ -1 +1 @@
Subproject commit 1ce2e8b37c931bcc4ac1381f17bc05018282a677
Subproject commit efbb0ee9c69b117cf26c84d635e135415ba5649f

View File

@ -19,6 +19,7 @@ var _ = require('lodash'),
clients = require('./clients'),
users = require('./users'),
slugs = require('./slugs'),
themes = require('./themes'),
subscribers = require('./subscribers'),
authentication = require('./authentication'),
uploads = require('./upload'),
@ -239,6 +240,13 @@ http = function http(apiMethod) {
if (res.get('Content-Type') && res.get('Content-Type').indexOf('text/csv') === 0) {
return res.status(200).send(response);
}
// CASE: api method response wants to handle the express response
// example: serve files (stream)
if (_.isFunction(response)) {
return response(req, res, next);
}
// Send a properly formatting HTTP response containing the data with correct headers
res.json(response || {});
}).catch(function onAPIError(error) {
@ -271,7 +279,8 @@ module.exports = {
subscribers: subscribers,
authentication: authentication,
uploads: uploads,
slack: slack
slack: slack,
themes: themes
};
/**

177
core/server/api/themes.js Normal file
View File

@ -0,0 +1,177 @@
// # Themes API
// RESTful API for Themes
var Promise = require('bluebird'),
_ = require('lodash'),
gscan = require('gscan'),
fs = require('fs-extra'),
config = require('../config'),
errors = require('../errors'),
storage = require('../storage'),
settings = require('./settings'),
utils = require('./utils'),
i18n = require('../i18n'),
themes;
/**
* ## Themes API Methods
*
* **See:** [API Methods](index.js.html#api%20methods)
*/
themes = {
upload: function upload(options) {
options = options || {};
// consistent filename uploads
options.originalname = options.originalname.toLowerCase();
var zip = {
path: options.path,
name: options.originalname,
shortName: options.originalname.split('.zip')[0]
}, theme, storageAdapter = storage.getStorage('themes');
// check if zip name is casper.zip
if (zip.name === 'casper.zip') {
throw new errors.ValidationError(i18n.t('errors.api.themes.overrideCasper'));
}
return utils.handlePermissions('themes', 'add')(options)
.then(function () {
return gscan.checkZip(zip, {keepExtractedDir: true});
})
.then(function (_theme) {
theme = _theme;
theme = gscan.format(theme);
if (!theme.results.error.length) {
return;
}
var validationErrors = [];
_.each(theme.results.error, function (error) {
if (error.failures) {
_.each(error.failures, function (childError) {
validationErrors.push(new errors.ValidationError(i18n.t('errors.api.themes.invalidTheme', {
reason: childError.ref
})));
});
}
validationErrors.push(new errors.ValidationError(i18n.t('errors.api.themes.invalidTheme', {
reason: error.rule
})));
});
throw validationErrors;
})
.then(function () {
return storageAdapter.exists(config.paths.themePath + '/' + zip.shortName);
})
.then(function (themeExists) {
// delete existing theme
if (themeExists) {
return storageAdapter.delete(zip.shortName, config.paths.themePath);
}
})
.then(function () {
return storageAdapter.exists(config.paths.themePath + '/' + zip.name);
})
.then(function (zipExists) {
// delete existing theme zip
if (zipExists) {
return storageAdapter.delete(zip.name, config.paths.themePath);
}
})
.then(function () {
// store extracted theme
return storageAdapter.save({
name: zip.shortName,
path: theme.path
}, config.paths.themePath);
})
.then(function () {
// force reload of availableThemes
// right now the logic is in the ConfigManager
// if we create a theme collection, we don't have to read them from disk
return config.loadThemes();
})
.then(function () {
// the settings endpoint is used to fetch the availableThemes
// so we have to force updating the in process cache
return settings.updateSettingsCache();
})
.then(function (settings) {
// gscan theme structure !== ghost theme structure
return {themes: [_.find(settings.availableThemes.value, {name: zip.shortName})]};
})
.finally(function () {
// remove zip upload from multer
// happens in background
Promise.promisify(fs.removeSync)(zip.path)
.catch(function (err) {
errors.logError(err);
});
// remove extracted dir from gscan
// happens in background
if (theme) {
Promise.promisify(fs.removeSync)(theme.path)
.catch(function (err) {
errors.logError(err);
});
}
});
},
download: function download(options) {
var themeName = options.name,
theme = config.paths.availableThemes[themeName],
storageAdapter = storage.getStorage('themes');
if (!theme) {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.themes.invalidRequest')));
}
return utils.handlePermissions('themes', 'read')(options)
.then(function () {
return storageAdapter.serve({isTheme: true, name: themeName});
});
},
/**
* remove theme zip
* remove theme folder
*/
destroy: function destroy(options) {
var name = options.name,
zipName = name + '.zip',
theme,
storageAdapter = storage.getStorage('themes');
return utils.handlePermissions('themes', 'destroy')(options)
.then(function () {
if (name === 'casper') {
throw new errors.ValidationError(i18n.t('errors.api.themes.destroyCasper'));
}
theme = config.paths.availableThemes[name];
if (!theme) {
throw new errors.NotFoundError(i18n.t('errors.api.themes.themeDoesNotExist'));
}
return storageAdapter.delete(name, config.paths.themePath);
})
.then(function () {
return storageAdapter.delete(zipName, config.paths.themePath);
})
.then(function () {
return config.loadThemes();
})
.then(function () {
return settings.updateSettingsCache();
});
}
};
module.exports = themes;

View File

@ -78,11 +78,31 @@ ConfigManager.prototype.init = function (rawConfig) {
// just the object appropriate for this NODE_ENV
self.set(rawConfig);
return Promise.all([readThemes(self._config.paths.themePath), readDirectory(self._config.paths.appPath)]).then(function (paths) {
self._config.paths.availableThemes = paths[0];
self._config.paths.availableApps = paths[1];
return self._config;
});
return self.loadThemes()
.then(function () {
return self.loadApps();
})
.then(function () {
return self._config;
});
};
ConfigManager.prototype.loadThemes = function () {
var self = this;
return readThemes(self._config.paths.themePath)
.then(function (result) {
self._config.paths.availableThemes = result;
});
};
ConfigManager.prototype.loadApps = function () {
var self = this;
return readDirectory(self._config.paths.appPath)
.then(function (result) {
self._config.paths.availableApps = result;
});
};
/**
@ -173,13 +193,13 @@ ConfigManager.prototype.set = function (config) {
} else {
// ensure there is a default image storage adapter
if (!this._config.storage.active.images) {
this._config.storage.active.images = defaultSchedulingAdapter;
this._config.storage.active.images = defaultStorageAdapter;
}
// ensure there is a default theme storage adapter
if (!this._config.storage.active.themes) {
this._config.storage.active.themes = defaultSchedulingAdapter;
}
// @TODO: right now we only support theme uploads to local file storage
// @TODO: we need to change reading themes from disk on bootstrap (see loadThemes)
this._config.storage.active.themes = defaultStorageAdapter;
}
if (activeSchedulingAdapter === defaultSchedulingAdapter) {
@ -250,7 +270,7 @@ ConfigManager.prototype.set = function (config) {
uploads: {
subscribers: {
extensions: ['.csv'],
contentTypes: ['text/csv','application/csv']
contentTypes: ['text/csv', 'application/csv']
},
images: {
extensions: ['.jpg', '.jpeg', '.gif', '.png', '.svg', '.svgz'],
@ -259,6 +279,10 @@ ConfigManager.prototype.set = function (config) {
db: {
extensions: ['.json'],
contentTypes: ['application/octet-stream', 'application/json']
},
themes: {
extensions: ['.zip'],
contentTypes: ['application/zip']
}
},
deprecatedItems: ['updateCheck', 'mail.fromaddress'],

View File

@ -0,0 +1,33 @@
var utils = require('../utils'),
permissions = require('../../../../permissions'),
resource = 'theme';
function getPermissions() {
return utils.findModelFixtures('Permission', {object_type: resource});
}
function getRelations() {
return utils.findPermissionRelationsForObject(resource);
}
function printResult(logger, result, message) {
if (result.done === result.expected) {
logger.info(message);
} else {
logger.warn('(' + result.done + '/' + result.expected + ') ' + message);
}
}
module.exports = function addThemePermissions(options, logger) {
var modelToAdd = getPermissions(),
relationToAdd = getRelations();
return utils.addFixturesForModel(modelToAdd, options).then(function (result) {
printResult(logger, result, 'Adding permissions fixtures for ' + resource + 's');
return utils.addFixturesForRelation(relationToAdd, options);
}).then(function (result) {
printResult(logger, result, 'Adding permissions_roles fixtures for ' + resource + 's');
}).then(function () {
return permissions.init(options);
});
};

View File

@ -0,0 +1,3 @@
module.exports = [
require('./01-add-themes-permissions')
];

View File

@ -190,6 +190,21 @@
"action_type": "edit",
"object_type": "theme"
},
{
"name": "Upload themes",
"action_type": "add",
"object_type": "theme"
},
{
"name": "Download themes",
"action_type": "read",
"object_type": "theme"
},
{
"name": "Delete themes",
"action_type": "destroy",
"object_type": "theme"
},
{
"name": "Browse users",
"action_type": "browse",

View File

@ -1,7 +1,7 @@
{
"core": {
"databaseVersion": {
"defaultValue": "006"
"defaultValue": "007"
},
"dbHash": {
"defaultValue": null

View File

@ -273,9 +273,11 @@ canThis = function (context) {
return result.beginCheck(context);
};
init = refresh = function () {
init = refresh = function (options) {
options = options || {};
// Load all the permissions
return Models.Permission.findAll().then(function (perms) {
return Models.Permission.findAll(options).then(function (perms) {
var seenActions = {};
exported.actionsMap = {};

View File

@ -99,6 +99,24 @@ apiRoutes = function apiRoutes(middleware) {
// ## Slugs
router.get('/slugs/:type/:name', authenticatePrivate, api.http(api.slugs.generate));
// ## Themes
router.get('/themes/:name/download',
authenticatePrivate,
api.http(api.themes.download)
);
router.post('/themes/upload',
authenticatePrivate,
middleware.upload.single('theme'),
middleware.validation.upload({type: 'themes'}),
api.http(api.themes.upload)
);
router.del('/themes/:name',
authenticatePrivate,
api.http(api.themes.destroy)
);
// ## Notifications
router.get('/notifications', authenticatePrivate, api.http(api.notifications.browse));
router.post('/notifications', authenticatePrivate, api.http(api.notifications.add));

View File

@ -2,18 +2,20 @@
// The (default) module for storing images, using the local file system
var serveStatic = require('express').static,
fs = require('fs-extra'),
path = require('path'),
util = require('util'),
Promise = require('bluebird'),
errors = require('../errors'),
config = require('../config'),
utils = require('../utils'),
BaseStore = require('./base');
fs = require('fs-extra'),
path = require('path'),
util = require('util'),
Promise = require('bluebird'),
execFileAsPromise = Promise.promisify(require('child_process').execFile),
errors = require('../errors'),
config = require('../config'),
utils = require('../utils'),
BaseStore = require('./base');
function LocalFileStore() {
BaseStore.call(this);
}
util.inherits(LocalFileStore, BaseStore);
// ### Save
@ -33,7 +35,7 @@ LocalFileStore.prototype.save = function (image, targetDir) {
// The src for the image must be in URI format, not a file system path, which in Windows uses \
// For local file system storage can use relative path so add a slash
var fullUrl = (config.paths.subdir + '/' + config.paths.imagesRelPath + '/' +
path.relative(config.paths.imagesPath, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/');
path.relative(config.paths.imagesPath, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/');
return fullUrl;
}).catch(function (e) {
errors.logError(e);
@ -51,14 +53,52 @@ LocalFileStore.prototype.exists = function (filename) {
};
// middleware for serving the files
LocalFileStore.prototype.serve = function () {
// For some reason send divides the max age number by 1000
// Fallthrough: false ensures that if an image isn't found, it automatically 404s
return serveStatic(config.paths.imagesPath, {maxAge: utils.ONE_YEAR_MS, fallthrough: false});
LocalFileStore.prototype.serve = function (options) {
var self = this;
options = options || {};
// CASE: serve themes
// serveStatic can't be used to serve themes, because
// download files depending on the route (see `send` npm module)
if (options.isTheme) {
return function downloadTheme(req, res, next) {
var themeName = options.name,
zipName = themeName + '.zip',
zipPath = config.paths.themePath + '/' + zipName,
stream;
self.exists(zipPath)
.then(function (zipExists) {
if (!zipExists) {
return execFileAsPromise('zip', ['-r', zipName, themeName], {cwd: config.paths.themePath});
}
})
.then(function () {
res.set({
'Content-disposition': 'attachment; filename={themeName}.zip'.replace('{themeName}', themeName),
'Content-Type': 'application/zip'
});
stream = fs.createReadStream(zipPath);
stream.pipe(res);
})
.catch(function (err) {
next(err);
});
};
} else {
// CASE: serve images
// For some reason send divides the max age number by 1000
// Fallthrough: false ensures that if an image isn't found, it automatically 404s
return serveStatic(config.paths.imagesPath, {maxAge: utils.ONE_YEAR_MS, fallthrough: false});
}
};
LocalFileStore.prototype.delete = function () {
return Promise.reject('not implemented');
LocalFileStore.prototype.delete = function (fileName, targetDir) {
targetDir = targetDir || this.getTargetDir(config.paths.imagesPath);
var path = targetDir + '/' + fileName;
return Promise.promisify(fs.remove)(path);
};
module.exports = LocalFileStore;

View File

@ -358,7 +358,11 @@
"noPermissionToBrowseThemes": "You do not have permission to browse themes.",
"noPermissionToEditThemes": "You do not have permission to edit themes.",
"themeDoesNotExist": "Theme does not exist.",
"invalidRequest": "Invalid request."
"invalidTheme": "Theme is invalid: {reason}",
"missingFile": "Please select a theme.",
"invalidFile": "Please select a valid zip file.",
"overrideCasper": "Please rename your zip, it's not allowed to override the default casper theme.",
"destroyCasper": "Deleting the default casper theme is not allowed."
},
"images": {
"missingFile": "Please select an image.",

View File

@ -0,0 +1,274 @@
var testUtils = require('../../../utils'),
should = require('should'),
supertest = require('supertest'),
fs = require('fs-extra'),
path = require('path'),
_ = require('lodash'),
ghost = require('../../../../../core'),
config = require('../../../../../core/server/config'),
request;
describe('Themes API', function () {
var scope = {
ownerownerAccessToken: '',
editorAccessToken: '',
uploadTheme: function uploadTheme(options) {
var themePath = options.themePath,
fieldName = options.fieldName || 'theme',
accessToken = options.accessToken || scope.ownerAccessToken;
return request.post(testUtils.API.getApiQuery('themes/upload'))
.set('Authorization', 'Bearer ' + accessToken)
.attach(fieldName, themePath);
}
};
before(function (done) {
ghost().then(function (ghostServer) {
request = supertest.agent(ghostServer.rootApp);
}).then(function () {
return testUtils.doAuth(request, 'perms:theme', 'perms:init', 'users:roles:no-owner');
}).then(function (token) {
scope.ownerAccessToken = token;
// 2 === Editor
request.userIndex = 2;
return testUtils.doAuth(request);
}).then(function (token) {
scope.editorAccessToken = token;
done();
}).catch(done);
});
after(function (done) {
// clean successful uploaded themes
fs.removeSync(config.paths.themePath + '/valid');
fs.removeSync(config.paths.themePath + '/casper.zip');
// gscan creates /test/tmp in test mode
fs.removeSync(config.paths.appRoot + '/test');
testUtils.clearData()
.then(function () {
done();
}).catch(done);
});
describe('success cases', function () {
it('get all available themes', function (done) {
request.get(testUtils.API.getApiQuery('settings/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.end(function (err, res) {
if (err) {
return done(err);
}
var availableThemes = _.find(res.body.settings, {key: 'availableThemes'});
should.exist(availableThemes);
availableThemes.value.length.should.be.above(0);
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.paths.themePath).join().match(/valid/gi).length.should.eql(1);
done();
});
});
});
it('get all available themes', function (done) {
request.get(testUtils.API.getApiQuery('settings/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.end(function (err, res) {
if (err) {
return done(err);
}
var availableThemes = _.find(res.body.settings, {key: 'availableThemes'});
should.exist(availableThemes);
// ensure the new 'valid' theme is available
should.exist(_.find(availableThemes.value, {name: 'valid'}));
done();
});
});
it('download theme uuid', function (done) {
request.get(testUtils.API.getApiQuery('themes/casper/download/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect('Content-Type', /application\/zip/)
.expect('Content-Disposition', 'attachment; filename=casper.zip')
.expect(200)
.end(function (err) {
if (err) {
return done(err);
}
done();
});
});
it('delete theme uuid', function (done) {
request.del(testUtils.API.getApiQuery('themes/valid'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(204)
.end(function (err) {
if (err) {
return done(err);
}
fs.existsSync(config.paths.themePath + '/valid').should.eql(false);
fs.existsSync(config.paths.themePath + '/valid.zip').should.eql(false);
done();
});
});
});
describe('error cases', function () {
it('upload invalid theme', function (done) {
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/themes/invalid.zip')})
.end(function (err, res) {
if (err) {
return done(err);
}
res.statusCode.should.eql(422);
res.body.errors.length.should.eql(1);
res.body.errors[0].message.should.eql('Theme is invalid: A template file called post.hbs must be present.');
done();
});
});
it('upload casper.zip', function (done) {
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/themes/casper.zip')})
.end(function (err, res) {
if (err) {
return done(err);
}
res.statusCode.should.eql(422);
res.body.errors.length.should.eql(1);
res.body.errors[0].message.should.eql('Please rename your zip, it\'s not allowed to override the default casper theme.');
done();
});
});
it('delete casper', function (done) {
request.del(testUtils.API.getApiQuery('themes/casper'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(422)
.end(function (err) {
if (err) {
return done(err);
}
done();
});
});
it('delete not existent theme', function (done) {
request.del(testUtils.API.getApiQuery('themes/not-existent'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.expect(404)
.end(function (err) {
if (err) {
return done(err);
}
done();
});
});
it('upload non application/zip', function (done) {
scope.uploadTheme({themePath: path.join(__dirname, '/../../../utils/fixtures/csv/single-column-with-header.csv')})
.end(function (err, res) {
if (err) {
return done(err);
}
res.statusCode.should.eql(415);
done();
});
});
// @TODO: does not pass in travis with 0.10.x, but local it works
it.skip('upload different field name', function (done) {
scope.uploadTheme({
themePath: path.join(__dirname, '/../../../utils/fixtures/csv/single-column-with-header.csv'),
fieldName: 'wrong'
}).end(function (err, res) {
if (err) {
return done(err);
}
res.statusCode.should.eql(500);
res.body.errors[0].message.should.eql('Unexpected field');
done();
});
});
describe('As Editor', function () {
it('no permissions to upload theme', function (done) {
scope.uploadTheme({
themePath: path.join(__dirname, '/../../../utils/fixtures/themes/valid.zip'),
accessToken: scope.editorAccessToken
}).end(function (err, res) {
if (err) {
return done(err);
}
res.statusCode.should.eql(403);
done();
});
});
it('no permissions to delete theme', function (done) {
request.del(testUtils.API.getApiQuery('themes/test'))
.set('Authorization', 'Bearer ' + scope.editorAccessToken)
.expect(403)
.end(function (err) {
if (err) {
return done(err);
}
done();
});
});
it('no permissions to download theme', function (done) {
request.get(testUtils.API.getApiQuery('themes/casper/download/'))
.set('Authorization', 'Bearer ' + scope.editorAccessToken)
.expect(403)
.end(function (err) {
if (err) {
return done(err);
}
done();
});
});
});
});
});

View File

@ -104,48 +104,54 @@ describe('Database Migration (special functions)', function () {
permissions[21].should.be.AssignedToRoles(['Administrator']);
permissions[22].name.should.eql('Edit themes');
permissions[22].should.be.AssignedToRoles(['Administrator']);
permissions[23].name.should.eql('Upload themes');
permissions[23].should.be.AssignedToRoles(['Administrator']);
permissions[24].name.should.eql('Download themes');
permissions[24].should.be.AssignedToRoles(['Administrator']);
permissions[25].name.should.eql('Delete themes');
permissions[25].should.be.AssignedToRoles(['Administrator']);
// Users
permissions[23].name.should.eql('Browse users');
permissions[23].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[24].name.should.eql('Read users');
permissions[24].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[25].name.should.eql('Edit users');
permissions[25].should.be.AssignedToRoles(['Administrator', 'Editor']);
permissions[26].name.should.eql('Add users');
permissions[26].should.be.AssignedToRoles(['Administrator', 'Editor']);
permissions[27].name.should.eql('Delete users');
permissions[27].should.be.AssignedToRoles(['Administrator', 'Editor']);
permissions[26].name.should.eql('Browse users');
permissions[26].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[27].name.should.eql('Read users');
permissions[27].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[28].name.should.eql('Edit users');
permissions[28].should.be.AssignedToRoles(['Administrator', 'Editor']);
permissions[29].name.should.eql('Add users');
permissions[29].should.be.AssignedToRoles(['Administrator', 'Editor']);
permissions[30].name.should.eql('Delete users');
permissions[30].should.be.AssignedToRoles(['Administrator', 'Editor']);
// Roles
permissions[28].name.should.eql('Assign a role');
permissions[28].should.be.AssignedToRoles(['Administrator', 'Editor']);
permissions[29].name.should.eql('Browse roles');
permissions[29].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[31].name.should.eql('Assign a role');
permissions[31].should.be.AssignedToRoles(['Administrator', 'Editor']);
permissions[32].name.should.eql('Browse roles');
permissions[32].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
// Clients
permissions[30].name.should.eql('Browse clients');
permissions[30].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[31].name.should.eql('Read clients');
permissions[31].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[32].name.should.eql('Edit clients');
permissions[32].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[33].name.should.eql('Add clients');
permissions[33].name.should.eql('Browse clients');
permissions[33].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[34].name.should.eql('Delete clients');
permissions[34].name.should.eql('Read clients');
permissions[34].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[35].name.should.eql('Edit clients');
permissions[35].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[36].name.should.eql('Add clients');
permissions[36].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[37].name.should.eql('Delete clients');
permissions[37].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
// Subscribers
permissions[35].name.should.eql('Browse subscribers');
permissions[35].should.be.AssignedToRoles(['Administrator']);
permissions[36].name.should.eql('Read subscribers');
permissions[36].should.be.AssignedToRoles(['Administrator']);
permissions[37].name.should.eql('Edit subscribers');
permissions[37].should.be.AssignedToRoles(['Administrator']);
permissions[38].name.should.eql('Add subscribers');
permissions[38].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[39].name.should.eql('Delete subscribers');
permissions[38].name.should.eql('Browse subscribers');
permissions[38].should.be.AssignedToRoles(['Administrator']);
permissions[39].name.should.eql('Read subscribers');
permissions[39].should.be.AssignedToRoles(['Administrator']);
permissions[40].name.should.eql('Edit subscribers');
permissions[40].should.be.AssignedToRoles(['Administrator']);
permissions[41].name.should.eql('Add subscribers');
permissions[41].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
permissions[42].name.should.eql('Delete subscribers');
permissions[42].should.be.AssignedToRoles(['Administrator']);
});
describe('Populate', function () {
@ -206,7 +212,7 @@ describe('Database Migration (special functions)', function () {
result.roles.at(3).get('name').should.eql('Owner');
// Permissions
result.permissions.length.should.eql(40);
result.permissions.length.should.eql(43);
result.permissions.toJSON().should.be.CompletePermissions();
done();

View File

@ -204,7 +204,7 @@ describe('Config', function () {
config.storage.should.have.property('s3', {});
});
it('should allow setting a custom active storage as object', function () {
it('should use default theme adapter when passing an object', function () {
var storagePath = path.join(config.paths.contentPath, 'storage', 's3');
configUtils.set({
@ -217,7 +217,7 @@ describe('Config', function () {
config.storage.should.have.property('active', {
images: 'local-file-store',
themes: 's3'
themes: 'local-file-store'
});
});
@ -228,14 +228,14 @@ describe('Config', function () {
storage: {
active: {
images: 's2',
themes: 's3'
themes: 'local-file-store'
}
}
});
config.storage.should.have.property('active', {
images: 's2',
themes: 's3'
themes: 'local-file-store'
});
});
});

View File

@ -1,24 +1,26 @@
var should = require('should'),
sinon = require('sinon'),
_ = require('lodash'),
moment = require('moment'),
rewire = require('rewire'),
var should = require('should'),
sinon = require('sinon'),
_ = require('lodash'),
moment = require('moment'),
rewire = require('rewire'),
Promise = require('bluebird'),
// Stuff we are testing
configUtils = require('../utils/configUtils'),
models = require('../../server/models'),
api = require('../../server/api'),
configUtils = require('../utils/configUtils'),
models = require('../../server/models'),
api = require('../../server/api'),
permissions = require('../../server/permissions'),
notifications = require('../../server/api/notifications'),
versioning = require('../../server/data/schema/versioning'),
update = rewire('../../server/data/migration/fixtures/update'),
populate = rewire('../../server/data/migration/fixtures/populate'),
fixtureUtils = require('../../server/data/migration/fixtures/utils'),
fixtures004 = require('../../server/data/migration/fixtures/004'),
fixtures005 = require('../../server/data/migration/fixtures/005'),
fixtures006 = require('../../server/data/migration/fixtures/006'),
versioning = require('../../server/data/schema/versioning'),
update = rewire('../../server/data/migration/fixtures/update'),
populate = rewire('../../server/data/migration/fixtures/populate'),
fixtureUtils = require('../../server/data/migration/fixtures/utils'),
fixtures004 = require('../../server/data/migration/fixtures/004'),
fixtures005 = require('../../server/data/migration/fixtures/005'),
fixtures006 = require('../../server/data/migration/fixtures/006'),
fixtures007 = require('../../server/data/migration/fixtures/007'),
sandbox = sinon.sandbox.create();
sandbox = sinon.sandbox.create();
describe('Fixtures', function () {
var loggerStub, transactionStub;
@ -72,7 +74,7 @@ describe('Fixtures', function () {
sequenceStub.returns(Promise.resolve([]));
update(tasks, loggerStub, {transacting:transactionStub}).then(function (result) {
update(tasks, loggerStub, {transacting: transactionStub}).then(function (result) {
should.exist(result);
loggerStub.info.calledOnce.should.be.true();
@ -490,7 +492,7 @@ describe('Fixtures', function () {
loggerStub.info.calledOnce.should.be.true();
// gets called because we're stubbing to return an empty array
loggerStub.warn.calledOnce.should.be.true();
sinon.assert.callOrder(loggerStub.info, postAllStub, postCollStub.mapThen, postObjStub.load);
sinon.assert.callOrder(loggerStub.info, postAllStub, postCollStub.mapThen, postObjStub.load);
done();
}).catch(done);
@ -932,7 +934,7 @@ describe('Fixtures', function () {
sequenceStub.returns(Promise.resolve([]));
update(tasks, loggerStub, {transacting:transactionStub}).then(function (result) {
update(tasks, loggerStub, {transacting: transactionStub}).then(function (result) {
should.exist(result);
loggerStub.info.calledOnce.should.be.true();
@ -1099,6 +1101,77 @@ describe('Fixtures', function () {
});
});
});
describe('Update to 007', function () {
it('should call all the 007 fixture upgrades', function (done) {
// Setup
// Create a new stub, this will replace sequence, so that db calls don't actually get run
var sequenceStub = sandbox.stub(),
sequenceReset = update.__set__('sequence', sequenceStub),
tasks = versioning.getUpdateFixturesTasks('007', loggerStub);
sequenceStub.returns(Promise.resolve([]));
update(tasks, loggerStub, {transacting: transactionStub}).then(function (result) {
should.exist(result);
loggerStub.info.calledOnce.should.be.true();
loggerStub.warn.called.should.be.false();
sequenceStub.calledOnce.should.be.true();
sequenceStub.firstCall.calledWith(sinon.match.array, sinon.match.object, loggerStub).should.be.true();
sequenceStub.firstCall.args[0].should.be.an.Array().with.lengthOf(1);
sequenceStub.firstCall.args[0][0].should.be.a.Function().with.property('name', 'addThemePermissions');
// Reset
sequenceReset();
done();
}).catch(done);
});
describe('Tasks:', function () {
it('should have tasks for 007', function () {
should.exist(fixtures007);
fixtures007.should.be.an.Array().with.lengthOf(1);
});
describe('01-addThemePermissions', function () {
var updateThemePermissions = fixtures007[0], addModelStub, relationResult, addRelationStub, modelResult;
before(function () {
modelResult = {expected: 1, done: 1};
addModelStub = sandbox.stub(fixtureUtils, 'addFixturesForModel')
.returns(Promise.resolve(modelResult));
relationResult = {expected: 1, done: 1};
addRelationStub = sandbox.stub(fixtureUtils, 'addFixturesForRelation')
.returns(Promise.resolve(relationResult));
sandbox.stub(permissions, 'init').returns(Promise.resolve());
});
it('ensure permissions get updates', function (done) {
updateThemePermissions({context: {internal: true}}, loggerStub)
.then(function () {
addModelStub.calledOnce.should.be.true();
addModelStub.calledWith(
fixtureUtils.findModelFixtures('Permission', {object_type: 'theme'})
).should.be.true();
addRelationStub.calledOnce.should.be.true();
addRelationStub.calledWith(
fixtureUtils.findPermissionRelationsForObject('theme')
).should.be.true();
permissions.init.calledOnce.should.eql(true);
done();
})
.catch(done);
});
});
});
});
});
describe('Populate fixtures', function () {
@ -1111,14 +1184,14 @@ describe('Fixtures', function () {
clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve()),
permsAddStub = sandbox.stub(models.Permission, 'add').returns(Promise.resolve()),
// Existence checks
// Existence checks
postOneStub = sandbox.stub(models.Post, 'findOne').returns(Promise.resolve()),
tagOneStub = sandbox.stub(models.Tag, 'findOne').returns(Promise.resolve()),
roleOneStub = sandbox.stub(models.Role, 'findOne').returns(Promise.resolve()),
clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve()),
permOneStub = sandbox.stub(models.Permission, 'findOne').returns(Promise.resolve()),
// Relations
// Relations
fromItem = {
related: sandbox.stub().returnsThis(),
findWhere: sandbox.stub().returns({})
@ -1130,7 +1203,7 @@ describe('Fixtures', function () {
postsAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(modelMethodStub)),
tagsAllStub = sandbox.stub(models.Tag, 'findAll').returns(Promise.resolve(modelMethodStub)),
// Create Owner
// Create Owner
userAddStub = sandbox.stub(models.User, 'add').returns(Promise.resolve({}));
roleOneStub.onCall(4).returns(Promise.resolve({id: 1}));
@ -1147,9 +1220,9 @@ describe('Fixtures', function () {
clientOneStub.calledThrice.should.be.true();
clientAddStub.calledThrice.should.be.true();
permOneStub.callCount.should.eql(40);
permOneStub.callCount.should.eql(43);
permsAddStub.called.should.be.true();
permsAddStub.callCount.should.eql(40);
permsAddStub.callCount.should.eql(43);
permsAllStub.calledOnce.should.be.true();
rolesAllStub.calledOnce.should.be.true();

View File

@ -31,9 +31,9 @@ var should = require('should'),
// both of which are required for migrations to work properly.
describe('DB version integrity', function () {
// Only these variables should need updating
var currentDbVersion = '006',
var currentDbVersion = '007',
currentSchemaHash = 'f63f41ac97b5665a30c899409bbf9a83',
currentFixturesHash = '56f781fa3bba0fdbf98da5f232ec9b11';
currentFixturesHash = '30b0a956b04e634e7f2cddcae8d2fd20';
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
// and the values above will need updating as confirmation

View File

@ -108,7 +108,7 @@ describe('server bootstrap', function () {
migration.update.execute.calledWith({
fromVersion: '006',
toVersion: '006',
toVersion: '007',
forceMigration: undefined
}).should.eql(true);

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -45,6 +45,7 @@
"fs-extra": "0.30.0",
"ghost-gql": "0.0.5",
"glob": "5.0.15",
"gscan": "0.0.9",
"html-to-text": "2.1.3",
"image-size": "0.5.0",
"intl": "1.2.4",