/* global it:false */ /* global describe:false */ /* global before:false */ /* global after:false */ 'use strict'; const apps = require('../apps.js'), AuditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), common = require('./common.js'), constants = require('../constants.js'), expect = require('expect.js'), safe = require('safetydance'); describe('Apps', function () { const { domainSetup, cleanup, app, admin, user } = common; before(domainSetup); after(cleanup); describe('validatePortBindings', function () { it('does not allow invalid host port', function () { expect(apps._validatePortBindings({ port: -1 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 0 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 'text' }, { tcpPorts: { port: 5000 } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 65536 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 470 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); }); it('does not allow ports not as part of manifest', function () { expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { port3: null } })).to.be.an(Error); }); it('does not allow reserved ports', function () { expect(apps._validatePortBindings({ port: 443 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 50000 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 51000 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); expect(apps._validatePortBindings({ port: 50100 }, { tcpPorts: { port: 5000 } })).to.be.an(Error); }); it('allows valid bindings', function () { expect(apps._validatePortBindings({ port: 1024 }, { tcpPorts: { port: 5000 } })).to.be(null); expect(apps._validatePortBindings({ port1: 4033, port2: 3242, port3: 1234 }, { tcpPorts: { port1: {}, port2: {}, port3: {} } })).to.be(null); }); }); describe('validateAccessRestriction', function () { it('allows null input', function () { expect(apps._validateAccessRestriction(null)).to.eql(null); }); it('does not allow wrong user type', function () { expect(apps._validateAccessRestriction({ users: {} })).to.be.an(Error); }); it('allows user input', function () { expect(apps._validateAccessRestriction({ users: [] })).to.eql(null); }); it('allows single user input', function () { expect(apps._validateAccessRestriction({ users: [ 'someuserid' ] })).to.eql(null); }); it('allows multi user input', function () { expect(apps._validateAccessRestriction({ users: [ 'someuserid', 'someuserid1', 'someuserid2', 'someuserid3' ] })).to.eql(null); }); }); describe('validateUpstreamUri', function () { it('does not allow empty URI', function () { expect(apps._validateUpstreamUri('')).to.be.an(Error); }); it('does not allow invalid URI scheme', function () { expect(apps._validateUpstreamUri('bla:blub')).to.be.an(Error); }); it('does not allow unsupported scheme', function () { expect(apps._validateUpstreamUri('ftp://foobar.com')).to.be.an(Error); }); it('does not allow trailing URI paths ', function () { expect(apps._validateUpstreamUri('https://foobar.com/extra/path')).to.be.an(Error); }); it('allows IP', function () { expect(apps._validateUpstreamUri('http://1.2.3.4')).to.eql(null); }); it('allows IP with port', function () { expect(apps._validateUpstreamUri('http://1.2.3.4:80')).to.eql(null); }); it('allows domain', function () { expect(apps._validateUpstreamUri('https://www.cloudron.io')).to.eql(null); }); it('allows domain with port', function () { expect(apps._validateUpstreamUri('https://www.cloudron.io:443')).to.eql(null); }); }); describe('canAccess', function () { const someuser = { id: 'someuser', groupIds: [], role: 'user' }; const adminuser = { id: 'adminuser', groupIds: [ 'groupie' ], role: 'admin' }; it('returns true for unrestricted access', function () { expect(apps.canAccess({ accessRestriction: null }, someuser)).to.be(true); }); it('returns true for allowed user', function () { expect(apps.canAccess({ accessRestriction: { users: [ 'someuser' ] } }, someuser)).to.be(true); }); it('returns true for allowed user with multiple allowed', function () { expect(apps.canAccess({ accessRestriction: { users: [ 'foo', 'someuser', 'anotheruser' ] } }, someuser)).to.be(true); }); it('returns false for not allowed user', function () { expect(apps.canAccess({ accessRestriction: { users: [ 'foo' ] } }, someuser)).to.be(false); }); it('returns false for not allowed user with multiple allowed', function () { expect(apps.canAccess({ accessRestriction: { users: [ 'foo', 'anotheruser' ] } }, someuser)).to.be(false); }); it('returns false for no group or user', function () { expect(apps.canAccess({ accessRestriction: { users: [ ], groups: [ ] } }, someuser)).to.be(false); }); it('returns false for invalid group or user', function () { expect(apps.canAccess({ accessRestriction: { users: [ ], groups: [ 'nop' ] } }, someuser)).to.be(false); }); it('returns true for admin user', function () { expect(apps.canAccess({ accessRestriction: { users: [ ], groups: [ 'nop' ] } }, adminuser)).to.be(true); }); }); describe('isOperator', function () { const someuser = { id: 'someuser', groupIds: [], role: 'user' }; const adminuser = { id: 'adminuser', groupIds: [ 'groupie' ], role: 'admin' }; it('returns false for unrestricted access', function () { expect(apps.isOperator({ operators: null }, someuser)).to.be(false); }); it('returns true for allowed user', function () { expect(apps.isOperator({ operators: { users: [ 'someuser' ] } }, someuser)).to.be(true); }); it('returns true for allowed user with multiple allowed', function () { expect(apps.isOperator({ operators: { users: [ 'foo', 'someuser', 'anotheruser' ] } }, someuser)).to.be(true); }); it('returns false for not allowed user', function () { expect(apps.isOperator({ operators: { users: [ 'foo' ] } }, someuser)).to.be(false); }); it('returns false for not allowed user with multiple allowed', function () { expect(apps.isOperator({ operators: { users: [ 'foo', 'anotheruser' ] } }, someuser)).to.be(false); }); it('returns false for no group or user', function () { expect(apps.isOperator({ operators: { users: [ ], groups: [ ] } }, someuser)).to.be(false); }); it('returns false for invalid group or user', function () { expect(apps.isOperator({ operators: { users: [ ], groups: [ 'nop' ] } }, someuser)).to.be(false); }); it('returns true for admin user', function () { expect(apps.isOperator({ operators: { users: [ ], groups: [ 'nop' ] } }, adminuser)).to.be(true); }); }); describe('accessLevel', function () { const someuser = { id: 'someuser', groupIds: [ 'ops' ], role: 'user' }; const adminuser = { id: 'adminuser', groupIds: [ 'groupie' ], role: 'admin' }; it('return user for normal user', function () { expect(apps.accessLevel({ accessRestriction: null, operators: null }, someuser)).to.be('user'); expect(apps.accessLevel({ accessRestriction: null, operators: { users: [ ], groups: [ 'groupie' ] } }, someuser)).to.be('user'); }); it('returns operator for operator user', function () { expect(apps.accessLevel({ accessRestriction: null, operators: { users: [ 'someuser' ], groups: [ 'groupie' ] } }, someuser)).to.be('operator'); expect(apps.accessLevel({ accessRestriction: null, operators: { users: [], groups: [ 'ops' ] } }, someuser)).to.be('operator'); }); it('returns admin for admin user', function () { expect(apps.accessLevel({ accessRestriction: null, operators: null }, adminuser)).to.be('admin'); expect(apps.accessLevel({ accessRestriction: null, operators: { users: [], groups: [] } }, adminuser)).to.be('admin'); }); }); describe('crud', function () { it('cannot get invalid app', async function () { const result = await apps.get('nope'); expect(result).to.be(null); }); it('can add app', async function () { await apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, app.portBindings, app); }); it('cannot add with same app id', async function () { const [error] = await safe(apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, app.portBindings, app)); expect(error.reason).to.be(BoxError.ALREADY_EXISTS); }); it('cannot add with same app id', async function () { const [error] = await safe(apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, app.portBindings, app)); expect(error.reason).to.be(BoxError.ALREADY_EXISTS); }); it('can get app', async function () { const result = await apps.get(app.id); expect(result.manifest).to.eql(app.manifest); expect(result.portBindings).to.eql({}); expect(result.subdomain).to.eql(app.subdomain); }); it('can list apps', async function () { const result = await apps.list(); expect(result.length).to.be(1); expect(result[0].manifest).to.eql(app.manifest); expect(result[0].portBindings).to.eql({}); expect(result[0].subdomain).to.eql(app.subdomain); }); it('can listByUser', async function () { let result = await apps.listByUser(admin); expect(result.length).to.be(1); result = await apps.listByUser(user); expect(result.length).to.be(1); }); it('update succeeds', async function () { const data = { installationState: 'some-other-status', subdomain:'some-other-subdomain', domain: app.domain, // needs to be set whenever subdomain is set manifest: Object.assign({}, app.manifest, { version: '0.2.0' }), accessRestriction: '', memoryLimit: 1337, cpuShares: 102, operators: { users: [ 'someid' ] } }; await apps.update(app.id, data); const newApp = await apps.get(app.id); expect(newApp.installationState).to.be('some-other-status'); expect(newApp.subdomain).to.be('some-other-subdomain'); expect(newApp.manifest.version).to.be('0.2.0'); expect(newApp.accessRestriction).to.be(''); expect(newApp.memoryLimit).to.be(1337); expect(newApp.cpuShares).to.be(102); }); it('update of nonexisting app fails', async function () { const [error] = await safe(apps.update('random', { installationState: app.installationState, subdomain: app.subdomain })); expect(error.reason).to.be(BoxError.NOT_FOUND); }); it('delete succeeds', async function () { await apps.del(app.id); }); it('cannot delete previously delete record', async function () { const [error] = await safe(apps.del(app.id)); expect(error.reason).to.be(BoxError.NOT_FOUND); }); }); describe('setHealth', function () { before(async function () { await apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, app.portBindings, app); }); it('can set app as healthy', async function () { const result = await apps.get(app.id); expect(result.health).to.be(null); await apps.setHealth(app.id, apps.HEALTH_HEALTHY, new Date()); }); it('did set app as healthy', async function () { const result = await apps.get(app.id); expect(result.health).to.be(apps.HEALTH_HEALTHY); }); it('cannot set health of unknown app', async function () { const [error] = await safe(apps.setHealth('randomId', apps.HEALTH_HEALTHY, new Date())); expect(error.reason).to.be(BoxError.NOT_FOUND); }); }); describe('proxy app', function () { const app = require('./common.js').proxyApp; const newUpstreamUri = 'https://foobar.com:443'; before(async function () { await apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, app.portBindings, app); }); it('cannot set invalid upstream uri', async function () { const [error] = await safe(apps.setUpstreamUri(app, 'foo:bar:80', AuditSource.PLATFORM)); expect(error.reason).to.be(BoxError.BAD_FIELD); }); it('can set upstream uri', async function () { await apps.setUpstreamUri(app, newUpstreamUri, AuditSource.PLATFORM); const result = await apps.get(app.id); expect(result.upstreamUri).to.equal(newUpstreamUri); }); }); describe('configureInstalledApps', function () { const app1 = Object.assign({}, app, { id: 'id1', installationState: apps.ISTATE_ERROR, subdomain: 'loc1' }); const app2 = Object.assign({}, app, { id: 'id2', installationState: apps.ISTATE_INSTALLED, subdomain: 'loc2' }); before(async function () { await apps.update(app.id, { installationState: apps.ISTATE_INSTALLED }); await apps.add(app1.id, app1.appStoreId, app1.manifest, app1.subdomain, app1.domain, app1.portBindings, app1); await apps.add(app2.id, app2.appStoreId, app2.manifest, app2.subdomain, app2.domain, app2.portBindings, app2); }); after(async function () { await apps.del(app1.id); await apps.del(app2.id); }); it('can mark apps for reconfigure', async function () { await apps.configureInstalledApps(AuditSource.PLATFORM); const result = await apps.list(); expect(result[0].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE); expect(result[1].installationState).to.be(apps.ISTATE_ERROR); expect(result[2].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE); }); }); describe('restoreInstalledApps', function () { const app1 = Object.assign({}, app, { id: 'id1', installationState: apps.ISTATE_ERROR, subdomain: 'loc1' }); const app2 = Object.assign({}, app, { id: 'id2', installationState: apps.ISTATE_INSTALLED, subdomain: 'loc2' }); before(async function () { await apps.update(app.id, { installationState: apps.ISTATE_INSTALLED }); await apps.add(app1.id, app1.appStoreId, app1.manifest, app1.subdomain, app1.domain, app1.portBindings, app1); await apps.add(app2.id, app2.appStoreId, app2.manifest, app2.subdomain, app2.domain, app2.portBindings, app2); }); after(async function () { await apps.del(app1.id); await apps.del(app2.id); }); it('can mark apps for reconfigure', async function () { await apps.restoreInstalledApps({}, AuditSource.PLATFORM); const result = await apps.list(); expect(result[0].installationState).to.be(apps.ISTATE_PENDING_INSTALL); expect(result[1].installationState).to.be(apps.ISTATE_ERROR); expect(result[2].installationState).to.be(apps.ISTATE_PENDING_INSTALL); }); }); describe('parseCrontab', function () { it('succeeds for null crontob', function () { expect(apps._parseCrontab(null)).to.eql([]); }); it('succeeds for empty crontob', function () { expect(apps._parseCrontab('')).to.eql([]); }); it('throws for bad crontab', function () { safe(() => apps._parseCrontab('# some comment\n*/1 * * * ')); // incomplete pattern expect(safe.error.message).to.be('Invalid cron configuration at line 2'); safe(() => apps._parseCrontab('* * * * 13 command')); // bad pattern expect(safe.error.message).to.be('Invalid cron pattern at line 1'); safe(() => apps._parseCrontab('*/1 * * *\t* ')); // no command expect(safe.error.message).to.be('Invalid cron configuration at line 1'); safe(() => apps._parseCrontab('@whatever /bin/false')); // invalid extension expect(safe.error.message).to.be('Unknown extension pattern at line 1'); }); it('succeeds for crontab', function () { const result = apps._parseCrontab( '# this is a custom cron job\n' + ' */1 * * *\t* echo "==> This is a custom cron task running every minute"\n\n' + '00 */1 * * * echo "==> This is a custom cron task running every hour" '); // trailing spaces are trimmed expect(result.length).to.be(2); expect(result[0].schedule).to.be('*/1 * * * *'); expect(result[0].command).to.be('echo "==> This is a custom cron task running every minute"'); expect(result[1].schedule).to.be('00 */1 * * *'); expect(result[1].command).to.be('echo "==> This is a custom cron task running every hour"'); }); it('succeeds for crontab (extensions)', function () { const result = apps._parseCrontab('@service /bin/service\n\n@weekly /bin/weekly'); expect(result.length).to.be(2); expect(result[0].schedule).to.be('@service'); expect(result[0].command).to.be('/bin/service'); expect(result[1].schedule).to.be('0 0 * * 0'); expect(result[1].command).to.be('/bin/weekly'); }); }); });