428 lines
18 KiB
JavaScript
428 lines
18 KiB
JavaScript
/* 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');
|
|
});
|
|
});
|
|
});
|