/* 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'), expect = require('expect.js'), Location = require('../location.js'), safe = require('safetydance'); describe('Apps', function () { const { domainSetup, cleanup, app, admin, user , domain } = common; before(domainSetup); after(cleanup); describe('checkForPortBindingConflict', function () { before(async function () { await apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, [{ hostPort: 40000, type: 'tcp', count: 100 }, { hostPort: 50000, type: 'udp', count: 1 }], app); }); after(async function () { await apps.del(app.id); }); it('throws on exact conflict', async function () { let [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 40000, type: 'tcp', count: 1 }], {})); expect(error.reason).to.equal(BoxError.CONFLICT); [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 50000, type: 'udp', count: 1 }], {})); expect(error.reason).to.equal(BoxError.CONFLICT); }); it('throws on range conflict', async function () { let [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 40080, type: 'tcp', count: 40 }], {})); expect(error.reason).to.equal(BoxError.CONFLICT); [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 49995, type: 'udp', count: 20 }], {})); expect(error.reason).to.equal(BoxError.CONFLICT); }); it('succeeds without conflict', async function () { await apps._checkForPortBindingConflict([{ hostPort: 39995, type: 'tcp', count: 2 }], {}); await apps._checkForPortBindingConflict([{ hostPort: 45000, type: 'tcp', count: 1 }], {}); await apps._checkForPortBindingConflict([{ hostPort: 49995, type: 'udp', count: 2 }], {}); await apps._checkForPortBindingConflict([{ hostPort: 50001, type: 'udp', count: 1 }], {}); }); }); describe('validateLocations', function () { it('does not allow reserved subdomain', async function () { const location = new Location('my', domain.domain, Location.TYPE_ALIAS); expect(await apps._validateLocations([location])).to.be.an(Error); }); it('does not allow unknown domain', async function () { const location = new Location('my2', domain.domain + 'x', Location.TYPE_PRIMARY); expect(await apps._validateLocations([location])).to.be.an(Error); }); it('allows valid locations', async function () { const location = new Location('my2', domain.domain, Location.TYPE_SECONDARY); expect(await apps._validateLocations([location])).to.be(null); }); }); describe('validatePortBindings', function () { it('does not allow invalid host port', function () { expect(apps._validatePorts({ PORT: -1 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 0 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 'text' }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 65536 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 470 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); }); it('does not allow ports not as part of manifest', function () { expect(apps._validatePorts({ PORT: 1567 }, { tcpPorts: { } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 1567 }, { tcpPorts: { port3: null } })).to.be.an(Error); }); it('does not allow reserved ports', function () { expect(apps._validatePorts({ PORT: 443 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 50000 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 51000 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); expect(apps._validatePorts({ PORT: 50100 }, { tcpPorts: { PORT: 5000 } })).to.be.an(Error); }); it('allows valid bindings', function () { expect(apps._validatePorts({ PORT: 1024 }, { tcpPorts: { PORT: 5000 } })).to.be(null); expect(apps._validatePorts({ 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, cpuQuota: 79, 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.cpuQuota).to.be(79); }); 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 proxyApp = require('./common.js').proxyApp; const newUpstreamUri = 'https://foobar.com:443'; before(async function () { await apps.add(proxyApp.id, proxyApp.appStoreId, proxyApp.manifest, proxyApp.subdomain, proxyApp.domain, proxyApp.portBindings, proxyApp); }); it('cannot set invalid upstream uri', async function () { const [error] = await safe(apps.setUpstreamUri(proxyApp, 'foo:bar:80', AuditSource.PLATFORM)); expect(error.reason).to.be(BoxError.BAD_FIELD); }); it('can set upstream uri', async function () { await apps.setUpstreamUri(proxyApp, newUpstreamUri, AuditSource.PLATFORM); const result = await apps.get(proxyApp.id); expect(result.upstreamUri).to.equal(newUpstreamUri); }); }); describe('configureApps', 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.configureApps(await apps.list(), { scheduleNow: false }, 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('restoreApps', 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.restoreApps(await apps.list(), { scheduleNow: false }, 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: Field value (13) is out of range'); 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'); }); }); });