mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Moved apps to /services/ & moved individual tests (#9187)
refs #9178 * Moved app handling code into services/apps - Apps is a service, that allows for the App lifecycle - /server/apps = contains internal apps - /server/services/apps = contains code for managing/handling app life cycle, providing the proxy, etc * Split apps service tests into separate files * Moved internal app tests into test folders - Problem: Not all the tests in apps were unit tests, yet they were treated like they were in Gruntfile.js - Unit tests now live in /test/unit/apps - Route tests now live in /test/functional/routes/apps - Gruntfile.js has been updated to match * Switch api.read usage for settingsCache * Add tests to cover the basic App lifecycle * Simplify some of the init logic
This commit is contained in:
parent
97beaf0c1b
commit
882a2361ee
27 changed files with 785 additions and 675 deletions
10
Gruntfile.js
10
Gruntfile.js
|
@ -149,16 +149,14 @@ var overrides = require('./core/server/overrides'),
|
|||
// #### All Unit tests
|
||||
unit: {
|
||||
src: [
|
||||
'core/test/unit/**/*_spec.js',
|
||||
'core/server/apps/**/tests/*_spec.js'
|
||||
'core/test/unit/**/*_spec.js'
|
||||
]
|
||||
},
|
||||
|
||||
// #### All Integration tests
|
||||
integration: {
|
||||
src: [
|
||||
'core/test/integration/**/*_spec.js',
|
||||
'core/test/integration/*_spec.js'
|
||||
'core/test/integration/**/*_spec.js'
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -187,8 +185,7 @@ var overrides = require('./core/server/overrides'),
|
|||
coverage: {
|
||||
// they can also have coverage generated for them & the order doesn't matter
|
||||
src: [
|
||||
'core/test/unit',
|
||||
'core/server/apps'
|
||||
'core/test/unit'
|
||||
],
|
||||
options: {
|
||||
mask: '**/*_spec.js',
|
||||
|
@ -200,7 +197,6 @@ var overrides = require('./core/server/overrides'),
|
|||
coverage_all: {
|
||||
src: [
|
||||
'core/test/integration',
|
||||
'core/server/apps',
|
||||
'core/test/functional',
|
||||
'core/test/unit'
|
||||
],
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
|
||||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
logging = require('../logging'),
|
||||
errors = require('../errors'),
|
||||
api = require('../api'),
|
||||
loader = require('./loader'),
|
||||
i18n = require('../i18n'),
|
||||
config = require('../config'),
|
||||
// Holds the available apps
|
||||
availableApps = {};
|
||||
|
||||
function getInstalledApps() {
|
||||
return api.settings.read({context: {internal: true}, key: 'installed_apps'}).then(function (response) {
|
||||
var installed = response.settings[0];
|
||||
|
||||
installed.value = installed.value || '[]';
|
||||
|
||||
try {
|
||||
installed = JSON.parse(installed.value);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
return installed.concat(config.get('apps:internal'));
|
||||
});
|
||||
}
|
||||
|
||||
function saveInstalledApps(installedApps) {
|
||||
return getInstalledApps().then(function (currentInstalledApps) {
|
||||
var updatedAppsInstalled = _.difference(_.uniq(installedApps.concat(currentInstalledApps)), config.get('apps:internal'));
|
||||
|
||||
return api.settings.edit({settings: [{key: 'installed_apps', value: updatedAppsInstalled}]}, {context: {internal: true}});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function () {
|
||||
var appsToLoad;
|
||||
|
||||
try {
|
||||
// We have to parse the value because it's a string
|
||||
api.settings.read({context: {internal: true}, key: 'active_apps'}).then(function (response) {
|
||||
var aApps = response.settings[0];
|
||||
|
||||
appsToLoad = JSON.parse(aApps.value) || [];
|
||||
|
||||
appsToLoad = appsToLoad.concat(config.get('apps:internal'));
|
||||
});
|
||||
} catch (err) {
|
||||
logging.error(new errors.GhostError({
|
||||
err: err,
|
||||
context: i18n.t('errors.apps.failedToParseActiveAppsSettings.context'),
|
||||
help: i18n.t('errors.apps.failedToParseActiveAppsSettings.help')
|
||||
}));
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Grab all installed apps, install any not already installed that are in appsToLoad.
|
||||
return getInstalledApps().then(function (installedApps) {
|
||||
var loadedApps = {},
|
||||
recordLoadedApp = function (name, loadedApp) {
|
||||
// After loading the app, add it to our hash of loaded apps
|
||||
loadedApps[name] = loadedApp;
|
||||
|
||||
return Promise.resolve(loadedApp);
|
||||
},
|
||||
loadPromises = _.map(appsToLoad, function (app) {
|
||||
// If already installed, just activate the app
|
||||
if (_.includes(installedApps, app)) {
|
||||
return loader.activateAppByName(app).then(function (loadedApp) {
|
||||
return recordLoadedApp(app, loadedApp);
|
||||
});
|
||||
}
|
||||
|
||||
// Install, then activate the app
|
||||
return loader.installAppByName(app).then(function () {
|
||||
return loader.activateAppByName(app);
|
||||
}).then(function (loadedApp) {
|
||||
return recordLoadedApp(app, loadedApp);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(loadPromises).then(function () {
|
||||
// Save our installed apps to settings
|
||||
return saveInstalledApps(_.keys(loadedApps));
|
||||
}).then(function () {
|
||||
// Extend the loadedApps onto the available apps
|
||||
_.extend(availableApps, loadedApps);
|
||||
}).catch(function (err) {
|
||||
logging.error(new errors.GhostError({
|
||||
err: err,
|
||||
context: i18n.t('errors.apps.appWillNotBeLoaded.error'),
|
||||
help: i18n.t('errors.apps.appWillNotBeLoaded.help')
|
||||
}));
|
||||
});
|
||||
});
|
||||
},
|
||||
availableApps: availableApps
|
||||
};
|
|
@ -14,22 +14,23 @@ require('./overrides');
|
|||
|
||||
// Module dependencies
|
||||
var debug = require('ghost-ignition').debug('boot:init'),
|
||||
// Config should be first require, as it triggers the initial load of the config files
|
||||
config = require('./config'),
|
||||
Promise = require('bluebird'),
|
||||
i18n = require('./i18n'),
|
||||
models = require('./models'),
|
||||
permissions = require('./permissions'),
|
||||
apps = require('./apps'),
|
||||
auth = require('./auth'),
|
||||
dbHealth = require('./data/db/health'),
|
||||
xmlrpc = require('./services/xmlrpc'),
|
||||
slack = require('./services/slack'),
|
||||
GhostServer = require('./ghost-server'),
|
||||
scheduling = require('./adapters/scheduling'),
|
||||
settings = require('./settings'),
|
||||
themes = require('./themes'),
|
||||
utils = require('./utils');
|
||||
utils = require('./utils'),
|
||||
|
||||
// Services that need initialisation
|
||||
apps = require('./services/apps'),
|
||||
xmlrpc = require('./services/xmlrpc'),
|
||||
slack = require('./services/slack');
|
||||
|
||||
// ## Initialise Ghost
|
||||
function init() {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
var _ = require('lodash'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
76
core/server/services/apps/index.js
Normal file
76
core/server/services/apps/index.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
var debug = require('ghost-ignition').debug('services:apps'),
|
||||
_ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
logging = require('../../logging'),
|
||||
errors = require('../../errors'),
|
||||
api = require('../../api'),
|
||||
i18n = require('../../i18n'),
|
||||
config = require('../../config'),
|
||||
settingsCache = require('../../settings/cache'),
|
||||
loader = require('./loader'),
|
||||
// Internal APps are in config
|
||||
internalApps = config.get('apps:internal'),
|
||||
// Holds the available apps
|
||||
availableApps = {};
|
||||
|
||||
function recordLoadedApp(name, loadedApp) {
|
||||
// After loading the app, add it to our hash of loaded apps
|
||||
availableApps[name] = loadedApp;
|
||||
return loadedApp;
|
||||
}
|
||||
|
||||
function saveInstalledApps(installedApps) {
|
||||
debug('saving begin');
|
||||
var currentInstalledApps = settingsCache.get('installed_apps'),
|
||||
// Never save internal apps
|
||||
updatedAppsInstalled = _.difference(_.uniq(installedApps.concat(currentInstalledApps)), internalApps);
|
||||
|
||||
if (_.difference(updatedAppsInstalled, currentInstalledApps).length === 0) {
|
||||
debug('saving unneeded');
|
||||
return new Promise.resolve();
|
||||
}
|
||||
|
||||
debug('saving settings');
|
||||
return api.settings.edit({settings: [{key: 'installed_apps', value: updatedAppsInstalled}]}, {context: {internal: true}});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function () {
|
||||
debug('init begin');
|
||||
var activeApps = settingsCache.get('active_apps'),
|
||||
installedApps = settingsCache.get('installed_apps'),
|
||||
// Load means either activate, or install and activate
|
||||
// We load all Active Apps, and all Internal Apps
|
||||
appsToLoad = activeApps.concat(internalApps);
|
||||
|
||||
function loadApp(appName) {
|
||||
// If internal or already installed, the app only needs activating
|
||||
if (_.includes(internalApps, appName) || _.includes(installedApps, appName)) {
|
||||
return loader.activateAppByName(appName).then(function (loadedApp) {
|
||||
return recordLoadedApp(appName, loadedApp);
|
||||
});
|
||||
}
|
||||
|
||||
// Else first install, then activate the app
|
||||
return loader.installAppByName(appName).then(function () {
|
||||
return loader.activateAppByName(appName);
|
||||
}).then(function (loadedApp) {
|
||||
return recordLoadedApp(appName, loadedApp);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.map(appsToLoad, loadApp)
|
||||
.then(function () {
|
||||
// Save our installed apps to settings
|
||||
return saveInstalledApps(_.keys(availableApps));
|
||||
})
|
||||
.catch(function (err) {
|
||||
logging.error(new errors.GhostError({
|
||||
err: err,
|
||||
context: i18n.t('errors.apps.appWillNotBeLoaded.error'),
|
||||
help: i18n.t('errors.apps.appWillNotBeLoaded.help')
|
||||
}));
|
||||
});
|
||||
},
|
||||
availableApps: availableApps
|
||||
};
|
|
@ -1,13 +1,14 @@
|
|||
|
||||
var path = require('path'),
|
||||
_ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
config = require('../../config'),
|
||||
i18n = require('../../i18n'),
|
||||
|
||||
AppProxy = require('./proxy'),
|
||||
config = require('../config'),
|
||||
AppSandbox = require('./sandbox'),
|
||||
AppDependencies = require('./dependencies'),
|
||||
AppPermissions = require('./permissions'),
|
||||
i18n = require('../i18n'),
|
||||
|
||||
loader;
|
||||
|
||||
function isInternalApp(name) {
|
|
@ -1,7 +1,7 @@
|
|||
var fs = require('fs'),
|
||||
Promise = require('bluebird'),
|
||||
path = require('path'),
|
||||
parsePackageJson = require('../utils/packages').parsePackageJSON;
|
||||
parsePackageJson = require('../../utils/packages').parsePackageJSON;
|
||||
|
||||
function AppPermissions(appPath) {
|
||||
this.appPath = appPath;
|
|
@ -1,8 +1,8 @@
|
|||
var _ = require('lodash'),
|
||||
api = require('../api'),
|
||||
helpers = require('../helpers/register'),
|
||||
filters = require('../filters'),
|
||||
i18n = require('../i18n'),
|
||||
var _ = require('lodash'),
|
||||
api = require('../../api'),
|
||||
helpers = require('../../helpers/register'),
|
||||
filters = require('../../filters'),
|
||||
i18n = require('../../i18n'),
|
||||
generateProxyFunctions;
|
||||
|
||||
generateProxyFunctions = function (name, permissions, isInternal) {
|
|
@ -1,8 +1,7 @@
|
|||
|
||||
var path = require('path'),
|
||||
Module = require('module'),
|
||||
i18n = require('../i18n'),
|
||||
_ = require('lodash');
|
||||
var path = require('path'),
|
||||
Module = require('module'),
|
||||
i18n = require('../../i18n'),
|
||||
_ = require('lodash');
|
||||
|
||||
function AppSandbox(opts) {
|
||||
this.opts = _.defaults(opts || {}, AppSandbox.defaults);
|
|
@ -1,9 +1,9 @@
|
|||
var supertest = require('supertest'),
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
testUtils = require('../../../../test/utils'),
|
||||
labs = require('../../../utils/labs'),
|
||||
config = require('../../../config'),
|
||||
testUtils = require('../../../../utils'),
|
||||
labs = require('../../../../../server/utils/labs'),
|
||||
config = require('../../../../../server/config'),
|
||||
ghost = testUtils.startGhost,
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
var should = require('should'),
|
||||
|
||||
// Stuff we are testing
|
||||
ampComponentsHelper = require('../lib/helpers/amp_components');
|
||||
ampComponentsHelper = require('../../../../server/apps/amp/lib/helpers/amp_components');
|
||||
|
||||
describe('{{amp_components}} helper', function () {
|
||||
it('adds script tag for a gif', function () {
|
|
@ -3,7 +3,7 @@ var should = require('should'),
|
|||
configUtils = require('../../../../test/utils/configUtils'),
|
||||
|
||||
// Stuff we are testing
|
||||
ampContentHelper = rewire('../lib/helpers/amp_content');
|
||||
ampContentHelper = rewire('../../../../server/apps/amp/lib/helpers/amp_content');
|
||||
|
||||
// TODO: Amperize really needs to get stubbed, so we can test returning errors
|
||||
// properly and make this test faster!
|
|
@ -4,10 +4,10 @@ var should = require('should'),
|
|||
path = require('path'),
|
||||
Promise = require('bluebird'),
|
||||
|
||||
ampController = rewire('../lib/router'),
|
||||
errors = require('../../../errors'),
|
||||
configUtils = require('../../../../test/utils/configUtils'),
|
||||
themes = require('../../../themes'),
|
||||
ampController = rewire('../../../../server/apps/amp/lib/router'),
|
||||
errors = require('../../../../server/errors'),
|
||||
configUtils = require('../../../utils/configUtils'),
|
||||
themes = require('../../../../server/themes'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
var should = require('should'), // jshint ignore:line
|
||||
card = require('../cards/hr'),
|
||||
card = require('../../../../server/apps/default-cards/cards/hr'),
|
||||
SimpleDom = require('simple-dom'),
|
||||
opts;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
var should = require('should'), // jshint ignore:line
|
||||
card = require('../cards/html'),
|
||||
card = require('../../../../server/apps/default-cards/cards/html'),
|
||||
SimpleDom = require('simple-dom'),
|
||||
opts;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
var should = require('should'), // jshint ignore:line
|
||||
card = require('../cards/image'),
|
||||
card = require('../../../../server/apps/default-cards/cards/image'),
|
||||
SimpleDom = require('simple-dom'),
|
||||
opts;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
var should = require('should'), // jshint ignore:line
|
||||
card = require('../cards/markdown'),
|
||||
card = require('../../../../server/apps/default-cards/cards/markdown'),
|
||||
SimpleDom = require('simple-dom'),
|
||||
opts;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
var should = require('should'), // jshint ignore:line
|
||||
card = require('../atoms/soft-return'),
|
||||
card = require('../../../../server/apps/default-cards/atoms/soft-return'),
|
||||
SimpleDom = require('simple-dom'),
|
||||
opts;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
/*globals describe, beforeEach, afterEach, it*/
|
||||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
configUtils = require('../../../../test/utils/configUtils'),
|
||||
path = require('path'),
|
||||
themes = require('../../../themes'),
|
||||
privateController = require('../lib/router').controller,
|
||||
configUtils = require('../../../utils/configUtils'),
|
||||
themes = require('../../../../server/themes'),
|
||||
privateController = require('../../../../server/apps/private-blogging/lib/router').controller,
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
var should = require('should'),
|
||||
|
||||
// Stuff we are testing
|
||||
input_password = require('../lib/helpers/input_password');
|
||||
input_password = require('../../../../server/apps/private-blogging/lib/helpers/input_password');
|
||||
|
||||
describe('{{input_password}} helper', function () {
|
||||
it('has input_password helper', function () {
|
|
@ -3,9 +3,9 @@ var should = require('should'), // jshint ignore:line
|
|||
sinon = require('sinon'),
|
||||
crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
errors = require('../../../errors'),
|
||||
settingsCache = require('../../../settings/cache'),
|
||||
privateBlogging = require('../lib/middleware'),
|
||||
errors = require('../../../../server/errors'),
|
||||
settingsCache = require('../../../../server/settings/cache'),
|
||||
privateBlogging = require('../../../../server/apps/private-blogging/lib/middleware'),
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
function hash(password, salt) {
|
|
@ -1,526 +0,0 @@
|
|||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
path = require('path'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
_ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
helpers = require('../../server/helpers/register'),
|
||||
filters = require('../../server/filters'),
|
||||
i18n = require('../../server/i18n'),
|
||||
|
||||
// Stuff we are testing
|
||||
AppProxy = require('../../server/apps/proxy'),
|
||||
AppSandbox = require('../../server/apps/sandbox'),
|
||||
AppDependencies = require('../../server/apps/dependencies'),
|
||||
AppPermissions = require('../../server/apps/permissions'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
i18n.init();
|
||||
|
||||
describe('Apps', function () {
|
||||
var fakeApi;
|
||||
|
||||
beforeEach(function () {
|
||||
fakeApi = {
|
||||
posts: {
|
||||
browse: sandbox.stub(),
|
||||
read: sandbox.stub(),
|
||||
edit: sandbox.stub(),
|
||||
add: sandbox.stub(),
|
||||
destroy: sandbox.stub()
|
||||
},
|
||||
users: {
|
||||
browse: sandbox.stub(),
|
||||
read: sandbox.stub(),
|
||||
edit: sandbox.stub()
|
||||
},
|
||||
tags: {
|
||||
all: sandbox.stub()
|
||||
},
|
||||
notifications: {
|
||||
destroy: sandbox.stub(),
|
||||
add: sandbox.stub()
|
||||
},
|
||||
settings: {
|
||||
browse: sandbox.stub(),
|
||||
read: sandbox.stub(),
|
||||
add: sandbox.stub()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Proxy', function () {
|
||||
it('requires a name to be passed', function () {
|
||||
function makeWithoutName() {
|
||||
return new AppProxy({});
|
||||
}
|
||||
|
||||
makeWithoutName.should.throw('Must provide an app name for api context');
|
||||
});
|
||||
|
||||
it('requires permissions to be passed', function () {
|
||||
function makeWithoutPerms() {
|
||||
return new AppProxy({
|
||||
name: 'NoPerms'
|
||||
});
|
||||
}
|
||||
|
||||
makeWithoutPerms.should.throw('Must provide app permissions');
|
||||
});
|
||||
|
||||
it('creates a ghost proxy', function () {
|
||||
var appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
});
|
||||
|
||||
should.exist(appProxy.filters);
|
||||
should.exist(appProxy.filters.register);
|
||||
should.exist(appProxy.filters.deregister);
|
||||
|
||||
should.exist(appProxy.helpers);
|
||||
should.exist(appProxy.helpers.register);
|
||||
should.exist(appProxy.helpers.registerAsync);
|
||||
|
||||
should.exist(appProxy.api);
|
||||
|
||||
should.exist(appProxy.api.posts);
|
||||
should.exist(appProxy.api.posts.browse);
|
||||
should.exist(appProxy.api.posts.read);
|
||||
should.exist(appProxy.api.posts.edit);
|
||||
should.exist(appProxy.api.posts.add);
|
||||
should.exist(appProxy.api.posts.destroy);
|
||||
|
||||
should.not.exist(appProxy.api.users);
|
||||
|
||||
should.exist(appProxy.api.tags);
|
||||
should.exist(appProxy.api.tags.browse);
|
||||
|
||||
should.exist(appProxy.api.notifications);
|
||||
should.exist(appProxy.api.notifications.browse);
|
||||
should.exist(appProxy.api.notifications.add);
|
||||
should.exist(appProxy.api.notifications.destroy);
|
||||
|
||||
should.exist(appProxy.api.settings);
|
||||
should.exist(appProxy.api.settings.browse);
|
||||
should.exist(appProxy.api.settings.read);
|
||||
should.exist(appProxy.api.settings.edit);
|
||||
});
|
||||
|
||||
it('allows filter registration with permission', function (done) {
|
||||
var registerSpy = sandbox.spy(filters, 'registerFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['testFilter'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
fakePosts = [{id: 0}, {id: 1}],
|
||||
filterStub = sandbox.spy(function (val) {
|
||||
return val;
|
||||
});
|
||||
|
||||
appProxy.filters.register('testFilter', 5, filterStub);
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
|
||||
filterStub.called.should.equal(false);
|
||||
|
||||
filters.doFilter('testFilter', fakePosts)
|
||||
.then(function () {
|
||||
filterStub.called.should.equal(true);
|
||||
appProxy.filters.deregister('testFilter', 5, filterStub);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('does not allow filter registration without permission', function () {
|
||||
var registerSpy = sandbox.spy(filters, 'registerFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
filterStub = sandbox.stub().returns('test result');
|
||||
|
||||
function registerFilterWithoutPermission() {
|
||||
appProxy.filters.register('superSecretFilter', 5, filterStub);
|
||||
}
|
||||
|
||||
registerFilterWithoutPermission.should.throw('The App "TestApp" attempted to perform an action or access' +
|
||||
' a resource (filters.superSecretFilter) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(false);
|
||||
});
|
||||
|
||||
it('allows filter deregistration with permission', function (done) {
|
||||
var registerSpy = sandbox.spy(filters, 'deregisterFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostsRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
fakePosts = [{id: 0}, {id: 1}],
|
||||
filterStub = sandbox.stub().returns(fakePosts);
|
||||
|
||||
appProxy.filters.deregister('prePostsRender', 5, filterStub);
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
|
||||
filterStub.called.should.equal(false);
|
||||
|
||||
filters.doFilter('prePostsRender', fakePosts)
|
||||
.then(function () {
|
||||
filterStub.called.should.equal(false);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('does not allow filter deregistration without permission', function () {
|
||||
var registerSpy = sandbox.spy(filters, 'deregisterFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
filterStub = sandbox.stub().returns('test result');
|
||||
|
||||
function deregisterFilterWithoutPermission() {
|
||||
appProxy.filters.deregister('superSecretFilter', 5, filterStub);
|
||||
}
|
||||
|
||||
deregisterFilterWithoutPermission.should.throw('The App "TestApp" attempted to perform an action or ' +
|
||||
'access a resource (filters.superSecretFilter) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(false);
|
||||
});
|
||||
|
||||
it('allows helper registration with permission', function () {
|
||||
var registerSpy = sandbox.stub(helpers, 'registerThemeHelper'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
});
|
||||
|
||||
appProxy.helpers.register('myTestHelper', sandbox.stub().returns('test result'));
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
});
|
||||
|
||||
it('does not allow helper registration without permission', function () {
|
||||
var registerSpy = sandbox.stub(helpers, 'registerThemeHelper'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
});
|
||||
|
||||
function registerWithoutPermissions() {
|
||||
appProxy.helpers.register('otherHelper', sandbox.stub().returns('test result'));
|
||||
}
|
||||
|
||||
registerWithoutPermissions.should.throw('The App "TestApp" attempted to perform an action or access a ' +
|
||||
'resource (helpers.otherHelper) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(false);
|
||||
});
|
||||
|
||||
it('does allow INTERNAL app to register helper without permission', function () {
|
||||
var registerSpy = sandbox.stub(helpers, 'registerThemeHelper'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {},
|
||||
internal: true
|
||||
});
|
||||
|
||||
function registerWithoutPermissions() {
|
||||
appProxy.helpers.register('otherHelper', sandbox.stub().returns('test result'));
|
||||
}
|
||||
|
||||
registerWithoutPermissions.should.not.throw('The App "TestApp" attempted to perform an action or access a ' +
|
||||
'resource (helpers.otherHelper) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sandbox', function () {
|
||||
it('loads apps in a sandbox', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
appPath = path.resolve(__dirname, '..', 'utils', 'fixtures', 'app', 'good.js'),
|
||||
GoodApp,
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {}
|
||||
}),
|
||||
app;
|
||||
|
||||
GoodApp = appBox.loadApp(appPath);
|
||||
|
||||
should.exist(GoodApp);
|
||||
|
||||
app = new GoodApp(appProxy);
|
||||
|
||||
app.install(appProxy);
|
||||
|
||||
app.app.something.should.equal(42);
|
||||
app.app.util.util().should.equal(42);
|
||||
app.app.nested.other.should.equal(42);
|
||||
app.app.path.should.equal(appPath);
|
||||
});
|
||||
|
||||
it('does not allow apps to require blacklisted modules at top level', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badtop.js'),
|
||||
loadApp = function () {
|
||||
appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
loadApp.should.throw('Unsafe App require: knex');
|
||||
});
|
||||
|
||||
it('does not allow apps to require blacklisted modules at install', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badinstall.js'),
|
||||
BadApp,
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {}
|
||||
}),
|
||||
app,
|
||||
installApp = function () {
|
||||
app.install(appProxy);
|
||||
};
|
||||
|
||||
BadApp = appBox.loadApp(badAppPath);
|
||||
|
||||
app = new BadApp(appProxy);
|
||||
|
||||
installApp.should.throw('Unsafe App require: knex');
|
||||
});
|
||||
|
||||
it('does not allow apps to require blacklisted modules from other requires', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badrequire.js'),
|
||||
BadApp,
|
||||
loadApp = function () {
|
||||
BadApp = appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
loadApp.should.throw('Unsafe App require: knex');
|
||||
});
|
||||
|
||||
it('does not allow apps to require modules relatively outside their directory', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badoutside.js'),
|
||||
BadApp,
|
||||
loadApp = function () {
|
||||
BadApp = appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
loadApp.should.throw(/^Unsafe App require[\w\W]*example$/);
|
||||
});
|
||||
|
||||
it('does allow INTERNAL apps to require modules relatively outside their directory', function () {
|
||||
var appBox = new AppSandbox({internal: true}),
|
||||
badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badoutside.js'),
|
||||
InternalApp,
|
||||
loadApp = function () {
|
||||
InternalApp = appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
InternalApp = appBox.loadApp(badAppPath);
|
||||
|
||||
loadApp.should.not.throw(/^Unsafe App require[\w\W]*example$/);
|
||||
|
||||
InternalApp.should.be.a.Function();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependencies', function () {
|
||||
it('can install by package.json', function (done) {
|
||||
var deps = new AppDependencies(process.cwd()),
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
deps.spawnCommand = sandbox.stub().returns(fakeEmitter);
|
||||
|
||||
deps.install().then(function () {
|
||||
deps.spawnCommand.calledWith('npm').should.equal(true);
|
||||
done();
|
||||
}).catch(done);
|
||||
|
||||
_.delay(function () {
|
||||
fakeEmitter.emit('exit');
|
||||
}, 30);
|
||||
});
|
||||
it('does not install when no package.json', function (done) {
|
||||
var deps = new AppDependencies(__dirname),
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
deps.spawnCommand = sandbox.stub().returns(fakeEmitter);
|
||||
|
||||
deps.install().then(function () {
|
||||
deps.spawnCommand.called.should.equal(false);
|
||||
done();
|
||||
}).catch(done);
|
||||
|
||||
_.defer(function () {
|
||||
fakeEmitter.emit('exit');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Permissions', function () {
|
||||
var noGhostPackageJson = {
|
||||
name: 'myapp',
|
||||
version: '0.0.1',
|
||||
description: 'My example app',
|
||||
main: 'index.js',
|
||||
scripts: {
|
||||
test: 'echo \'Error: no test specified\' && exit 1'
|
||||
},
|
||||
author: 'Ghost',
|
||||
license: 'MIT',
|
||||
dependencies: {
|
||||
'ghost-app': '0.0.1'
|
||||
}
|
||||
},
|
||||
validGhostPackageJson = {
|
||||
name: 'myapp',
|
||||
version: '0.0.1',
|
||||
description: 'My example app',
|
||||
main: 'index.js',
|
||||
scripts: {
|
||||
test: 'echo \'Error: no test specified\' && exit 1'
|
||||
},
|
||||
author: 'Ghost',
|
||||
license: 'MIT',
|
||||
dependencies: {
|
||||
'ghost-app': '0.0.1'
|
||||
},
|
||||
ghost: {
|
||||
permissions: {
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete'],
|
||||
users: ['browse', 'read', 'edit', 'add', 'delete'],
|
||||
settings: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it('has default permissions to read and browse posts', function () {
|
||||
should.exist(AppPermissions.DefaultPermissions);
|
||||
|
||||
should.exist(AppPermissions.DefaultPermissions.posts);
|
||||
|
||||
AppPermissions.DefaultPermissions.posts.should.containEql('browse');
|
||||
AppPermissions.DefaultPermissions.posts.should.containEql('read');
|
||||
|
||||
// Make it hurt to add more so additional checks are added here
|
||||
_.keys(AppPermissions.DefaultPermissions).length.should.equal(1);
|
||||
});
|
||||
it('uses default permissions if no package.json', function (done) {
|
||||
var perms = new AppPermissions('test');
|
||||
|
||||
// No package.json in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(false));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
should.exist(readPerms);
|
||||
|
||||
readPerms.should.equal(AppPermissions.DefaultPermissions);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
it('uses default permissions if no ghost object in package.json', function (done) {
|
||||
var perms = new AppPermissions('test'),
|
||||
noGhostPackageJsonContents = JSON.stringify(noGhostPackageJson, null, 2);
|
||||
|
||||
// package.json IS in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(true));
|
||||
// no ghost property on package
|
||||
sandbox.stub(perms, 'getPackageContents').returns(Promise.resolve(noGhostPackageJsonContents));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
should.exist(readPerms);
|
||||
|
||||
readPerms.should.equal(AppPermissions.DefaultPermissions);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
it('rejects when reading malformed package.json', function (done) {
|
||||
var perms = new AppPermissions('test');
|
||||
|
||||
// package.json IS in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(true));
|
||||
// malformed JSON on package
|
||||
sandbox.stub(perms, 'getPackageContents').returns(Promise.reject(new Error('package.json file is malformed')));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
/*jshint unused:false*/
|
||||
done(new Error('should not resolve'));
|
||||
}).catch(function (err) {
|
||||
err.message.should.equal('package.json file is malformed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('reads from package.json in root of app directory', function (done) {
|
||||
var perms = new AppPermissions('test'),
|
||||
validGhostPackageJsonContents = validGhostPackageJson;
|
||||
|
||||
// package.json IS in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(true));
|
||||
// valid ghost property on package
|
||||
sandbox.stub(perms, 'getPackageContents').returns(Promise.resolve(validGhostPackageJsonContents));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
should.exist(readPerms);
|
||||
|
||||
readPerms.should.not.equal(AppPermissions.DefaultPermissions);
|
||||
|
||||
should.exist(readPerms.posts);
|
||||
readPerms.posts.length.should.equal(5);
|
||||
|
||||
should.exist(readPerms.users);
|
||||
readPerms.users.length.should.equal(5);
|
||||
|
||||
should.exist(readPerms.settings);
|
||||
readPerms.settings.length.should.equal(5);
|
||||
|
||||
_.keys(readPerms).length.should.equal(3);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
48
core/test/unit/services/apps/dependencies_spec.js
Normal file
48
core/test/unit/services/apps/dependencies_spec.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
var should = require('should'), // jshint ignore:line
|
||||
sinon = require('sinon'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
_ = require('lodash'),
|
||||
|
||||
// Stuff we are testing
|
||||
AppDependencies = require('../../../../server/services/apps/dependencies'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('Apps', function () {
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Dependencies', function () {
|
||||
it('can install by package.json', function (done) {
|
||||
var deps = new AppDependencies(process.cwd()),
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
deps.spawnCommand = sandbox.stub().returns(fakeEmitter);
|
||||
|
||||
deps.install().then(function () {
|
||||
deps.spawnCommand.calledWith('npm').should.equal(true);
|
||||
done();
|
||||
}).catch(done);
|
||||
|
||||
_.delay(function () {
|
||||
fakeEmitter.emit('exit');
|
||||
}, 30);
|
||||
});
|
||||
it('does not install when no package.json', function (done) {
|
||||
var deps = new AppDependencies(__dirname),
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
deps.spawnCommand = sandbox.stub().returns(fakeEmitter);
|
||||
|
||||
deps.install().then(function () {
|
||||
deps.spawnCommand.called.should.equal(false);
|
||||
done();
|
||||
}).catch(done);
|
||||
|
||||
_.defer(function () {
|
||||
fakeEmitter.emit('exit');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
100
core/test/unit/services/apps/lifecycle_spec.js
Normal file
100
core/test/unit/services/apps/lifecycle_spec.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
|
||||
settingsCache = require('../../../../server/settings/cache'),
|
||||
api = require('../../../../server/api'),
|
||||
|
||||
// Stuff we are testing
|
||||
AppLoader = require('../../../../server/services/apps/loader'),
|
||||
AppIndex = require('../../../../server/services/apps'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('Apps', function () {
|
||||
var settingsCacheStub,
|
||||
settingsEditStub,
|
||||
loaderActivateStub,
|
||||
loaderInstallStub;
|
||||
|
||||
beforeEach(function () {
|
||||
settingsCacheStub = sandbox.stub(settingsCache, 'get');
|
||||
settingsEditStub = sandbox.stub(api.settings, 'edit');
|
||||
loaderActivateStub = sandbox.stub(AppLoader, 'activateAppByName', function (appName) {
|
||||
return new Promise.resolve(appName);
|
||||
});
|
||||
loaderInstallStub = sandbox.stub(AppLoader, 'installAppByName', function (appName) {
|
||||
return new Promise.resolve(appName);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('will activate, but not install, internal apps', function (done) {
|
||||
settingsCacheStub.withArgs('installed_apps').returns([]);
|
||||
settingsCacheStub.withArgs('active_apps').returns([]);
|
||||
|
||||
AppIndex
|
||||
.init()
|
||||
.then(function () {
|
||||
var availableApps = Object.keys(AppIndex.availableApps);
|
||||
|
||||
// This is all a bit weird... but check that internal apps aren't saved as installed apps
|
||||
// @TODO simplify so this is reduced
|
||||
settingsCacheStub.callCount.should.eql(3);
|
||||
settingsEditStub.callCount.should.eql(0);
|
||||
|
||||
// Test that activate is called 4 times, and install 0 time
|
||||
loaderActivateStub.callCount.should.eql(4);
|
||||
loaderInstallStub.callCount.should.eql(0);
|
||||
|
||||
// Test that the 4 internal apps are loaded as expected
|
||||
availableApps.should.be.an.Array().with.lengthOf(4);
|
||||
availableApps.should.containEql('amp');
|
||||
availableApps.should.containEql('default-cards');
|
||||
availableApps.should.containEql('private-blogging');
|
||||
availableApps.should.containEql('subscribers');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('will activate & install custom apps as needed', function (done) {
|
||||
settingsCacheStub.withArgs('installed_apps').returns(['testA']);
|
||||
settingsCacheStub.withArgs('active_apps').returns(['testA', 'testB']);
|
||||
|
||||
AppIndex
|
||||
.init()
|
||||
.then(function () {
|
||||
var availableApps = Object.keys(AppIndex.availableApps);
|
||||
|
||||
// This is all a bit weird... but check that internal apps aren't saved as installed apps
|
||||
// @TODO simplify so this is reduced
|
||||
settingsCacheStub.callCount.should.eql(3);
|
||||
settingsEditStub.callCount.should.eql(1);
|
||||
should.exist(settingsEditStub.firstCall.args[0].settings);
|
||||
should.exist(settingsEditStub.firstCall.args[0].settings[0]);
|
||||
settingsEditStub.firstCall.args[0].settings[0].key.should.eql('installed_apps');
|
||||
settingsEditStub.firstCall.args[0].settings[0].value.should.eql(['testA', 'testB']);
|
||||
|
||||
// Test that activate is called 6 times, and install only 1 time
|
||||
loaderActivateStub.callCount.should.eql(6);
|
||||
loaderInstallStub.callCount.should.eql(1);
|
||||
|
||||
// Test that the 4 internal apps are loaded as expected
|
||||
availableApps.should.be.an.Array().with.lengthOf(6);
|
||||
availableApps.should.containEql('amp');
|
||||
availableApps.should.containEql('default-cards');
|
||||
availableApps.should.containEql('private-blogging');
|
||||
availableApps.should.containEql('subscribers');
|
||||
availableApps.should.containEql('testA');
|
||||
availableApps.should.containEql('testB');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
140
core/test/unit/services/apps/permissions_spec.js
Normal file
140
core/test/unit/services/apps/permissions_spec.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
_ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
|
||||
// Stuff we are testing
|
||||
AppPermissions = require('../../../../server/services/apps/permissions'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('Apps', function () {
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Permissions', function () {
|
||||
var noGhostPackageJson = {
|
||||
name: 'myapp',
|
||||
version: '0.0.1',
|
||||
description: 'My example app',
|
||||
main: 'index.js',
|
||||
scripts: {
|
||||
test: 'echo \'Error: no test specified\' && exit 1'
|
||||
},
|
||||
author: 'Ghost',
|
||||
license: 'MIT',
|
||||
dependencies: {
|
||||
'ghost-app': '0.0.1'
|
||||
}
|
||||
},
|
||||
validGhostPackageJson = {
|
||||
name: 'myapp',
|
||||
version: '0.0.1',
|
||||
description: 'My example app',
|
||||
main: 'index.js',
|
||||
scripts: {
|
||||
test: 'echo \'Error: no test specified\' && exit 1'
|
||||
},
|
||||
author: 'Ghost',
|
||||
license: 'MIT',
|
||||
dependencies: {
|
||||
'ghost-app': '0.0.1'
|
||||
},
|
||||
ghost: {
|
||||
permissions: {
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete'],
|
||||
users: ['browse', 'read', 'edit', 'add', 'delete'],
|
||||
settings: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it('has default permissions to read and browse posts', function () {
|
||||
should.exist(AppPermissions.DefaultPermissions);
|
||||
|
||||
should.exist(AppPermissions.DefaultPermissions.posts);
|
||||
|
||||
AppPermissions.DefaultPermissions.posts.should.containEql('browse');
|
||||
AppPermissions.DefaultPermissions.posts.should.containEql('read');
|
||||
|
||||
// Make it hurt to add more so additional checks are added here
|
||||
_.keys(AppPermissions.DefaultPermissions).length.should.equal(1);
|
||||
});
|
||||
it('uses default permissions if no package.json', function (done) {
|
||||
var perms = new AppPermissions('test');
|
||||
|
||||
// No package.json in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(false));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
should.exist(readPerms);
|
||||
|
||||
readPerms.should.equal(AppPermissions.DefaultPermissions);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
it('uses default permissions if no ghost object in package.json', function (done) {
|
||||
var perms = new AppPermissions('test'),
|
||||
noGhostPackageJsonContents = JSON.stringify(noGhostPackageJson, null, 2);
|
||||
|
||||
// package.json IS in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(true));
|
||||
// no ghost property on package
|
||||
sandbox.stub(perms, 'getPackageContents').returns(Promise.resolve(noGhostPackageJsonContents));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
should.exist(readPerms);
|
||||
|
||||
readPerms.should.equal(AppPermissions.DefaultPermissions);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
it('rejects when reading malformed package.json', function (done) {
|
||||
var perms = new AppPermissions('test');
|
||||
|
||||
// package.json IS in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(true));
|
||||
// malformed JSON on package
|
||||
sandbox.stub(perms, 'getPackageContents').returns(Promise.reject(new Error('package.json file is malformed')));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
/*jshint unused:false*/
|
||||
done(new Error('should not resolve'));
|
||||
}).catch(function (err) {
|
||||
err.message.should.equal('package.json file is malformed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('reads from package.json in root of app directory', function (done) {
|
||||
var perms = new AppPermissions('test'),
|
||||
validGhostPackageJsonContents = validGhostPackageJson;
|
||||
|
||||
// package.json IS in this directory
|
||||
sandbox.stub(perms, 'checkPackageContentsExists').returns(Promise.resolve(true));
|
||||
// valid ghost property on package
|
||||
sandbox.stub(perms, 'getPackageContents').returns(Promise.resolve(validGhostPackageJsonContents));
|
||||
|
||||
perms.read().then(function (readPerms) {
|
||||
should.exist(readPerms);
|
||||
|
||||
readPerms.should.not.equal(AppPermissions.DefaultPermissions);
|
||||
|
||||
should.exist(readPerms.posts);
|
||||
readPerms.posts.length.should.equal(5);
|
||||
|
||||
should.exist(readPerms.users);
|
||||
readPerms.users.length.should.equal(5);
|
||||
|
||||
should.exist(readPerms.settings);
|
||||
readPerms.settings.length.should.equal(5);
|
||||
|
||||
_.keys(readPerms).length.should.equal(3);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
265
core/test/unit/services/apps/proxy_spec.js
Normal file
265
core/test/unit/services/apps/proxy_spec.js
Normal file
|
@ -0,0 +1,265 @@
|
|||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
helpers = require('../../../../server/helpers/register'),
|
||||
filters = require('../../../../server/filters'),
|
||||
|
||||
// Stuff we are testing
|
||||
AppProxy = require('../../../../server/services/apps/proxy'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('Apps', function () {
|
||||
var fakeApi;
|
||||
|
||||
beforeEach(function () {
|
||||
fakeApi = {
|
||||
posts: {
|
||||
browse: sandbox.stub(),
|
||||
read: sandbox.stub(),
|
||||
edit: sandbox.stub(),
|
||||
add: sandbox.stub(),
|
||||
destroy: sandbox.stub()
|
||||
},
|
||||
users: {
|
||||
browse: sandbox.stub(),
|
||||
read: sandbox.stub(),
|
||||
edit: sandbox.stub()
|
||||
},
|
||||
tags: {
|
||||
all: sandbox.stub()
|
||||
},
|
||||
notifications: {
|
||||
destroy: sandbox.stub(),
|
||||
add: sandbox.stub()
|
||||
},
|
||||
settings: {
|
||||
browse: sandbox.stub(),
|
||||
read: sandbox.stub(),
|
||||
add: sandbox.stub()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Proxy', function () {
|
||||
it('requires a name to be passed', function () {
|
||||
function makeWithoutName() {
|
||||
return new AppProxy({});
|
||||
}
|
||||
|
||||
makeWithoutName.should.throw('Must provide an app name for api context');
|
||||
});
|
||||
|
||||
it('requires permissions to be passed', function () {
|
||||
function makeWithoutPerms() {
|
||||
return new AppProxy({
|
||||
name: 'NoPerms'
|
||||
});
|
||||
}
|
||||
|
||||
makeWithoutPerms.should.throw('Must provide app permissions');
|
||||
});
|
||||
|
||||
it('creates a ghost proxy', function () {
|
||||
var appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
});
|
||||
|
||||
should.exist(appProxy.filters);
|
||||
should.exist(appProxy.filters.register);
|
||||
should.exist(appProxy.filters.deregister);
|
||||
|
||||
should.exist(appProxy.helpers);
|
||||
should.exist(appProxy.helpers.register);
|
||||
should.exist(appProxy.helpers.registerAsync);
|
||||
|
||||
should.exist(appProxy.api);
|
||||
|
||||
should.exist(appProxy.api.posts);
|
||||
should.exist(appProxy.api.posts.browse);
|
||||
should.exist(appProxy.api.posts.read);
|
||||
should.exist(appProxy.api.posts.edit);
|
||||
should.exist(appProxy.api.posts.add);
|
||||
should.exist(appProxy.api.posts.destroy);
|
||||
|
||||
should.not.exist(appProxy.api.users);
|
||||
|
||||
should.exist(appProxy.api.tags);
|
||||
should.exist(appProxy.api.tags.browse);
|
||||
|
||||
should.exist(appProxy.api.notifications);
|
||||
should.exist(appProxy.api.notifications.browse);
|
||||
should.exist(appProxy.api.notifications.add);
|
||||
should.exist(appProxy.api.notifications.destroy);
|
||||
|
||||
should.exist(appProxy.api.settings);
|
||||
should.exist(appProxy.api.settings.browse);
|
||||
should.exist(appProxy.api.settings.read);
|
||||
should.exist(appProxy.api.settings.edit);
|
||||
});
|
||||
|
||||
it('allows filter registration with permission', function (done) {
|
||||
var registerSpy = sandbox.spy(filters, 'registerFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['testFilter'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
fakePosts = [{id: 0}, {id: 1}],
|
||||
filterStub = sandbox.spy(function (val) {
|
||||
return val;
|
||||
});
|
||||
|
||||
appProxy.filters.register('testFilter', 5, filterStub);
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
|
||||
filterStub.called.should.equal(false);
|
||||
|
||||
filters.doFilter('testFilter', fakePosts)
|
||||
.then(function () {
|
||||
filterStub.called.should.equal(true);
|
||||
appProxy.filters.deregister('testFilter', 5, filterStub);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('does not allow filter registration without permission', function () {
|
||||
var registerSpy = sandbox.spy(filters, 'registerFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
filterStub = sandbox.stub().returns('test result');
|
||||
|
||||
function registerFilterWithoutPermission() {
|
||||
appProxy.filters.register('superSecretFilter', 5, filterStub);
|
||||
}
|
||||
|
||||
registerFilterWithoutPermission.should.throw('The App "TestApp" attempted to perform an action or access' +
|
||||
' a resource (filters.superSecretFilter) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(false);
|
||||
});
|
||||
|
||||
it('allows filter deregistration with permission', function (done) {
|
||||
var registerSpy = sandbox.spy(filters, 'deregisterFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostsRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
fakePosts = [{id: 0}, {id: 1}],
|
||||
filterStub = sandbox.stub().returns(fakePosts);
|
||||
|
||||
appProxy.filters.deregister('prePostsRender', 5, filterStub);
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
|
||||
filterStub.called.should.equal(false);
|
||||
|
||||
filters.doFilter('prePostsRender', fakePosts)
|
||||
.then(function () {
|
||||
filterStub.called.should.equal(false);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('does not allow filter deregistration without permission', function () {
|
||||
var registerSpy = sandbox.spy(filters, 'deregisterFilter'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
}),
|
||||
filterStub = sandbox.stub().returns('test result');
|
||||
|
||||
function deregisterFilterWithoutPermission() {
|
||||
appProxy.filters.deregister('superSecretFilter', 5, filterStub);
|
||||
}
|
||||
|
||||
deregisterFilterWithoutPermission.should.throw('The App "TestApp" attempted to perform an action or ' +
|
||||
'access a resource (filters.superSecretFilter) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(false);
|
||||
});
|
||||
|
||||
it('allows helper registration with permission', function () {
|
||||
var registerSpy = sandbox.stub(helpers, 'registerThemeHelper'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
});
|
||||
|
||||
appProxy.helpers.register('myTestHelper', sandbox.stub().returns('test result'));
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
});
|
||||
|
||||
it('does not allow helper registration without permission', function () {
|
||||
var registerSpy = sandbox.stub(helpers, 'registerThemeHelper'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {
|
||||
filters: ['prePostRender'],
|
||||
helpers: ['myTestHelper'],
|
||||
posts: ['browse', 'read', 'edit', 'add', 'delete']
|
||||
}
|
||||
});
|
||||
|
||||
function registerWithoutPermissions() {
|
||||
appProxy.helpers.register('otherHelper', sandbox.stub().returns('test result'));
|
||||
}
|
||||
|
||||
registerWithoutPermissions.should.throw('The App "TestApp" attempted to perform an action or access a ' +
|
||||
'resource (helpers.otherHelper) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(false);
|
||||
});
|
||||
|
||||
it('does allow INTERNAL app to register helper without permission', function () {
|
||||
var registerSpy = sandbox.stub(helpers, 'registerThemeHelper'),
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {},
|
||||
internal: true
|
||||
});
|
||||
|
||||
function registerWithoutPermissions() {
|
||||
appProxy.helpers.register('otherHelper', sandbox.stub().returns('test result'));
|
||||
}
|
||||
|
||||
registerWithoutPermissions.should.not.throw('The App "TestApp" attempted to perform an action or access a ' +
|
||||
'resource (helpers.otherHelper) without permission.');
|
||||
|
||||
registerSpy.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
112
core/test/unit/services/apps/sandbox_spec.js
Normal file
112
core/test/unit/services/apps/sandbox_spec.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
path = require('path'),
|
||||
|
||||
// Stuff we are testing
|
||||
AppProxy = require('../../../../server/services/apps/proxy'),
|
||||
AppSandbox = require('../../../../server/services/apps/sandbox'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('Apps', function () {
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Sandbox', function () {
|
||||
function makeAppPath(fileName) {
|
||||
return path.resolve(__dirname, '..', '..', '..', 'utils', 'fixtures', 'app', fileName);
|
||||
}
|
||||
|
||||
it('loads apps in a sandbox', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
appPath = makeAppPath('good.js'),
|
||||
GoodApp,
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {}
|
||||
}),
|
||||
app;
|
||||
|
||||
GoodApp = appBox.loadApp(appPath);
|
||||
|
||||
should.exist(GoodApp);
|
||||
|
||||
app = new GoodApp(appProxy);
|
||||
|
||||
app.install(appProxy);
|
||||
|
||||
app.app.something.should.equal(42);
|
||||
app.app.util.util().should.equal(42);
|
||||
app.app.nested.other.should.equal(42);
|
||||
app.app.path.should.equal(appPath);
|
||||
});
|
||||
|
||||
it('does not allow apps to require blacklisted modules at top level', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = makeAppPath('badtop.js'),
|
||||
loadApp = function () {
|
||||
appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
loadApp.should.throw('Unsafe App require: knex');
|
||||
});
|
||||
|
||||
it('does not allow apps to require blacklisted modules at install', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = makeAppPath('badinstall.js'),
|
||||
BadApp,
|
||||
appProxy = new AppProxy({
|
||||
name: 'TestApp',
|
||||
permissions: {}
|
||||
}),
|
||||
app,
|
||||
installApp = function () {
|
||||
app.install(appProxy);
|
||||
};
|
||||
|
||||
BadApp = appBox.loadApp(badAppPath);
|
||||
|
||||
app = new BadApp(appProxy);
|
||||
|
||||
installApp.should.throw('Unsafe App require: knex');
|
||||
});
|
||||
|
||||
it('does not allow apps to require blacklisted modules from other requires', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = makeAppPath('badrequire.js'),
|
||||
BadApp,
|
||||
loadApp = function () {
|
||||
BadApp = appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
loadApp.should.throw('Unsafe App require: knex');
|
||||
});
|
||||
|
||||
it('does not allow apps to require modules relatively outside their directory', function () {
|
||||
var appBox = new AppSandbox(),
|
||||
badAppPath = makeAppPath('badoutside.js'),
|
||||
BadApp,
|
||||
loadApp = function () {
|
||||
BadApp = appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
loadApp.should.throw(/^Unsafe App require[\w\W]*example$/);
|
||||
});
|
||||
|
||||
it('does allow INTERNAL apps to require modules relatively outside their directory', function () {
|
||||
var appBox = new AppSandbox({internal: true}),
|
||||
badAppPath = makeAppPath('badoutside.js'),
|
||||
InternalApp,
|
||||
loadApp = function () {
|
||||
InternalApp = appBox.loadApp(badAppPath);
|
||||
};
|
||||
|
||||
InternalApp = appBox.loadApp(badAppPath);
|
||||
|
||||
loadApp.should.not.throw(/^Unsafe App require[\w\W]*example$/);
|
||||
|
||||
InternalApp.should.be.a.Function();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue