var path = require('path'), EventEmitter = require('events').EventEmitter, should = require('should'), sinon = require('sinon'), _ = require('lodash'), Promise = require('bluebird'), helpers = require('../../server/helpers'), 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'); i18n.init(); describe('Apps', function () { var sandbox, fakeApi; beforeEach(function () { sandbox = sinon.sandbox.create(); 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.spy(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.spy(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.spy(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); }); }); });