deploy1 6 年 前
コミット
13f825b844
18 ファイル変更741 行追加0 行削除
  1. 3 0
      .dockerignore
  2. 2 0
      .gitignore
  3. 100 0
      CHANGELOG
  4. 37 0
      CloudronManifest.json
  5. 9 0
      DESCRIPTION.md
  6. 30 0
      Dockerfile
  7. 9 0
      LICENSE
  8. 3 0
      POSTINSTALL.md
  9. 1 0
      README.md
  10. BIN
      logo.png
  11. BIN
      screenshots/etherpad-01.png
  12. BIN
      screenshots/etherpad-02.png
  13. BIN
      screenshots/etherpad-03.png
  14. 181 0
      settings.json.template
  15. 65 0
      start.sh
  16. 7 0
      test/.jshintrc
  17. 23 0
      test/package.json
  18. 271 0
      test/test.js

+ 3 - 0
.dockerignore

@@ -0,0 +1,3 @@
+test/*
+screenshots
+

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+node_modules/
+

+ 100 - 0
CHANGELOG

@@ -0,0 +1,100 @@
+[0.3.0]
+* Initial version
+
+[0.4.0]
+* Update to upstream 1.6.0
+
+[0.4.1]
+* Remove the default pad text
+
+[0.4.2]
+* Update to latest auth module
+
+[0.4.3]
+* Update to latest base image
+
+[0.5.0]
+* Update to upstream version 1.6.1
+* Update base image to 0.10.0
+* Support plugin installation
+
+[0.6.0]
+* Support customizing the config file in the admin panel
+* Enable abiword import/export
+
+[0.7.0]
+* Add basic styling
+* Add pad listing
+
+[1.0.0]
+* Support deeplinks across logins
+
+[1.1.0]
+* Update etherpad to 1.6.2
+
+[1.2.0]
+* Preserve APIKEY and SESSIONKEY
+* Rework installation to not rely on installDeps.sh
+* Install ep_cloudron module in the Dockerfile and not at runtime
+* Rework how node_modules is setup
+
+[1.2.1]
+* Preserve APIKEY and SESSIONKEY
+* Rework installation to not rely on installDeps.sh
+* Install ep_cloudron module in the Dockerfile and not at runtime
+* Rework how node_modules is setup
+
+[1.2.2]
+* Preserve APIKEY and SESSIONKEY
+* Rework installation to not rely on installDeps.sh
+* Install ep_cloudron module in the Dockerfile and not at runtime
+* Rework how node_modules is setup
+
+[1.3.0]
+* Support plugin installation
+* Custom settings can be saved in `/app/data/settings.json`
+
+[1.3.1]
+* Support plugin installation
+* Custom settings can be saved in `/app/data/settings.json`
+
+[1.4.0]
+* Optional SSO - Install etherpad without cloudron auth
+
+[1.5.0]
+* Add support for [Etherpad API](http://etherpad.org/doc/v1.3.0/#index_http_api)
+
+[1.6.0]
+* Update Etherpad to 1.6.3
+
+[1.6.1]
+* Update Etherpad to 1.6.5
+* Fixes multiple [security issues](http://blog.etherpad.org/2018/04/07/important-release-1-6-4/)
+
+[1.7.0]
+* Update Etherpad to 1.6.6
+* Upgrade node.js to 8.11.1
+
+[1.8.0]
+* Update node.js to 9.9.0 to fix plugin installation issue
+* Improve the set of default plugins
+
+[2.0.0]
+* Use LDAP login instead of OAuth
+* Restyle the landing page and document listing
+
+[2.1.0]
+* Update Etherpad to 1.7.0
+* FIX: getLineHTMLForExport() no longer produces multiple copies of a line. WARNING: this could potentially break some plugins
+* FIX: authorship of bullet points no longer changes when a second author edits them
+* FIX: improved Firefox compatibility (non printable keys)
+* FIX: getPadPlainText() was not working
+* SECURITY: updated MySQL, Elasticsearch and PostgreSQL drivers
+* SECURITY: started updating deprecated code and packages
+
+[2.2.0]
+* Use latest base image
+
+[2.2.1]
+* Fix issue where npm modules were symlinked to /tmp instead of /run
+

+ 37 - 0
CloudronManifest.json

@@ -0,0 +1,37 @@
+{
+  "id": "alt.org.etherpad.cloudronapp",
+  "title": "Etherpad",
+  "author": "Etherpad Developers",
+  "description": "file://DESCRIPTION.md",
+  "tagline": " Collaborating in real-time",
+  "version": "0.0.1",
+  "healthCheckPath": "/healthcheck",
+  "httpPort": 9001,
+  "manifestVersion": 1,
+  "website": "http://etherpad.org/",
+  "contactEmail": "support@iske.dk",
+  "icon": "file://logo.png",
+  "postInstallMessage": "file://POSTINSTALL.md",
+  "mediaLinks": [
+    "https://s3.amazonaws.com/cloudron-app-screenshots/org.etherpad.cloudronapp/9bce548d411c917ef0a758fddf07a3ba94fe08bb/etherpad-01.png",
+    "https://s3.amazonaws.com/cloudron-app-screenshots/org.etherpad.cloudronapp/9bce548d411c917ef0a758fddf07a3ba94fe08bb/etherpad-02.png",
+    "https://s3.amazonaws.com/cloudron-app-screenshots/org.etherpad.cloudronapp/9bce548d411c917ef0a758fddf07a3ba94fe08bb/etherpad-03.png"
+  ],
+  "addons": {
+    "mysql": {},
+    "ldap": {},
+    "localstorage": {}
+  },
+  "memoryLimit": 524288000,
+  "tags": [
+    "document",
+    "docs",
+    "collaboration",
+    "editor",
+    "notes"
+  ],
+  "changelog": "file://CHANGELOG",
+  "minBoxVersion": "1.8.1",
+  "documentationUrl": "https://cloudron.io/documentation/apps/etherpad/",
+  "optionalSso": true
+}

+ 9 - 0
DESCRIPTION.md

@@ -0,0 +1,9 @@
+This app packages Etherpad <upstream>1.7.0</upstream>.
+
+### Collaborating in really real-time
+
+No more sending your stuff back and forth via email, just set up a pad, share the link and start collaborating!
+
+Etherpad allows you to edit documents collaboratively in real-time, much like a live multi-player editor that runs in your browser. Write articles, press releases, to-do lists, etc. together with your friends, fellow students or colleagues, all working on the same document at the same time.
+
+All instances provide access to all data through a well-documented API and supports import/export to many major data exchange formats.

+ 30 - 0
Dockerfile

@@ -0,0 +1,30 @@
+FROM docker.iske.dk/base-image:0.0.1
+
+RUN mkdir -p /app/code
+WORKDIR /app/code
+
+RUN apt-get update -y && apt-get install -y abiword tidy && rm -r /var/cache/apt /var/lib/apt/lists
+
+RUN curl -L https://github.com/ether/etherpad-lite/tarball/1.7.0 | tar -xz --strip-components 1 -f -
+
+# node_modules have to be in data to allow plugins to be installable at runtime
+RUN cd /app/code/src && npm install && \
+    ln -s /app/data/node_modules /app/code/node_modules
+
+# https://github.com/ether/etherpad-lite/issues/2683
+RUN touch src/.ep_initialized
+
+COPY settings.json.template /app/code/settings.json.template
+RUN mv src/static/custom src/static/custom_templates && ln -s /app/data/custom src/static/custom
+
+# make these writable (var is used for cache)
+# node_modules contains plugins, the etherpad code is only linked into /app/data/node_modules
+RUN ln -s /app/data/APIKEY.txt /app/code/APIKEY.txt && \
+    ln -s /app/data/SESSIONKEY.txt /app/code/SESSIONKEY.txt && \
+    rm -rf /app/code/var && ln -s /run/etherpad-lite/var /app/code/var && \
+    rm -rf /home/cloudron/.npm && ln -s /run/etherpad-lite/npm /home/cloudron/.npm && \
+    rm -rf /root/.npm && ln -s /run/etherpad-lite/npm /root/.npm
+
+COPY start.sh /app/code/
+
+CMD [ "/app/code/start.sh" ]

+ 9 - 0
LICENSE

@@ -0,0 +1,9 @@
+MIT License (MIT)
+Copyright (c) 2016 Cloudron UG
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+

+ 3 - 0
POSTINSTALL.md

@@ -0,0 +1,3 @@
+To install plugins or change the configuration, visit the admin interface at `/admin`.
+
+

+ 1 - 0
README.md

@@ -0,0 +1 @@
+##etherpat-lite-app

BIN
logo.png


BIN
screenshots/etherpad-01.png


BIN
screenshots/etherpad-02.png


BIN
screenshots/etherpad-03.png


+ 181 - 0
settings.json.template

@@ -0,0 +1,181 @@
+/*
+  This file must be valid JSON. But comments are allowed
+
+  Please edit settings.json, not settings.json.template
+
+  To still commit settings without credentials you can
+  store any credential settings in credentials.json
+*/
+{
+  // Name your instance!
+  "title": "Etherpad",
+
+  // favicon default name
+  // alternatively, set up a fully specified Url to your own favicon
+  "favicon": "favicon.ico",
+
+  //IP and port which etherpad should bind at
+  "ip": "0.0.0.0",
+  "port" : 9001,
+
+  // Option to hide/show the settings.json in admin page, default option is set to true
+  "showSettingsInAdminPage" : false,
+
+   "dbType" : "mysql",
+   "dbSettings" : {
+                    "user"    : "##MYSQL_USERNAME",
+                    "host"    : "##MYSQL_HOST",
+                    "password": "##MYSQL_PASSWORD",
+                    "database": "##MYSQL_DATABASE",
+                    "port"    : "##MYSQL_PORT",
+                    "charset" : "utf8mb4"
+                  },
+
+  //the default text of a pad
+  "defaultPadText" : "Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at http:\/\/etherpad.org\n",
+
+  /* Default Pad behavior, users can override by changing */
+  "padOptions": {
+    "noColors": false,
+    "showControls": true,
+    "showChat": true,
+    "showLineNumbers": false,
+    "useMonospaceFont": false,
+    "userName": false,
+    "userColor": false,
+    "rtl": false,
+    "alwaysShowChat": false,
+    "chatAndUsers": false,
+    "lang": "en-gb"
+  },
+
+  /* Pad Shortcut Keys */
+  "padShortcutEnabled" : {
+    "altF9"     : true, /* focus on the File Menu and/or editbar */
+    "altC"      : true, /* focus on the Chat window */
+    "cmdShift2" : true, /* shows a gritter popup showing a line author */
+    "delete"    : true,
+    "return"    : true,
+    "esc"       : true, /* in mozilla versions 14-19 avoid reconnecting pad */
+    "cmdS"      : true, /* save a revision */
+    "tab"       : true, /* indent */
+    "cmdZ"      : true, /* undo/redo */
+    "cmdY"      : true, /* redo */
+    "cmdI"      : true, /* italic */
+    "cmdB"      : true, /* bold */
+    "cmdU"      : true, /* underline */
+    "cmd5"      : true, /* strike through */
+    "cmdShiftL" : true, /* unordered list */
+    "cmdShiftN" : true, /* ordered list */
+    "cmdShift1" : true, /* ordered list */
+    "cmdShiftC" : true, /* clear authorship */
+    "cmdH"      : true, /* backspace */
+    "ctrlHome"  : true, /* scroll to top of pad */
+    "pageUp"    : true,
+    "pageDown"  : true
+  },
+
+  /* Should we suppress errors from being visible in the default Pad Text? */
+  "suppressErrorsInPadText" : false,
+
+  /* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
+  "requireSession" : false,
+
+  /* Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. */
+  "editOnly" : false,
+
+  /* Users, who have a valid session, automatically get granted access to password protected pads */
+  "sessionNoPassword" : false,
+
+  /* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly,
+     but makes it impossible to debug the javascript/css */
+  "minify" : true,
+
+  /* How long may clients use served javascript code (in seconds)? Without versioning this
+     may cause problems during deployment. Set to 0 to disable caching */
+  "maxAge" : 21600, // 60 * 60 * 6 = 6 hours
+
+  /* This is the absolute path to the Abiword executable. Setting it to null, disables abiword.
+     Abiword is needed to advanced import/export features of pads*/
+  "abiword" : "/usr/bin/abiword",
+
+  /* This is the absolute path to the soffice executable. Setting it to null, disables LibreOffice exporting.
+     LibreOffice can be used in lieu of Abiword to export pads */
+  "soffice" : null,
+
+  /* This is the path to the Tidy executable. Setting it to null, disables Tidy.
+     Tidy is used to improve the quality of exported pads*/
+  "tidyHtml" : null,
+
+  /* Allow import of file types other than the supported types: txt, doc, docx, rtf, odt, html & htm */
+  "allowUnknownFileEnds" : true,
+
+  /* This setting is used if you require authentication of all users.
+     Note: /admin always requires authentication. */
+  "requireAuthentication" : true,
+
+  /* Require authorization by a module, or a user with is_admin set, see below. */
+  "requireAuthorization" : true,
+
+  /*when you use NginX or another proxy/ load-balancer set this to true*/
+  "trustProxy" : true,
+
+  /* Privacy: disable IP logging */
+  "disableIPlogging" : false,
+
+  /* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
+     message is shown to user. Set to 0 to disable automatic reconnection */
+  "automaticReconnectionTimeout" : 0,
+
+  /* Users for basic authentication. is_admin = true gives access to /admin.
+     If you do not uncomment this, /admin will not be available! */
+  "users": {
+    "cloudron": {}
+  },
+
+  // restrict socket.io transport methods
+  "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"],
+
+  // Allow Load Testing tools to hit the Etherpad Instance.  Warning this will disable security on the instance.
+  "loadTest": false,
+
+  // Disable indentation on new line when previous line ends with some special chars (':', '[', '(', '{')
+  /*
+  "indentationOnNewLine": false,
+  */
+
+  /* The toolbar buttons configuration.
+  "toolbar": {
+    "left": [
+      ["bold", "italic", "underline", "strikethrough"],
+      ["orderedlist", "unorderedlist", "indent", "outdent"],
+      ["undo", "redo"],
+      ["clearauthorship"]
+    ],
+    "right": [
+      ["importexport", "timeslider", "savedrevision"],
+      ["settings", "embed"],
+      ["showusers"]
+    ],
+    "timeslider": [
+      ["timeslider_export", "timeslider_returnToPad"]
+    ]
+  },
+  */
+
+  /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
+  "loglevel": "INFO",
+
+  //Logging configuration. See log4js documentation for further information
+  // https://github.com/nomiddlename/log4js-node
+  // You can add as many appenders as you want here:
+  "logconfig" :
+    { "appenders": [
+        { "type": "console"
+        //, "category": "access"// only logs pad access
+        }
+      ]
+    },
+
+  "ep_page_view_default" : true
+}

