diff --git a/package-lock.json b/package-lock.json index aab996506..9758313f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "eslint": "^10.0.0", "eventsource": "^4.1.0", "expect.js": "*", + "globals": "^17.3.0", "mocha": "^11.7.5", "nock": "^14.0.11", "ssh2": "^1.17.0" @@ -5413,6 +5414,19 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/google-auth-library": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.8.0.tgz", diff --git a/package.json b/package.json index 75f5f22c0..70ffae4f9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "url": "https://git.cloudron.io/platform/box.git" }, "dependencies": { - "@simplewebauthn/server": "^13.2.2", "@aws-sdk/client-route-53": "^3.990.0", "@aws-sdk/client-s3": "^3.990.0", "@aws-sdk/lib-storage": "^3.990.0", @@ -23,6 +22,7 @@ "@cloudron/superagent": "^1.0.1", "@google-cloud/dns": "^5.3.1", "@google-cloud/storage": "^7.19.0", + "@simplewebauthn/server": "^13.2.2", "@smithy/node-http-handler": "^4.4.10", "@smithy/util-retry": "^4.2.8", "@types/node": "^25.2.3", @@ -68,6 +68,7 @@ "eslint": "^10.0.0", "eventsource": "^4.1.0", "expect.js": "*", + "globals": "^17.3.0", "mocha": "^11.7.5", "nock": "^14.0.11", "ssh2": "^1.17.0" diff --git a/src/backupcleaner.js b/src/backupcleaner.js index 71392b77b..c3fa40ebf 100644 --- a/src/backupcleaner.js +++ b/src/backupcleaner.js @@ -208,7 +208,7 @@ async function cleanupMissingBackups(site, progressCallback) { if (constants.TEST) return missingBackupPaths; - let page = 1, result = []; + let page = 1, result; do { result = await backups.listByTypePaged(backups.BACKUP_TYPE_BOX, site.id, page, perPage); diff --git a/src/backupformat/rsync.js b/src/backupformat/rsync.js index ae87fa53d..f173f0ad9 100644 --- a/src/backupformat/rsync.js +++ b/src/backupformat/rsync.js @@ -47,7 +47,7 @@ async function addFile(sourceFile, encryption, uploader, progressCallback) { const hash = new HashStream(); const destStream = uploader.createStream(); - let pipeline = null; + let pipeline; if (encryption) { const encryptStream = new EncryptStream(encryption); pipeline = safe(stream.pipeline(sourceStream, encryptStream, ps, hash, destStream)); diff --git a/src/backupformat/tgz.js b/src/backupformat/tgz.js index bb66052d2..78ca7efdf 100644 --- a/src/backupformat/tgz.js +++ b/src/backupformat/tgz.js @@ -152,7 +152,7 @@ async function tarPack(dataLayout, encryption, uploader, progressCallback) { const hash = new HashStream(); const destStream = uploader.createStream(); - let pipeline = null; + let pipeline; if (encryption) { const encryptStream = new EncryptStream(encryption); pipeline = safe(stream.pipeline(pack, gzip, encryptStream, ps, hash, destStream)); diff --git a/src/dns/linode.js b/src/dns/linode.js index 83853b046..93d0c595f 100644 --- a/src/dns/linode.js +++ b/src/dns/linode.js @@ -62,7 +62,7 @@ async function getZoneRecords(domainConfig, zoneName, name, type) { const zoneId = await getZoneId(domainConfig, zoneName); - let page = 0, more = false; + let page = 0, more; let records = []; do { diff --git a/src/logs.js b/src/logs.js index d6ad664a0..a02644238 100644 --- a/src/logs.js +++ b/src/logs.js @@ -43,7 +43,7 @@ class LogStream extends TransformStream { _transform(chunk, encoding, callback) { const data = this._soFar + this._decoder.write(chunk); - let start = this._soFar.length, end = -1; + let start = this._soFar.length, end; while ((end = data.indexOf('\n', start)) !== -1) { const line = data.slice(start, end); // does not include end this.push(this._format(line)); diff --git a/src/metrics.js b/src/metrics.js index c8b2c9025..37224a078 100644 --- a/src/metrics.js +++ b/src/metrics.js @@ -10,6 +10,7 @@ import network from './network.js'; import os from 'node:os'; import { Readable } from 'node:stream'; import safe from 'safetydance'; +import services from './services.js'; import shellModule from './shell.js'; import superagent from '@cloudron/superagent'; import _ from './underscore.js'; @@ -214,7 +215,7 @@ async function sendToGraphite() { // the datapoint is (value, timestamp) https://graphite.readthedocs.io/en/latest/ async function getGraphiteUrl() { const [error, result] = await safe(docker.inspect('graphite')); - if (error && error.reason === BoxError.NOT_FOUND) return { status: SERVICE_STATUS_STOPPED }; + if (error && error.reason === BoxError.NOT_FOUND) return { status: services.SERVICE_STATUS_STOPPED }; if (error) throw error; const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null); diff --git a/src/notifications.js b/src/notifications.js index 880a1baa6..b5e011b0e 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -2,7 +2,6 @@ import assert from 'node:assert'; import AuditSource from './auditsource.js'; import BoxError from './boxerror.js'; import changelog from './changelog.js'; -import constants from './constants.js'; import dashboard from './dashboard.js'; import database from './database.js'; import debugModule from 'debug'; diff --git a/src/oidcserver.js b/src/oidcserver.js index 019a3b185..eef14e18f 100644 --- a/src/oidcserver.js +++ b/src/oidcserver.js @@ -317,7 +317,7 @@ async function renderInteractionPage(req, res) { return res.send(ejs.render(TEMPLATE_LOGIN, data)); } else if (prompt.name === 'consent') { - let hasAccess = false; + let hasAccess; const data = { iconUrl: '/api/v1/cloudron/avatar', diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js deleted file mode 100644 index 72b49cb32..000000000 --- a/src/routes/test/apps-test.js +++ /dev/null @@ -1,1584 +0,0 @@ -/* global it:false */ - -import apps from '../../apps.js'; -import async from 'async'; -import child_process from 'node:child_process'; -import constants from '../../constants.js'; -import crypto from 'node:crypto'; -import database from '../../database.js'; -import Docker from 'dockerode'; -import expect from 'expect.js'; -import fs from 'node:fs'; -import hat from '../../hat.js'; -import http from 'node:http'; -import ldapServer from '../../ldapserver.js'; -import net from 'node:net'; -import nock from 'nock'; -import path from 'node:path'; -import paths from '../../paths.js'; -import platform from '../../platform.js'; -import safe from 'safetydance'; -import server from '../../server.js'; -import settings from '../../settings.js'; -import superagent from '@cloudron/superagent'; -import tokens from '../../tokens.js'; -import url from 'node:url'; - -/* global describe:false */ -/* global before:false */ - -const SERVER_URL = 'http://localhost:' + constants.PORT; - -const docker = new Docker({ socketPath: '/var/run/docker.sock' }); - -// Test image information -const TEST_IMAGE_REPO = 'docker.io/cloudron/io.cloudron.testapp'; -const TEST_IMAGE_TAG = '20201121-223249-985e86ebb'; -const 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' } -}; - -const APP_STORE_ID = 'test'; -let APP_ID; -const APP_SUBDOMAIN = 'appssubdomain'; -const APP_SUBDOMAIN_NEW = 'appssubdomainnew'; - -const APP_MANIFEST = {}; // JSON.parse(fs.readFileSync(import.meta.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'; -const user_1_id = null; - -// authentication token -const token = null; -const token_1 = null; - -let KEY, CERT; -// let appstoreIconServer = hock.createHock({ throwOnUnmatched: false }); - -function checkRedis(containerId, done) { - let 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.status !== 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.status !== 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, - ldapServer.start, - settings._setApiServerOrigin.bind(null, 'http://localhost:6060'), - - function (callback) { - superagent.post(SERVER_URL + '/api/v1/provision/setup') - .send({ domainConfig: DOMAIN_0 }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.status).to.eql(200); - - waitForSetup(callback); - }); - }, - - function (callback) { - superagent.post(SERVER_URL + '/api/v1/provision/activate') - .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.status).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(async function (err, res) { - expect(res.status).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...) - const token = { clientId: tokens.ID_WEBADMIN, identifier: user_1_id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; - const result = await tokens.add(token); - - token_1 = result.accessToken; - - callback(); - }); - }, - - function (callback) { - appstoreIconServer - .get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon') - .replyWithFile(200, path.resolve(import.meta.dirname, '../../../assets/avatar.png')); - - const 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.getStatus().message === '') 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, - ldapServer.stop, - ], done); -} - -xdescribe('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') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token }) - .send({ manifest: null }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token }) - .send({ manifest: 'epic' }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token }) - .send({ manifest: null, appStoreId: '' }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token }) - .send('garbage') - .end(function (err, res) { - expect(res.status).to.equal(400); - done(); - }); - }); - - it('app install fails - missing domain', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: 'some', accessRestriction: null }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: 'some', accessRestriction: null, domain: 'doesnotexist.com' }) - .end(function (err, res) { - expect(res.status).to.equal(400); - expect(res.body.message).to.eql('No such domain'); - done(); - }); - }); - - it('app install fails - invalid subdomain type', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: 42, accessRestriction: null, domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).to.equal(400); - expect(res.body.message).to.eql('subdomain is required'); - done(); - }); - }); - - it('app install fails - reserved admin subdomain', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: 'my', accessRestriction: null, domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).to.equal(400); - expect(res.body.message).to.contain('my is reserved'); - done(); - }); - }); - - it('app install fails - reserved smtp subdomain', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: constants.SMTP_SUBDOMAIN, accessRestriction: null, domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).to.equal(400); - expect(res.body.message).to.contain(constants.SMTP_SUBDOMAIN + ' is reserved'); - done(); - }); - }); - - it('app install fails - portBindings must be object', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: APP_SUBDOMAIN, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: APP_SUBDOMAIN, portBindings: {}, domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: APP_SUBDOMAIN, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).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') - .query({ access_token: token_1 }) - .send({ manifest: APP_MANIFEST, subdomain: APP_SUBDOMAIN, portBindings: null, accessRestriction: null, domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).to.equal(403); - done(); - }); - }); - - it('app install fails because manifest download fails', function (done) { - const fake = nock(settings.apiServerOrigin()).get('/api/v1/apps/test').reply(404, {}); - - superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ appStoreId: APP_STORE_ID, subdomain: APP_SUBDOMAIN, portBindings: null, domain: DOMAIN_0.domain, accessRestriction: { users: [ 'someuser' ], groups: [] } }) - .end(function (err, res) { - expect(res.status).to.equal(404); - expect(fake.isDone()).to.be.ok(); - done(); - }); - }); - - it('app install succeeds', async function () { - const fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST }); - - await settings.setAppstoreApiToken(USER_1_APPSTORE_TOKEN); - - const res = await superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ appStoreId: APP_STORE_ID, subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: { users: [ 'someuser' ], groups: [] } }); - - expect(res.status).to.equal(202); - expect(res.body.id).to.be.a('string'); - APP_ID = res.body.id; - expect(fake1.isDone()).to.be.ok(); - taskId = res.body.taskId; - }); - - it('app install fails because of conflicting subdomain', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps') - .query({ access_token: token }) - .send({ manifest: APP_MANIFEST, subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null }) - .end(function (err, res) { - expect(res.status).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.status).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_SUBDOMAIN + '.app'); - expect(res.body.mailboxDomain).to.be(DOMAIN_0.domain); - 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.status).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.status).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.status).to.equal(200); - expect(res.body.apps).to.be.an('array'); - expect(res.body.apps.length).to.equal(0); - done(); - }); - }); - }); - - xdescribe('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.status !== 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.dashboardOrigin()); - expect(data.Config.Env).to.contain('CLOUDRON_API_ORIGIN=' + settings.dashboardOrigin()); - expect(data.Config.Env).to.contain('CLOUDRON=1'); - expect(data.Config.Env).to.contain('CLOUDRON_APP_ORIGIN=https://' + APP_SUBDOMAIN + '.' + DOMAIN_0.domain); - expect(data.Config.Env).to.contain('CLOUDRON_APP_DOMAIN=' + APP_SUBDOMAIN + '.' + DOMAIN_0.domain); - // Hostname must not be set of app fqdn or app subdomain! - expect(data.Config.Hostname).to.not.contain(APP_SUBDOMAIN); - 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_SUBDOMAIN + '.conf')); - done(); - }); - - it('volume created', function (done) { - expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP_ID)); - const 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(); - }); - }); - - xit('tcp port mapping works', function (done) { - const 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(); - }); - }); - - xit('app responds to http request', function (done) { - console.log(`talking to http://${appEntry.containerIp}:7777`); - superagent.get(`http://${appEntry.containerIp}:7777`).end(function (error, result) { - console.dir(error); - expect(result.status).to.equal(200); - done(); - }); - }); - - it('installation - redis addon created', function (done) { - checkRedis('redis-' + APP_ID, done); - }); - }); - - xdescribe('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) { - const 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.status).to.be(400); - done(); - }); - }); - - it('logStream - stream logs', function (done) { - const 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 - const req = http.get(options, function (res) { - const 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.status).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.status).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.status).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.status).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.status).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.status).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.status).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.status).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.status).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.status).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') - .end(function (err, res) { - expect(res.status).to.equal(400); - done(); - }); - }); - - it('fails for invalid icon', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/icon') - .send({ icon: 'something non base64' }) - .end(function (err, res) { - expect(res.status).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.status).to.equal(200); - done(); - }); - }); - - it('did set the icon', function (done) { - superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/icon') - .end(function (err, res) { - expect(res.status).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.status).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.status).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.status).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.status).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.status).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.status).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.status).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.status).to.equal(200); - done(); - }); - }); - - ////////////// upstreamUri - it('cannot set empty upstreamUri', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/upstream_uri') - .query({ access_token: token }) - .send({ upstreamUri: '' }) - .end(function (err, res) { - expect(res.status).to.equal(400); - done(); - }); - }); - - it('cannot set bad upstreamUri', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/upstream_uri') - .query({ access_token: token }) - .send({ upstreamUri: 'foobar:com' }) - .end(function (err, res) { - expect(res.status).to.equal(400); - done(); - }); - }); - - it('can set upstreamUri', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/upstream_uri') - .query({ access_token: token }) - .send({ upstreamUri: 'https://1.2.3.4:443' }) - .end(function (err, res) { - expect(res.status).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({ subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, cert: CERT }) - .end(function (err, res) { - expect(res.status).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({ subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, key: KEY }) - .end(function (err, res) { - expect(res.status).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({ subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, cert: 'x' + CERT, key: KEY }) - .end(function (err, res) { - expect(res.status).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({ subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, cert: CERT, key: KEY }) - .end(function (err, res) { - expect(res.status).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({ subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, cert: null, key: null }) - .end(function (err, res) { - expect(res.status).to.equal(200); - done(); - }); - }); - }); - - xdescribe('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.status).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.status).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.status).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 reverseProxy - robotsTxt', function () { - it('fails with bad robotsTxt', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy') - .query({ access_token: token }) - .send({ robotsTxt: 34 }) - .end(function (err, res) { - expect(res.status).to.equal(400); - done(); - }); - }); - - it('can set robotsTxt', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy') - .query({ access_token: token }) - .send({ robotsTxt: 'any string is good', csp: null }) - .end(function (err, res) { - expect(res.status).to.equal(200); - done(); - }); - }); - - it('can reset robotsTxt', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy') - .query({ access_token: token }) - .send({ robotsTxt: null, csp: null }) - .end(function (err, res) { - expect(res.status).to.equal(200); - done(); - }); - }); - - it('fails with bad csp', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy') - .query({ access_token: token }) - .send({ robotsTxt: null, csp: 34 }) - .end(function (err, res) { - expect(res.status).to.equal(400); - done(); - }); - }); - - it('can set frame-ancestors', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy') - .query({ access_token: token }) - .send({ robotsTxt: null, csp: 'frame-ancestors \'self\'' }) - .end(function (err, res) { - expect(res.status).to.equal(200); - done(); - }); - }); - - it('can reset frame-ancestors', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy') - .query({ access_token: token }) - .send({ robotsTxt: null, csp: null }) - .end(function (err, res) { - expect(res.status).to.equal(200); - done(); - }); - }); - }); - - describe('configure subdomain', 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({ subdomain: 'hellothre' }) - .end(function (err, res) { - expect(res.status).to.equal(400); - done(); - }); - }); - - it('cannot reconfigure app with bad subdomain', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/location') - .query({ access_token: token }) - .send({ subdomain: 1234, domain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).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({ subdomain: APP_SUBDOMAIN_NEW, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null }) - .end(function (err, res) { - expect(res.status).to.equal(403); - done(); - }); - }); - - xit('can change subdomain', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/location') - .query({ access_token: token }) - .send({ subdomain: APP_SUBDOMAIN_NEW, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7172 } }) - .end(function (err, res) { - expect(res.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('did change subdomain', function (done) { - apps.get(APP_ID, function (error, app) { - if (error) return done(error); - - expect(app.mailboxName).to.be(APP_SUBDOMAIN_NEW + '.app'); // must follow subdomain change - expect(app.mailboxDomain).to.be(DOMAIN_0.domain); - - 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_SUBDOMAIN_NEW + '.' + DOMAIN_0.domain); - expect(data.Config.Env).to.contain('CLOUDRON_APP_DOMAIN=' + APP_SUBDOMAIN_NEW + '.' + DOMAIN_0.domain); - expect(data.Config.Hostname).to.not.contain(APP_SUBDOMAIN_NEW); - expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7172'); - expect(data.HostConfig.PortBindings['7778/tcp'][0].HostPort).to.eql('7172'); - done(); - }); - }); - }); - - xit('port mapping works after reconfiguration', function (done) { - setTimeout(function () { - const client = net.connect(7172); - client.on('data', function (data) { - expect(data.toString()).to.eql('ECHO_SERVER_PORT=7172'); - done(); - }); - client.on('error', done); - }, 4000); - }); - }); - - 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.status).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.status).to.equal(400); - done(); - }); - }); - - xit('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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('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(); - }); - }); - }); - - xit('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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('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.status).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.status).to.equal(400); - done(); - }); - }); - - xit('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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('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(); - }); - }); - }); - - xit('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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('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.status).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.status).to.equal(400); - done(); - }); - }); - - xit('can set mailbox', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') - .query({ access_token: token }) - .send({ mailboxName: 'genos', mailboxDomain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('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(); - }); - }); - }); - - xit('can reset mailbox', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') - .query({ access_token: token }) - .send({ mailboxName: null, mailboxDomain: DOMAIN_0.domain }) - .end(function (err, res) { - expect(res.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('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.subdomain + '@' + 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.status).to.equal(400); - done(); - }); - }); - - xit('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.status).to.equal(400); - done(); - }); - }); - - xit('can set data dir', function (done) { - const 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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, 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.status).to.equal(403); - done(); - }); - }); - - xit('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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for task', function (done) { - waitForTask(taskId, done); - }); - - xit('did stop the app', function (done) { - apps.get(APP_ID, function (error, app) { - if (error) return done(error); - - superagent.get(`http://${app.containerIp}:7777` + 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.status !== 200 || result.body.runState !== 'stopped') return done(new Error('App is not in stopped state')); - - done(); - }); - }); - }); - }); - - xit('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.status).to.equal(403); - done(); - }); - }); - - xit('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.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for app to start', function (done) { - waitForTask(taskId, function () { setTimeout(done, 5000); }); // give app 5 seconds to start - }); - - xit('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.status === 200) return done(); - done(new Error('app is not running')); - }); - }); - }); - - xit('can restart app', function (done) { - superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/restart') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.status).to.equal(202); - taskId = res.body.taskId; - done(); - }); - }); - - xit('wait for app to restart', function (done) { - waitForTask(taskId, function () { setTimeout(done, 12000); }); // give app 12 seconds (to die and start) - }); - - xit('did restart 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.status === 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.status).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.status).to.equal(403); - done(); - }); - }); - - xit('can uninstall app', function (done) { - const fake1 = nock(settings.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(200, { }); - const 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.status).to.equal(202); - taskId = res.body.taskId; - expect(fake1.isDone()).to.be.ok(); - expect(fake2.isDone()).to.be.ok(); - done(); - }); - }); - }); - - xdescribe('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.status === 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_SUBDOMAIN + '.conf')); - done(); - }); - - it('removed redis addon', function (done) { - docker.getContainer('redis-' + APP_ID).inspect(function (error) { - expect(error).to.be.ok(); - done(); - }); - }); - }); - - describe('the end', function () { - // this is here so we can debug things if tests fail - it('can stop box', stopBox); - }); -}); diff --git a/src/routes/test/appstore-test.js b/src/routes/test/appstore-test.js index 58de7bb8a..deebfb28e 100644 --- a/src/routes/test/appstore-test.js +++ b/src/routes/test/appstore-test.js @@ -5,7 +5,6 @@ import common from './common.js'; import constants from '../../constants.js'; import expect from 'expect.js'; import nock from 'nock'; -import settings from '../../settings.js'; import superagent from '@cloudron/superagent'; /* global describe:false */ diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index 4058de47f..9a729f4da 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -143,7 +143,6 @@ describe('Cloudron', function () { const email = 'setup3@account.com'; const password = 'test?!3434543534'; const username = 'setupuser3'; - const displayName = 'setup user3'; const response0 = await superagent.post(`${serverUrl}/api/v1/user_directory/profile_config`) .query({ access_token: owner.token }) diff --git a/src/routes/test/dockerregistries-test.js b/src/routes/test/dockerregistries-test.js index 227332066..e14e1374e 100644 --- a/src/routes/test/dockerregistries-test.js +++ b/src/routes/test/dockerregistries-test.js @@ -1,7 +1,6 @@ /* jslint node:true */ import common from './common.js'; -import constants from '../../constants.js'; import expect from 'expect.js'; import superagent from '@cloudron/superagent'; diff --git a/src/routes/test/users-test.js b/src/routes/test/users-test.js index 89d5d03e3..33f1eda99 100644 --- a/src/routes/test/users-test.js +++ b/src/routes/test/users-test.js @@ -383,13 +383,11 @@ describe('Users API', function () { }); it('can change avatar', async function () { - let customAvatarSize = 0; - const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}/avatar`) .query({ access_token: owner.token }) .attach('avatar', './logo.png'); - customAvatarSize = fs.readFileSync('./logo.png').length; + const customAvatarSize = fs.readFileSync('./logo.png').length; expect(response.status).to.equal(204); diff --git a/src/routes/test/volumes-test.js b/src/routes/test/volumes-test.js index f8f6644d5..b724d030e 100644 --- a/src/routes/test/volumes-test.js +++ b/src/routes/test/volumes-test.js @@ -68,7 +68,7 @@ describe('Volumes API', function () { }); it('cannot update volume', async function () { - const [error, response] = await safe(superagent.post(`${serverUrl}/api/v1/volumes/${volumeId}`) + const [, response] = await safe(superagent.post(`${serverUrl}/api/v1/volumes/${volumeId}`) .query({ access_token: owner.token }) .send({ mountOptions: { hostPath: '/media/cloudron-test-music-2' }}) .ok(() => true)); diff --git a/src/services.js b/src/services.js index c58f78a71..2530d4a09 100644 --- a/src/services.js +++ b/src/services.js @@ -2017,6 +2017,17 @@ const ADDONS = { } }; +async function importAppDatabase(app, addon) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof addon, 'string'); + + if (!(addon in ADDONS)) throw new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`); + + await ADDONS[addon].setup(app, app.manifest.addons[addon]); + await ADDONS[addon].clear(app, app.manifest.addons[addon]); // clear in case we crashed in a restore + await ADDONS[addon].restore(app, app.manifest.addons[addon]); +} + async function setupAddons(app, addons) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); diff --git a/src/storage/s3.js b/src/storage/s3.js index 4844d626b..56338aa00 100644 --- a/src/storage/s3.js +++ b/src/storage/s3.js @@ -271,7 +271,7 @@ class S3MultipartDownloadStream extends Readable { } _nextDownload() { - let len = 0; + let len; if (this._readSize + this._blockSize < this._fileSize) { len = this._blockSize; } else { diff --git a/src/tasks.js b/src/tasks.js index 327b9ca79..a0e702792 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -194,7 +194,7 @@ async function startTask(id, options) { assert.ok(sudoError, 'sudo should have errored because task did not complete!'); // taskworker.sh forwards the exit code of the actual worker. It's either a raw signal number OR the exit code - let taskError = null; + let taskError; if (sudoError.timedOut) taskError = { message: `Task ${id} timed out`, code: ETIMEOUT }; else if (sudoError.code === 70) taskError = { message: `Task ${id} stopped`, code: ESTOPPED }; // set by taskworker SIGTERM else if (sudoError.code === 9 /* SIGKILL */) taskError = { message: `Task ${id} ran out of memory or terminated`, code: ECRASHED }; // SIGTERM with oom gets set as 2 by nodejs diff --git a/src/test/datalayout-test.js b/src/test/datalayout-test.js index 72fce39fe..1acf995e6 100644 --- a/src/test/datalayout-test.js +++ b/src/test/datalayout-test.js @@ -4,7 +4,6 @@ import DataLayout from '../datalayout.js'; import expect from 'expect.js'; /* global describe:false */ -/* global before:false */ describe('DataLayout', function () { describe('no dirMap', function () { diff --git a/src/test/syncer-test.js b/src/test/syncer-test.js index 5d174a713..2153e7ef3 100644 --- a/src/test/syncer-test.js +++ b/src/test/syncer-test.js @@ -280,7 +280,8 @@ describe('Syncer', function () { }); const dataLayout = new DataLayout(gTmpDir, []); - let changes = await getChanges(dataLayout); + let changes; + await getChanges(dataLayout); execSync(`rm a; \ mkdir a; \ diff --git a/src/test/underscore-test.js b/src/test/underscore-test.js index 37e540fc7..d3e25a652 100644 --- a/src/test/underscore-test.js +++ b/src/test/underscore-test.js @@ -1,4 +1,4 @@ -/* global it, describe, before, after */ +/* global it, describe */ import expect from 'expect.js'; import _ from '../underscore.js'; diff --git a/src/test/validator-test.js b/src/test/validator-test.js index 0a2e54a55..adbdcde60 100644 --- a/src/test/validator-test.js +++ b/src/test/validator-test.js @@ -1,4 +1,4 @@ -/* global it, describe, before, after */ +/* global it, describe */ import expect from 'expect.js'; import validator from '../validator.js'; diff --git a/src/users.js b/src/users.js index 18a436d2b..a825bf681 100644 --- a/src/users.js +++ b/src/users.js @@ -600,7 +600,7 @@ async function verify(user, password, identifier, options) { return user; } - let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds + let localTotpCheck; // does 2fa need to be verified with local database 2fa creds if (user.source === 'ldap') { await externalLdap.verifyPassword(user.username, password, options); const externalLdapConfig = await externalLdap.getConfig();