'use strict'; /* global it:false */ /* global describe:false */ /* global before:false */ let apps = require('../../apps.js'), async = require('async'), child_process = require('child_process'), clients = require('../../clients.js'), constants = require('../../constants.js'), crypto = require('crypto'), database = require('../../database.js'), docker = require('../../docker.js').connection, expect = require('expect.js'), fs = require('fs'), hat = require('../../hat.js'), hock = require('hock'), http = require('http'), ldap = require('../../ldap.js'), net = require('net'), nock = require('nock'), path = require('path'), paths = require('../../paths.js'), platform = require('../../platform.js'), safe = require('safetydance'), server = require('../../server.js'), settings = require('../../settings.js'), settingsdb = require('../../settingsdb.js'), superagent = require('superagent'), tokendb = require('../../tokendb.js'), url = require('url'); var SERVER_URL = 'http://localhost:' + constants.PORT; // Test image information var TEST_IMAGE_REPO = 'cloudron/test'; var TEST_IMAGE_TAG = '25.19.0'; var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG; const DOMAIN_0 = { domain: 'example-apps-test.com', adminFqdn: 'my.example-apps-test.com', zoneName: 'example-apps-test.com', config: {}, provider: 'noop', fallbackCertificate: null, tlsConfig: { provider: 'fallback' } }; var APP_STORE_ID = 'test', APP_ID; var APP_LOCATION = 'appslocation'; var APP_LOCATION_2 = 'appslocationtwo'; var APP_LOCATION_NEW = 'appslocationnew'; var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8')); APP_MANIFEST.dockerImage = TEST_IMAGE; const USERNAME = 'superadmin'; const PASSWORD = 'Foobar?1337'; const EMAIL ='admin@me.com'; const USER_1_APPSTORE_TOKEN = 'appstoretoken'; const USERNAME_1 = 'user'; const EMAIL_1 ='user@me.com'; var user_1_id = null; // authentication token var token = null; var token_1 = null; let KEY, CERT; let appstoreIconServer = hock.createHock({ throwOnUnmatched: false }); function checkAddons(appEntry, done) { async.retry({ times: 15, interval: 3000 }, function (callback) { // this was previously written with superagent but it was getting sporadic EPIPE var req = http.get({ hostname: 'localhost', port: appEntry.httpPort, path: '/check_addons?username=' + USERNAME + '&password=' + PASSWORD }); req.on('error', callback); req.on('response', function (res) { if (res.statusCode !== 200) return callback('app returned non-200 status : ' + res.statusCode); var d = ''; res.on('data', function (chunk) { d += chunk.toString('utf8'); }); res.on('end', function () { var body = JSON.parse(d); delete body.recvmail; // unclear why dovecot mail delivery won't work delete body.stdenv; // cannot access APP_ORIGIN delete body.email; // sieve will fail not sure why yet delete body.docker; // TODO fix this for some reason we cannot connect to the docker proxy on port 3003 for (var key in body) { if (body[key] !== 'OK') { console.log('Not done yet: ' + JSON.stringify(body)); return callback('Not done yet: ' + JSON.stringify(body)); } } callback(); }); }); req.end(); }, done); } function checkRedis(containerId, done) { var redisIp, exportedRedisPort; docker.getContainer(containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data).to.be.ok(); redisIp = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress'); expect(redisIp).to.be.ok(); exportedRedisPort = safe.query(data, 'NetworkSettings.Ports.6379/tcp'); expect(exportedRedisPort).to.be(null); done(); }); } function waitForTask(taskId, callback) { process.stdout.write('Waiting for task ' + taskId + ' .'); async.retry({ times: 50, interval: 4000 }, function (retryCallback) { superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId) .query({ access_token: token }) .end(function (error, result) { process.stdout.write('.'); if (!result || result.statusCode !== 200) return retryCallback(null, new Error('Bad result')); if (result.body.active) return retryCallback(new Error('Still active')); retryCallback(); }); }, function (error, result) { console.log(); callback(error || result); }); } function waitForSetup(done) { async.retry({ times: 5, interval: 4000 }, function (retryCallback) { superagent.get(SERVER_URL + '/api/v1/cloudron/status') .end(function (error, result) { if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result')); if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback(); retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body))); }); }, done); } function startBox(done) { console.log('Starting box code...'); child_process.execSync('openssl req -subj "/CN=*.' + DOMAIN_0.domain + '/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt'); KEY = fs.readFileSync('/tmp/server.key', 'utf8'); CERT = fs.readFileSync('/tmp/server.crt', 'utf8'); process.env.TEST_CREATE_INFRA = 1; safe.fs.unlinkSync(paths.INFRA_VERSION_FILE); async.series([ // first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds database.initialize, database._clear, server.start, ldap.start, settings._setApiServerOrigin.bind(null, 'http://localhost:6060'), function (callback) { superagent.post(SERVER_URL + '/api/v1/cloudron/setup') .send({ dnsConfig: DOMAIN_0 }) .end(function (error, result) { expect(result).to.be.ok(); expect(result.statusCode).to.eql(200); waitForSetup(callback); }); }, function (callback) { superagent.post(SERVER_URL + '/api/v1/cloudron/activate') .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) .end(function (error, result) { expect(result).to.be.ok(); expect(result.statusCode).to.eql(201); // stash for further use token = result.body.token; callback(); }); }, function (callback) { superagent.post(SERVER_URL + '/api/v1/users') .query({ access_token: token }) .send({ username: USERNAME_1, email: EMAIL_1, invite: false }) .end(function (err, res) { expect(res.statusCode).to.equal(201); user_1_id = res.body.id; token_1 = hat(8 * 32); // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: user_1_id, clientId: 'cid-sdk', expires: Date.now() + 1000000, scope: 'apps', name: '' }, callback); // cid-sdk means we don't need to send password }); }, function (callback) { appstoreIconServer .get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon') .replyWithFile(200, path.resolve(__dirname, '../../../assets/avatar.png')); var port = parseInt(url.parse(settings.apiServerOrigin()).port, 10); http.createServer(appstoreIconServer.handler).listen(port, callback); }, function (callback) { process.stdout.write('Waiting for platform to be ready...'); async.retry({ times: 500, interval: 1000 }, function (retryCallback) { if (platform._isReady) return retryCallback(); process.stdout.write('.'); retryCallback('Platform not ready yet'); }, function (error) { if (error) return callback(error); console.log(); console.log('Platform is ready'); callback(); }); } ], done); } function stopBox(done) { console.log('Stopping box code...'); delete process.env.TEST_CREATE_INFRA; child_process.execSync('docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f'); appstoreIconServer.done(); async.series([ database._clear, server.stop, ldap.stop, ], done); } describe('App API', function () { let taskId = ''; before(startBox); describe('Install', function () { it('app install fails - missing manifest', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.eql('appStoreId or manifest is required'); done(); }); }); it('app install fails - null manifest', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: null }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.eql('appStoreId or manifest is required'); done(); }); }); it('app install fails - bad manifest format', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: 'epic' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.eql('manifest must be an object'); done(); }); }); it('app install fails - empty appStoreId format', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: null, appStoreId: '' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.eql('appStoreId or manifest is required'); done(); }); }); it('app install fails - invalid json', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send('garbage') .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('app install fails - missing domain', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.eql('domain is required'); done(); }); }); it('app install fails - non-existing domain', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null, domain: 'doesnotexist.com' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.eql('No such domain'); done(); }); }); it('app install fails - invalid location type', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null, domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.eql('location is required'); done(); }); }); it('app install fails - reserved admin location', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null, domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.contain('my is reserved'); done(); }); }); it('app install fails - reserved smtp location', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: constants.SMTP_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.contain(constants.SMTP_LOCATION + ' is reserved'); done(); }); }); it('app install fails - portBindings must be object', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.contain('portBindings must be an object'); done(); }); }); it('app install fails - accessRestriction is required', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.contain('accessRestriction is required'); done(); }); }); it('app install fails - accessRestriction type is wrong', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(400); expect(res.body.message).to.contain('accessRestriction is required'); done(); }); }); it('app install fails for non admin', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token_1 }) .send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null, domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(403); done(); }); }); it('app install fails because manifest download fails', function (done) { var fake = nock(settings.apiServerOrigin()).get('/api/v1/apps/test').reply(404, {}); superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, domain: DOMAIN_0.domain, accessRestriction: { users: [ 'someuser' ], groups: [] } }) .end(function (err, res) { expect(res.statusCode).to.equal(404); expect(fake.isDone()).to.be.ok(); done(); }); }); it('app install fails due to purchase failure', function (done) { var fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST }); superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null }) .end(function (err, res) { expect(res.statusCode).to.equal(424); expect(fake1.isDone()).to.be.ok(); done(); }); }); it('app install succeeds with purchase', function (done) { var fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST }); var fake2 = nock(settings.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { }); settingsdb.set(settings.CLOUDRON_TOKEN_KEY, USER_1_APPSTORE_TOKEN, function (error) { if (error) return done(error); superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: { users: [ 'someuser' ], groups: [] } }) .end(function (err, res) { expect(res.statusCode).to.equal(202); expect(res.body.id).to.be.a('string'); APP_ID = res.body.id; expect(fake1.isDone()).to.be.ok(); expect(fake2.isDone()).to.be.ok(); taskId = res.body.taskId; done(); }); }); }); it('app install fails because of conflicting location', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null }) .end(function (err, res) { expect(res.statusCode).to.equal(409); done(); }); }); }); describe('get', function () { it('can get app status', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID) .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); expect(res.body.id).to.eql(APP_ID); expect(res.body.installationState).to.be.ok(); expect(res.body.mailboxName).to.be(APP_LOCATION + '.app'); done(); }); }); it('cannot get invalid app status', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/kubachi') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(404); done(); }); }); it('can get all apps', function (done) { superagent.get(SERVER_URL + '/api/v1/apps') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); expect(res.body.apps).to.be.an('array'); expect(res.body.apps[0].id).to.eql(APP_ID); expect(res.body.apps[0].installationState).to.be.ok(); done(); }); }); it('non admin cannot see the app due to accessRestriction', function (done) { superagent.get(SERVER_URL + '/api/v1/apps') .query({ access_token: token_1 }) .end(function (err, res) { expect(res.statusCode).to.equal(200); expect(res.body.apps).to.be.an('array'); expect(res.body.apps.length).to.equal(0); done(); }); }); }); describe('post installation', function () { let appResult, appEntry; it('task completed', function (done) { waitForTask(taskId, done); }); it('app is running', function (callback) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID) .query({ access_token: token }) .end(function (err, res) { if (res.statusCode !== 200) return callback(new Error('Response error')); if (res.body.installationState === apps.ISTATE_INSTALLED) { appResult = res.body; return callback(); } if (res.body.installationState === apps.ISTATE_ERROR) return callback(new Error('Install error')); callback(new Error('Unknown app state:' + res.body.installationState)); }); }); it('can get app', function (done) { apps.get(appResult.id, function (error, app) { expect(!error).to.be.ok(); expect(app).to.be.an('object'); appEntry = app; done(); }); }); it('container created', function (done) { expect(appResult.containerId).to.be(undefined); expect(appEntry.containerId).to.be.ok(); docker.getContainer(appEntry.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.Config.ExposedPorts['7777/tcp']).to.eql({ }); expect(data.Config.Env).to.contain('CLOUDRON_WEBADMIN_ORIGIN=' + settings.adminOrigin()); expect(data.Config.Env).to.contain('CLOUDRON_API_ORIGIN=' + settings.adminOrigin()); expect(data.Config.Env).to.contain('CLOUDRON=1'); expect(data.Config.Env).to.contain('CLOUDRON_APP_ORIGIN=https://' + APP_LOCATION + '.' + DOMAIN_0.domain); expect(data.Config.Env).to.contain('CLOUDRON_APP_DOMAIN=' + APP_LOCATION + '.' + DOMAIN_0.domain); // Hostname must not be set of app fqdn or app location! expect(data.Config.Hostname).to.not.contain(APP_LOCATION); expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7171'); expect(data.HostConfig.PortBindings['7778/tcp'][0].HostPort).to.eql('7171'); done(); }); }); it('nginx config', function (done) { expect(fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP_LOCATION + '.conf')); done(); }); it('volume created', function (done) { expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP_ID)); let volume = docker.getVolume(APP_ID + '-localstorage'); volume.inspect(function (error, volume) { expect(error).to.be(null); expect(volume.Labels.appId).to.eql(APP_ID); expect(volume.Options.device).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data'); done(); }); }); it('http is up and running', function (done) { var tryCount = 20; // TODO what does that check for? expect(appResult.httpPort).to.be(undefined); (function healthCheck() { superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath) .end(function (err, res) { if (err || res.statusCode !== 200) { if (--tryCount === 0) { console.log('Unable to curl http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath); return done(new Error('Timedout')); } return setTimeout(healthCheck, 2000); } expect(!err).to.be.ok(); expect(res.statusCode).to.equal(200); done(); }); })(); }); it('tcp port mapping works', function (done) { var client = net.connect(7171); client.on('data', function (data) { expect(data.toString()).to.eql('ECHO_SERVER_PORT=7171'); done(); }); client.on('error', done); }); it('running container has volume mounted', function (done) { docker.getContainer(appEntry.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Type).to.eql('volume'); done(); }); }); it('app responds to http request', function (done) { superagent.get('http://localhost:' + appEntry.httpPort).end(function (err, res) { expect(!err).to.be.ok(); expect(res.statusCode).to.equal(200); done(); }); }); it('oauth addon config', function (done) { var appContainer = docker.getContainer(appEntry.containerId); appContainer.inspect(function (error, data) { expect(error).to.not.be.ok(); clients.getByAppIdAndType(APP_ID, clients.TYPE_OAUTH, function (error, client) { expect(error).to.not.be.ok(); expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens expect(client.clientSecret.length).to.be(256); // 32 hex chars (8 * 256 bits) expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_ID=' + client.id); expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_SECRET=' + client.clientSecret); done(); }); }); }); it('installation - app can populate addons', function (done) { superagent.get(`http://localhost:${appEntry.httpPort}/populate_addons`).end(function (error, res) { expect(!error).to.be.ok(); expect(res.statusCode).to.equal(200); for (var key in res.body) { expect(res.body[key]).to.be('OK'); } done(); }); }); it('installation - app can check addons', function (done) { console.log('This test can take a while as it waits for scheduler addon to tick 3'); checkAddons(appEntry, done); }); it('installation - redis addon created', function (done) { checkRedis('redis-' + APP_ID, done); }); }); describe('logs', function () { it('logs - stdout and stderr', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs') .query({ access_token: token }) .buffer(false) .end(function (err, res) { var data = ''; res.on('data', function (d) { data += d.toString('utf8'); }); res.on('end', function () { expect(data.length).to.not.be(0); done(); }); res.on('error', done); }); }); it('logStream - requires event-stream accept header', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logstream') .query({ access_token: token, fromLine: 0 }) .end(function (err, res) { expect(res.statusCode).to.be(400); done(); }); }); it('logStream - stream logs', function (done) { var options = { port: constants.PORT, host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token, headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' } }; // superagent doesn't work. maybe https://github.com/visionmedia/superagent/issues/420 var req = http.get(options, function (res) { var data = ''; res.on('data', function (d) { data += d.toString('utf8'); }); setTimeout(function checkData() { expect(data.length).to.not.be(0); data.split('\n').forEach(function (line) { if (line.indexOf('id: ') !== 0) return; expect(parseInt(line.substr(4), 10)).to.be.a('number'); // timestamp }); req.abort(); done(); }, 1000); res.on('error', done); }); req.on('error', done); }); }); describe('configure (db fields)', function () { it('fails for no label', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/label') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails for invalid label', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/label') .query({ access_token: token }) .send({ label: null }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set the label', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/label') .query({ access_token: token }) .send({ label: 'LABEL'}) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('did set the label', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID) .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); expect(res.body.label).to.be('LABEL'); done(); }); }); ///////////// tags it('fails for no tags', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/tags') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails for null tags', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/tags') .query({ access_token: token }) .send({ tags: null }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails for empty tag', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/tags') .query({ access_token: token }) .send({ tags: ['tag1', '', 'tag2'] }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails for non-string tag', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/tags') .query({ access_token: token }) .send({ tags: ['tag1', 123, 'tag2'] }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set the tags', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/tags') .query({ access_token: token }) .send({ tags: [ 'tag1', 'tag2' ] }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('did set the tags', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID) .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); expect(res.body.tags).to.eql([ 'tag1', 'tag2' ]); done(); }); }); ///////////// icon it('fails for no icon', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/icon') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails for invalid icon', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/icon') .query({ access_token: token }) .send({ icon: 'something non base64' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set the icon', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/icon') .query({ access_token: token }) .send({ icon: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=' }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('did set the icon', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/icon') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); // response is some PNG done(); }); }); it('can reset the icon', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/icon') .query({ access_token: token }) .send({ icon: null }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); ////////////// automatic updates it('can disable automatic updates', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/automatic_update') .query({ access_token: token }) .send({ enable: false }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('did disable automatic updates', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID) .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); expect(res.body.enableAutomaticUpdate).to.be(false); done(); }); }); it('can disable automatic backups', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/automatic_backup') .query({ access_token: token }) .send({ enable: false }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('did disable automatic backups', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID) .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); expect(res.body.enableBackup).to.be(false); done(); }); }); ////////////// access restriction it('cannot set bad accessRestriction', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/access_restriction') .query({ access_token: token }) .send({ accessRestriction: false }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can clear accessRestriction', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/access_restriction') .query({ access_token: token }) .send({ accessRestriction: null }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('can set accessRestriction', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/access_restriction') .query({ access_token: token }) .send({ accessRestriction: { users: [ 'someuserid' ], groups: [ 'somegroupid' ] } }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); /////////////// cert it('cannot set only the cert, no key', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/cert') .query({ access_token: token }) .send({ cert: CERT }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('cannot reconfigure app with only the key, no cert', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/cert') .query({ access_token: token }) .send({ key: KEY }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('cannot set invalid cert', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/cert') .query({ access_token: token }) .send({ cert: 'x' + CERT, key: KEY }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set cert', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/cert') .query({ access_token: token }) .send({ cert: CERT, key: KEY }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('can reset cert', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/cert') .query({ access_token: token }) .send({ cert: null, key: null }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); }); describe('memory limit', function () { it('fails for no memory limit', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/memory_limit') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails for invalid memory limit', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/memory_limit') .query({ access_token: token }) .send({ memoryLimit: -34 }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set the memory limit', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/memory_limit') .query({ access_token: token }) .send({ memoryLimit: 512 * 1024 * 1024 }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for memory limit', function (done) { waitForTask(taskId, done); }); it('did set memory limit', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.HostConfig.Memory).to.be(512 * 1024 * 1024/2); done(); }); }); }); }); describe('configure robotsTxt', function () { it('fails with missing robotsTxt', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails with bad robotsTxt', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt') .query({ access_token: token }) .send({ robotsTxt: 34 }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set robotsTxt', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt') .query({ access_token: token }) .send({ robotsTxt: 'any string is good' }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); it('can reset robotsTxt', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt') .query({ access_token: token }) .send({ robotsTxt: null }) .end(function (err, res) { expect(res.statusCode).to.equal(200); done(); }); }); }); describe('configure location', function () { it('cannot reconfigure app with missing domain', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/location') .query({ access_token: token }) .send({ location: 'hellothre' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('cannot reconfigure app with bad location', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/location') .query({ access_token: token }) .send({ location: 1234, domain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('non admin cannot reconfigure app', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/location') .query({ access_token: token_1 }) .send({ location: APP_LOCATION_NEW, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null }) .end(function (err, res) { expect(res.statusCode).to.equal(403); done(); }); }); it('can change location', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/location') .query({ access_token: token }) .send({ location: APP_LOCATION_NEW, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7172 } }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did change location', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); expect(app.mailboxName).to.be(APP_LOCATION_NEW + '.app'); // must follow location change docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.Config.Env).to.contain('CLOUDRON_APP_ORIGIN=https://' + APP_LOCATION_NEW + '.' + DOMAIN_0.domain); expect(data.Config.Env).to.contain('CLOUDRON_APP_DOMAIN=' + APP_LOCATION_NEW + '.' + DOMAIN_0.domain); expect(data.Config.Hostname).to.not.contain(APP_LOCATION_NEW); expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7172'); expect(data.HostConfig.PortBindings['7778/tcp'][0].HostPort).to.eql('7172'); done(); }); }); }); it('port mapping works after reconfiguration', function (done) { setTimeout(function () { var client = net.connect(7172); client.on('data', function (data) { expect(data.toString()).to.eql('ECHO_SERVER_PORT=7172'); done(); }); client.on('error', done); }, 4000); }); it('app can check addons', function (done) { console.log('This test can take a while as it waits for scheduler addon to tick 4'); apps.get(APP_ID, function (error, app) { if (error) return done(error); checkAddons(app, done); }); }); }); describe('configure debug mode', function () { it('fails with missing mode', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/debug_mode') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails with bad debug mode', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/debug_mode') .query({ access_token: token }) .send({ debugMode: 'sleep' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set debug mode', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/debug_mode') .query({ access_token: token }) .send({ debugMode: { readonlyRootfs: false } }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did change readonly rootfs', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.HostConfig.ReadonlyRootfs).to.be(false); done(); }); }); }); it('can reset debug mode', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/debug_mode') .query({ access_token: token }) .send({ debugMode: null }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did change readonly rootfs', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.HostConfig.ReadonlyRootfs).to.be(true); done(); }); }); }); }); describe('configure env', function () { it('fails with missing env', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/env') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails with bad env mode', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/env') .query({ access_token: token }) .send({ env: 'ok' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set env', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/env') .query({ access_token: token }) .send({ env: { 'OPM': 'SAITAMA' } }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did set env', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.Config.Env).to.contain('OPM=SAITAMA'); done(); }); }); }); it('can reset env', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/env') .query({ access_token: token }) .send({ env: {} }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did reset env', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.Config.Env).to.not.contain('OPM=SAITAMA'); done(); }); }); }); }); describe('configure mailbox', function () { it('fails with missing mailbox', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails with bad mailbox', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') .query({ access_token: token }) .send({ mailbox: 'genos@cloudron.io' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set mailbox', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') .query({ access_token: token }) .send({ mailboxName: 'genos' }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did set mailbox', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.Config.Env).to.contain('CLOUDRON_MAIL_FROM=genos@' + app.domain); done(); }); }); }); it('can reset mailbox', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') .query({ access_token: token }) .send({ mailboxName: null }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did reset mailbox', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); expect(data.Config.Env).to.not.contain('CLOUDRON_MAIL_FROM=' + app.location + '@' + app.domain + '.app'); done(); }); }); }); }); describe('configure data dir', function () { it('fails with missing datadir', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/data_dir') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('fails with bad data dir', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/data_dir') .query({ access_token: token }) .send({ dataDir: 'what' }) .end(function (err, res) { expect(res.statusCode).to.equal(400); done(); }); }); it('can set data dir', function (done) { let dataDir = path.join(paths.baseDir(), 'apps-test-datadir-' + crypto.randomBytes(4).readUInt32LE(0)); fs.mkdirSync(dataDir); superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/data_dir') .query({ access_token: token }) .send({ dataDir: dataDir }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('app can check addons', function (done) { console.log('This test can take a while as it waits for scheduler addon to tick 4'); apps.get(APP_ID, function (error, app) { if (error) return done(error); checkAddons(app, done); }); }); it('can reset data dir', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/data_dir') .query({ access_token: token }) .send({ dataDir: null }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('app can check addons', function (done) { console.log('This test can take a while as it waits for scheduler addon to tick 4'); apps.get(APP_ID, function (error, app) { if (error) return done(error); checkAddons(app, done); }); }); }); describe('start/stop', function () { it('non admin cannot stop app', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop') .query({ access_token: token_1 }) .end(function (err, res) { expect(res.statusCode).to.equal(403); done(); }); }); it('can stop app', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for task', function (done) { waitForTask(taskId, done); }); it('did stop the app', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); superagent.get('http://localhost:' + app.httpPort + APP_MANIFEST.healthCheckPath).end(function (err) { if (!err || err.code !== 'ECONNREFUSED') return done(new Error('App has not died')); // wait for app status to be updated superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID).query({ access_token: token }).end(function (error, result) { if (error || result.statusCode !== 200 || result.body.runState !== 'stopped') return done(new Error('App is not in stopped state')); done(); }); }); }); }); it('nonadmin cannot start app', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start') .query({ access_token: token_1 }) .end(function (err, res) { expect(res.statusCode).to.equal(403); done(); }); }); it('can start app', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; done(); }); }); it('wait for app to start', function (done) { waitForTask(taskId, function () { setTimeout(done, 5000); }); // give app 5 seconds to start }); it('did start the app', function (done) { apps.get(APP_ID, function (error, app) { if (error) return done(error); superagent.get('http://localhost:' + app.httpPort + APP_MANIFEST.healthCheckPath) .end(function (err, res) { if (res && res.statusCode === 200) return done(); done(new Error('app is not running')); }); }); }); }); describe('uninstall', function () { it('cannot uninstall invalid app', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(404); done(); }); }); it('non admin cannot uninstall app', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall') .query({ access_token: token_1 }) .end(function (err, res) { expect(res.statusCode).to.equal(403); done(); }); }); it('can uninstall app', function (done) { var fake1 = nock(settings.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(200, { }); var fake2 = nock(settings.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { }); superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; expect(fake1.isDone()).to.be.ok(); expect(fake2.isDone()).to.be.ok(); done(); }); }); }); describe('post uninstall', function () { let appEntry; it('can get app', function (done) { apps.get(APP_ID, function (error, app) { expect(!error).to.be.ok(); expect(app).to.be.an('object'); appEntry = app; done(); }); }); it('did uninstall the app', function (done) { waitForTask(taskId, done); }); it('app is gone', function (done) { superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID) .query({ access_token: token }) .end(function (err, res) { if (res.statusCode === 404) return done(null); done(new Error('App is still there')); }); }); it('container destroyed', function (done) { docker.getContainer(appEntry.containerId).inspect(function (error, data) { expect(error).to.be.ok(); expect(data).to.not.be.ok(); done(); }); }); it('volume destroyed', function (done) { expect(!fs.existsSync(paths.APPS_DATA_DIR + '/' + APP_ID)); done(); }); it('removed nginx', function (done) { expect(!fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP_LOCATION + '.conf')); done(); }); it('removed redis addon', function (done) { docker.getContainer('redis-' + APP_ID).inspect(function (error) { expect(error).to.be.ok(); done(); }); }); }); describe('not sure what this is', function () { it('app install succeeds again', function (done) { var fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST }); var fake2 = nock(settings.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { }); superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null }) .end(function (err, res) { expect(res.statusCode).to.equal(202); expect(res.body.id).to.be.a('string'); APP_ID = res.body.id; expect(fake1.isDone()).to.be.ok(); expect(fake2.isDone()).to.be.ok(); done(); }); }); it('app install fails with developer token', function (done) { superagent.post(SERVER_URL + '/api/v1/developer/login') .send({ username: USERNAME, password: PASSWORD }) .end(function (error, result) { expect(error).to.not.be.ok(); expect(result.statusCode).to.equal(200); expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date'); expect(result.body.accessToken).to.be.a('string'); // overwrite non dev token token = result.body.accessToken; superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) .send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null }) .end(function (err, res) { expect(res.statusCode).to.equal(424); // appstore purchase external error done(); }); }); }); }); describe('the end', function () { // this is here so we can debug things if tests fail it('can stop box', stopBox); }); });