+ 65 - 0
start.sh

@@ -0,0 +1,65 @@
+#!/bin/bash
+
+set -eu
+
+echo "=========================="
+echo "      Etherpad start      "
+echo "=========================="
+
+# make npm behave a bit to improve our log display
+export npm_config_progress=false
+export npm_config_color=false
+export npm_config_spin=false
+
+echo "=> Ensure directories"
+mkdir -p /run/etherpad-lite/run /run/etherpad-lite/var /app/data/node_modules /run/etherpad-lite/npm /app/data/custom
+
+# the idea here is make the node_modules dir writable so that ep can install plugins there
+# the ep_cloudron has to be symlinked at the top level so that ep can find it
+echo "=> Fixing up node_modules"
+ln -Tsf /app/code/src /app/data/node_modules/ep_etherpad-lite # this symlink is "require"d by plugins
+
+echo "=> Ensuring cloudron plugin"
+npm install ep_cloudron@2.0.4
+# Use this instead of the line above during ep_cloudron development
+# npm install git+https://git.cloudron.io/cloudron/ep_cloudron.git\#devel --force
+
+# We setup the app with some useful default plugins, the user may or may not uninstall them afterwards
+if [[ ! -f "/app/data/.installed" ]]; then
+  echo "=> First run, installing default plugins"
+  npm install --no-package-lock ep_page_view ep_headings2 ep_font_color ep_font_family ep_font_size ep_superscript ep_subscript
+  touch "/app/data/.installed"
+fi
+
+echo "=> Ensuring templates"
+for f in "index" "pad" "timeslider"; do
+  if [[ ! -f "/app/data/custom/$f.js" ]]; then
+    cp "/app/code/src/static/custom_templates/js.template" "/app/data/custom/$f.js"
+  fi
+
+  if [[ ! -f "/app/data/custom/$f.css" ]]; then
+    cp "/app/code/src/static/custom_templates/css.template" "/app/data/custom/$f.css"
+  fi
+done
+
+echo "=> Generating settings.json"
+sed -e "s/##MYSQL_HOST/${MYSQL_HOST}/g" \
+    -e "s/##MYSQL_PORT/${MYSQL_PORT}/g" \
+    -e "s/##MYSQL_USERNAME/${MYSQL_USERNAME}/g" \
+    -e "s/##MYSQL_PASSWORD/${MYSQL_PASSWORD}/g" \
+    -e "s/##MYSQL_DATABASE/${MYSQL_DATABASE}/g" \
+    /app/code/settings.json.template > "/run/etherpad-lite/settings.json"
+
+echo "=> Ensure /app/data/settings.json"
+if [[ ! -f "/app/data/settings.json" ]]; then
+  echo -e "{\n\n}" > "/app/data/settings.json"
+fi
+
+echo "=> Ensure folder permissions"
+chown -R cloudron:cloudron /run/etherpad-lite /app/data
+
+echo "=> Starting etherpad"
+export NODE_ENV=production
+# etherpad expects APIKEY, SESSIONKEY, node_modules in curdir
+cd /app/code
+exec /usr/local/bin/gosu cloudron:cloudron node /app/code/src/node/server.js --settings /run/etherpad-lite/settings.json --credentials /app/data/settings.json

