0
Fork 0
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:
Hannah Wolfe 2017-10-30 12:31:04 +00:00 committed by GitHub
parent 97beaf0c1b
commit 882a2361ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 785 additions and 675 deletions

View file

@ -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'
],

View file

@ -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
};

View file

@ -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() {

View file

@ -1,4 +1,3 @@
var _ = require('lodash'),
fs = require('fs'),
path = require('path'),

View 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
};

View file

@ -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) {

View file

@ -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;

View file

@ -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) {

View file

@ -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);

View file

@ -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();

View file

@ -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 () {

View file

@ -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!

View file

@ -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();

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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 () {

View file

@ -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) {

View file

@ -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);
});
});
});

View 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');
});
});
});
});

View 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);
});
});

View 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);
});
});
});

View 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);
});
});
});

View 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();
});
});
});