+ 7 - 0
test/.jshintrc

@@ -0,0 +1,7 @@
+{
+    "node": true,
+    "browser": true,
+    "unused": true,
+    "globalstrict": true,
+    "predef": [ "angular", "$", "describe", "it", "before", "after" ]
+}

+ 23 - 0
test/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "test",
+  "version": "1.0.0",
+  "description": "",
+  "main": "test.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "devDependencies": {
+    "ejs": "^2.3.4",
+    "expect.js": "^0.3.1",
+    "mkdirp": "^0.5.1",
+    "mocha": "^2.3.4",
+    "rimraf": "^2.4.4",
+    "selenium-webdriver": "^2.48.2",
+    "superagent": "^1.4.0"
+  },
+  "dependencies": {
+    "chromedriver": "^2.37.0"
+  }
+}

+ 271 - 0
test/test.js

@@ -0,0 +1,271 @@
+#!/usr/bin/env node
+
+'use strict';
+
+require('chromedriver');
+
+var execSync = require('child_process').execSync,
+    expect = require('expect.js'),
+    fs = require('fs'),
+    path = require('path'),
+    superagent = require('superagent'),
+    util = require('util');
+
+var by = require('selenium-webdriver').By,
+    until = require('selenium-webdriver').until;
+
+if (!process.env.USERNAME || !process.env.PASSWORD) {
+    console.log('USERNAME and PASSWORD env vars need to be set');
+    process.exit(1);
+}
+
+describe('Application life cycle test', function () {
+    this.timeout(0);
+
+    var chrome = require('selenium-webdriver/chrome');
+    var browser = new chrome.Driver();
+
+    var TEST_TIMEOUT = parseInt(process.env.TIMEOUT, 10) || 20000;
+    var LOCATION = 'test';
+    var app, apiKey;
+    var username = process.env.USERNAME;
+    var password = process.env.PASSWORD;
+
+    after(function () {
+        browser.quit();
+    });
+
+    function getAppInfo() {
+        var inspect = JSON.parse(execSync('cloudron inspect'));
+        app = inspect.apps.filter(function (a) { return a.location === LOCATION || a.location === LOCATION + '2'; })[0];
+        expect(app).to.be.an('object');
+    }
+
+    function login(username, password, done) {
+        browser.manage().deleteAllCookies().then(function () {
+            return browser.get('https://' + app.fqdn);
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.id('username')));
+        }).then(function () {
+            return browser.findElement(by.id('username')).sendKeys(username);
+        }).then(function () {
+            return browser.findElement(by.id('password')).sendKeys(password);
+        }).then(function () {
+            return browser.findElement(by.tagName('form')).submit();
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.id('documentName')));
+        }).then(function () {
+            done();
+        });
+    }
+
+    function createDocument(done) {
+        browser.get('https://' + app.fqdn).then(function () {
+            return browser.wait(until.elementLocated(by.id('documentName')));
+        }).then(function () {
+            return browser.findElement(by.id('documentName')).sendKeys('paddington');
+        }).then(function () {
+            return browser.findElement(by.tagName('form')).submit();
+        }).then(function () {
+            return browser.wait(function () {
+                return browser.getCurrentUrl().then(function (url) {
+                    return url === 'https://' + app.fqdn + '/p/paddington';
+                });
+            }, TEST_TIMEOUT);
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.name('ace_outer')), TEST_TIMEOUT);
+        }).then(function () {
+            return browser.switchTo().frame(browser.findElement(by.name('ace_outer')));
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.name('ace_inner')), TEST_TIMEOUT);
+        }).then(function () {
+            return browser.switchTo().frame(browser.findElement(by.name('ace_inner')));
+        }).then(function () {
+            return browser.findElement(by.xpath('//body')).sendKeys('Cloudron');
+        }).then(function () { // do not navigate away too fast before ep commits the text
+            return browser.sleep(5000);
+        }).then(function () {
+            done();
+        });
+    }
+
+    function getDocument(done) {
+        browser.get('https://' + app.fqdn + '/p/paddington').then(function () {
+            return browser.wait(function () {
+                return browser.getCurrentUrl().then(function (url) {
+                    return url === 'https://' + app.fqdn + '/p/paddington';
+                });
+            }, TEST_TIMEOUT);
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.name('ace_outer')), TEST_TIMEOUT);
+        }).then(function () {
+            return browser.switchTo().frame(browser.findElement(by.name('ace_outer')));
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.name('ace_inner')), TEST_TIMEOUT);
+        }).then(function () {
+            return browser.switchTo().frame(browser.findElement(by.name('ace_inner')));
+        }).then(function () {
+            return browser.findElement(by.xpath('//span[text()="Cloudron"]'));
+        }).then(function () {
+            done();
+        });
+    }
+
+    function installPlugin(done) {
+        browser.get('https://' + app.fqdn + '/admin/plugins').then(function () {
+            return browser.wait(until.elementLocated(by.xpath('//a[text()="delete_after_delay"]')), TEST_TIMEOUT);
+        }).then(function () {
+            var button = browser.findElement(by.xpath('//tr[@class="ep_delete_after_delay"]//input[@class="do-install"]'));
+            return browser.executeScript('arguments[0].scrollIntoView(true)', button);
+        }).then(function () {
+            return browser.findElement(by.xpath('//tr[@class="ep_delete_after_delay"]//input[@class="do-install"]')).click();
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.xpath('//tr[@class="ep_delete_after_delay"]//input[@class="do-uninstall"]')), TEST_TIMEOUT);
+        }).then(function () {
+            done();
+        });
+    }
+
+    function checkPlugin(done) {
+        browser.get('https://' + app.fqdn + '/admin/plugins').then(function () {
+            return browser.wait(until.elementLocated(by.xpath('//tr[@class="ep_delete_after_delay"]')), TEST_TIMEOUT);
+        }).then(function () {
+            return browser.wait(until.elementLocated(by.xpath('//tr[@class="ep_delete_after_delay"]//input[@class="do-uninstall"]')), TEST_TIMEOUT);
+        }).then(function () {
+            done();
+        });
+    }
+
+    function getApiKey(done) {
+        var out = execSync(util.format('cloudron exec --app %s -- cat /app/data/APIKEY.txt', app.id));
+        apiKey = out.toString('utf8');
+        console.log('apiKey is ' + apiKey);
+        expect(apiKey.length).to.be(64);
+        done();
+    }
+
+    function testApiAccess(done) {
+        superagent.get('https://' + app.fqdn + '/api/1.2.7/listAllPads').query({ apikey: apiKey }).end(function (error, result) {
+            expect(error).to.be(null);
+            expect(result.statusCode).to.be(200);
+            expect(result.body.data.padIDs).to.contain('paddington');
+            done();
+        });
+    }
+
+    xit('build app', function () {
+        execSync('cloudron build', { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+    });
+
+    it('install app without sso', function () {
+        execSync('cloudron install --new --wait --no-sso --location ' + LOCATION, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+    });
+
+    it('can get app information', getAppInfo);
+    it('can setup custom users', function () {
+        fs.writeFileSync('/tmp/settings.json', JSON.stringify({ users: { admin: { password: 'secret123', is_admin: true } } }), 'utf8');
+        execSync('cloudron push --app ' + app.id + ' /tmp/settings.json /app/data/settings.json', { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+        execSync('cloudron restart --wait --app ' + app.id);
+    });
+    it('can login', login.bind(null, 'admin', 'secret123'));
+    it('can create document', createDocument);
+    it('can get existing document', getDocument);
+    it('can install plugin', installPlugin);
+    it('can get api key', getApiKey);
+    it('can access api', testApiAccess);
+    it('uninstall app', function (done) {
+        // ensure we don't hit NXDOMAIN in the mean time
+        browser.get('about:blank').then(function () {
+            execSync('cloudron uninstall --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+            done();
+        });
+    });
+
+    it('install app', function () {
+        execSync('cloudron install --new --wait --location ' + LOCATION, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+    });
+
+    it('can get app information', getAppInfo);
+
+    it('can login', login.bind(null, username, password));
+    it('can create document', createDocument);
+    it('can get existing document', getDocument);
+    it('can install plugin', installPlugin);
+    it('can get api key', getApiKey);
+    it('can access api', testApiAccess);
+
+    it('can restart app', function () {
+        execSync('cloudron restart --wait --app ' + app.id);
+    });
+
+    it('can get existing document', getDocument);
+    it('can check plugin', checkPlugin);
+    it('can access api', testApiAccess);
+
+    it('backup app', function () {
+        execSync('cloudron backup create --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+    });
+
+    it('restore app', function () {
+        execSync('cloudron restore --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+    });
+
+    it('can get existing document', getDocument);
+    it('can check plugin', checkPlugin);
+    it('can access api', testApiAccess);
+
+    it('move to different location', function (done) {
+        browser.manage().deleteAllCookies();
+
+        // ensure we don't hit NXDOMAIN in the mean time
+        browser.get('about:blank').then(function () {
+            execSync('cloudron configure --wait --location ' + LOCATION + '2 --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+
+            var inspect = JSON.parse(execSync('cloudron inspect'));
+            app = inspect.apps.filter(function (a) { return a.location === LOCATION + '2'; })[0];
+            expect(app).to.be.an('object');
+
+            done();
+        });
+    });
+
+    it('can login', login.bind(null, username, password));
+    it('can get existing document', getDocument);
+    it('can check plugin', checkPlugin);
+    it('can access api', testApiAccess);
+
+    it('uninstall app', function (done) {
+        // ensure we don't hit NXDOMAIN in the mean time
+        browser.get('about:blank').then(function () {
+            execSync('cloudron uninstall --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+            done();
+        });
+    });
+
+    // test update
+    it('can install app', function () {
+        execSync('cloudron install --new --wait --appstore-id org.etherpad.cloudronapp --location ' + LOCATION, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+    });
+
+    it('can get app information', getAppInfo);
+    it('can login', login.bind(null, username, password));
+    it('can create document', createDocument);
+    it('can get existing document', getDocument);
+    it('can install plugin', installPlugin);
+    it('can get api key', getApiKey);
+
+    it('can update', function () {
+        execSync('cloudron install --wait --app ' + LOCATION, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+    });
+
+    it('can get existing document', getDocument);
+    it('can check plugin', checkPlugin);
+    it('can access api', testApiAccess);
+    it('uninstall app', function (done) {
+        // ensure we don't hit NXDOMAIN in the mean time
+        browser.get('about:blank').then(function () {
+            execSync('cloudron uninstall --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' });
+            done();
+        });
+    });
+});