OIDC auth implemented, tests updated

This commit is contained in:
Vladimir D 2024-02-14 14:13:42 +04:00 committed by Johannes Zellner
parent fdc4e20c77
commit 146b5ac17e
6 changed files with 160 additions and 93 deletions

View file

@ -16,7 +16,7 @@
"postgresql": {}, "postgresql": {},
"redis": {}, "redis": {},
"sendmail": {}, "sendmail": {},
"ldap": {}, "oidc": { "loginRedirectUri": "/auth/auth/openid_connect/callback" },
"scheduler": { "scheduler": {
"cleanup": { "cleanup": {
"schedule": "11 01 * * *", "schedule": "11 01 * * *",

View file

@ -32,15 +32,15 @@ SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none SMTP_OPENSSL_VERIFY_MODE=none
# SSO configuration # SSO configuration
LDAP_ENABLED= OIDC_ENABLED=
LDAP_HOST= OIDC_DISPLAY_NAME=
LDAP_PORT= OIDC_ISSUER=
LDAP_BASE= OIDC_CLIENT_ID=
LDAP_BIND_DN= OIDC_CLIENT_SECRET=
LDAP_PASSWORD= OIDC_REDIRECT_URI=
LDAP_UID=username OIDC_DISCOVERY=
LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(mail=%{email})) OIDC_SCOPE=
LDAP_METHOD=plain OIDC_UID_FIELD=
# Application secrets # Application secrets
SECRET_KEY_BASE= SECRET_KEY_BASE=

View file

@ -30,16 +30,40 @@ sed -e "s/DB_HOST=.*/DB_HOST=${CLOUDRON_POSTGRESQL_HOST}/g" \
-e "s/WEB_DOMAIN=.*/WEB_DOMAIN=${CLOUDRON_APP_DOMAIN}/g" \ -e "s/WEB_DOMAIN=.*/WEB_DOMAIN=${CLOUDRON_APP_DOMAIN}/g" \
-i /app/data/env.production -i /app/data/env.production
if [[ -n "${CLOUDRON_LDAP_SERVER:-}" ]]; then # migrate LDAP settings to OIDC
sed -e "s/LDAP_ENABLED=.*/LDAP_ENABLED=true/g" \ if grep -q "^LDAP_ENABLED" /app/data/env.production; then
-e "s/LDAP_HOST=.*/LDAP_HOST=${CLOUDRON_LDAP_SERVER}/g" \ # get rid LDAP settings
-e "s/LDAP_PORT=.*/LDAP_PORT=${CLOUDRON_LDAP_PORT}/g" \ sed -e "s/LDAP_.*//g" \
-e "s/LDAP_BASE=.*/LDAP_BASE=${CLOUDRON_LDAP_USERS_BASE_DN}/g" \ -e "s/# SSO configuration//g" \
-e "s/LDAP_BIND_DN=.*/LDAP_BIND_DN=${CLOUDRON_LDAP_BIND_DN}/g" \ -i /app/data/env.production
-e "s/LDAP_PASSWORD=.*/LDAP_PASSWORD=${CLOUDRON_LDAP_BIND_PASSWORD}/g" \
cat >> /app/data/env.production <<EOT
# SSO configuration
OIDC_ENABLED=
OIDC_DISPLAY_NAME=
OIDC_ISSUER=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_REDIRECT_URI=
OIDC_DISCOVERY=
OIDC_SCOPE=
OIDC_UID_FIELD=
EOT
fi
if [[ -n "${CLOUDRON_OIDC_ISSUER:-}" ]]; then
echo "==> Setting up OIDC"
sed -e "s/OIDC_ENABLED=.*/OIDC_ENABLED=true/g" \
-e "s/OIDC_DISPLAY_NAME=.*/OIDC_DISPLAY_NAME=Cloudron/g" \
-e "s/OIDC_ISSUER=.*/OIDC_ISSUER=${CLOUDRON_OIDC_ISSUER//\//\\\/}/g" \
-e "s/OIDC_CLIENT_ID=.*/OIDC_CLIENT_ID=${CLOUDRON_OIDC_CLIENT_ID}/g" \
-e "s/OIDC_CLIENT_SECRET=.*/OIDC_CLIENT_SECRET=${CLOUDRON_OIDC_CLIENT_SECRET}/g" \
-e "s/OIDC_REDIRECT_URI=.*/OIDC_REDIRECT_URI=${CLOUDRON_APP_ORIGIN//\//\\\/}\/auth\/auth\/openid_connect\/callback/g" \
-e "s/OIDC_DISCOVERY=.*/OIDC_DISCOVERY=true/g" \
-e "s/OIDC_SCOPE=.*/OIDC_SCOPE=openid,profile,email/g" \
-e "s/OIDC_UID_FIELD=.*/OIDC_UID_FIELD=sub/g" \
-i /app/data/env.production -i /app/data/env.production
else
sed -e "s/LDAP_ENABLED=.*/LDAP_ENABLED=false/g" -i /app/data/env.production
fi fi
rm -f /run/mastodon/Gemfile.lock && cp /app/code/Gemfile.lock.original /run/mastodon/Gemfile.lock rm -f /run/mastodon/Gemfile.lock && cp /app/code/Gemfile.lock.original /run/mastodon/Gemfile.lock
@ -57,7 +81,7 @@ if grep -q "^SECRET_KEY_BASE=$" /app/data/env.production; then
echo "==> Init database" echo "==> Init database"
HOME=/app/data SAFETY_ASSURED=1 bundle exec rails db:schema:load db:seed HOME=/app/data SAFETY_ASSURED=1 bundle exec rails db:schema:load db:seed
if [[ -n "${CLOUDRON_LDAP_SERVER:-}" ]]; then if [[ -n "${CLOUDRON_OIDC_ISSUER:-}" ]]; then
echo "Disabling registration by default" echo "Disabling registration by default"
PGPASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD} psql -h ${CLOUDRON_POSTGRESQL_HOST} -p ${CLOUDRON_POSTGRESQL_PORT} -U ${CLOUDRON_POSTGRESQL_USERNAME} -d ${CLOUDRON_POSTGRESQL_DATABASE} \ PGPASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD} psql -h ${CLOUDRON_POSTGRESQL_HOST} -p ${CLOUDRON_POSTGRESQL_PORT} -U ${CLOUDRON_POSTGRESQL_USERNAME} -d ${CLOUDRON_POSTGRESQL_DATABASE} \
-c "INSERT INTO settings (var, value) VALUES ('registrations_mode', 'none')" -c "INSERT INTO settings (var, value) VALUES ('registrations_mode', 'none')"

74
test/package-lock.json generated
View file

@ -13,7 +13,7 @@
}, },
"devDependencies": { "devDependencies": {
"expect.js": "^0.3.1", "expect.js": "^0.3.1",
"mocha": "^10.2.0", "mocha": "^10.3.0",
"selenium-webdriver": "^4.17.0" "selenium-webdriver": "^4.17.0"
} }
}, },
@ -825,9 +825,9 @@
} }
}, },
"node_modules/mocha": { "node_modules/mocha": {
"version": "10.2.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz",
"integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ansi-colors": "4.1.1", "ansi-colors": "4.1.1",
@ -837,13 +837,12 @@
"diff": "5.0.0", "diff": "5.0.0",
"escape-string-regexp": "4.0.0", "escape-string-regexp": "4.0.0",
"find-up": "5.0.0", "find-up": "5.0.0",
"glob": "7.2.0", "glob": "8.1.0",
"he": "1.2.0", "he": "1.2.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"log-symbols": "4.1.0", "log-symbols": "4.1.0",
"minimatch": "5.0.1", "minimatch": "5.0.1",
"ms": "2.1.3", "ms": "2.1.3",
"nanoid": "3.3.3",
"serialize-javascript": "6.0.0", "serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1", "strip-json-comments": "3.1.1",
"supports-color": "8.1.1", "supports-color": "8.1.1",
@ -858,10 +857,6 @@
}, },
"engines": { "engines": {
"node": ">= 14.0.0" "node": ">= 14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mochajs"
} }
}, },
"node_modules/mocha/node_modules/brace-expansion": { "node_modules/mocha/node_modules/brace-expansion": {
@ -873,6 +868,25 @@
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"node_modules/mocha/node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mocha/node_modules/minimatch": { "node_modules/mocha/node_modules/minimatch": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
@ -896,18 +910,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/nanoid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -1967,9 +1969,9 @@
} }
}, },
"mocha": { "mocha": {
"version": "10.2.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz",
"integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-colors": "4.1.1", "ansi-colors": "4.1.1",
@ -1979,13 +1981,12 @@
"diff": "5.0.0", "diff": "5.0.0",
"escape-string-regexp": "4.0.0", "escape-string-regexp": "4.0.0",
"find-up": "5.0.0", "find-up": "5.0.0",
"glob": "7.2.0", "glob": "8.1.0",
"he": "1.2.0", "he": "1.2.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"log-symbols": "4.1.0", "log-symbols": "4.1.0",
"minimatch": "5.0.1", "minimatch": "5.0.1",
"ms": "2.1.3", "ms": "2.1.3",
"nanoid": "3.3.3",
"serialize-javascript": "6.0.0", "serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1", "strip-json-comments": "3.1.1",
"supports-color": "8.1.1", "supports-color": "8.1.1",
@ -2004,6 +2005,19 @@
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
}
},
"minimatch": { "minimatch": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
@ -2026,12 +2040,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"nanoid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true
},
"normalize-path": { "normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View file

@ -10,7 +10,7 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"expect.js": "^0.3.1", "expect.js": "^0.3.1",
"mocha": "^10.2.0", "mocha": "^10.3.0",
"selenium-webdriver": "^4.17.0" "selenium-webdriver": "^4.17.0"
}, },
"dependencies": { "dependencies": {

View file

@ -30,6 +30,7 @@ describe('Application life cycle test', function () {
const EXEC_ARGS = { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }; const EXEC_ARGS = { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' };
let browser, app; let browser, app;
var athenticated_by_oidc = false;
let username = process.env.USERNAME; let username = process.env.USERNAME;
let password = process.env.PASSWORD; let password = process.env.PASSWORD;
let manifest = require('../CloudronManifest.json'); let manifest = require('../CloudronManifest.json');
@ -42,6 +43,15 @@ describe('Application life cycle test', function () {
browser.quit(); browser.quit();
}); });
function sleep(millis) {
return new Promise(resolve => setTimeout(resolve, millis));
}
async function waitForElement(elem) {
await browser.wait(until.elementLocated(elem), TEST_TIMEOUT);
await browser.wait(until.elementIsVisible(browser.findElement(elem)), TEST_TIMEOUT);
}
async function exists(selector) { async function exists(selector) {
await browser.wait(until.elementLocated(selector), TEST_TIMEOUT); await browser.wait(until.elementLocated(selector), TEST_TIMEOUT);
} }
@ -55,7 +65,7 @@ describe('Application life cycle test', function () {
if (mode === 'none') { if (mode === 'none') {
await browser.get('https://' + app.fqdn); await browser.get('https://' + app.fqdn);
await browser.sleep(2000); await browser.sleep(2000);
await browser.findElement(By.xpath('//div[@class="sign-in-banner"]/descendant::button/span[contains(text(), "Create account")]')).click(); await browser.findElement(By.xpath('//div[@class="sign-in-banner"]/descendant::button/span[contains(text(), "Create account")] | //div[@class="sign-in-banner"]/descendant::a/span[contains(text(), "Create account")]')).click();
await visible(By.xpath('//span[contains(text()[2], "is currently not possible")]')); await visible(By.xpath('//span[contains(text()[2], "is currently not possible")]'));
} else if (mode === 'open') { } else if (mode === 'open') {
await browser.get('https://' + app.fqdn + '/auth/sign_up'); await browser.get('https://' + app.fqdn + '/auth/sign_up');
@ -72,6 +82,28 @@ describe('Application life cycle test', function () {
await browser.sleep(3000); // can be wizard or timeline at this point await browser.sleep(3000); // can be wizard or timeline at this point
} }
async function loginOIDC(username, password) {
browser.manage().deleteAllCookies();
await browser.get(`https://${app.fqdn}/auth/sign_in`);
await browser.sleep(4000);
await browser.findElement(By.xpath('//a[contains(@class, "button") and text()="Cloudron"]')).click();
await browser.sleep(4000);
if (!athenticated_by_oidc) {
await waitForElement(By.xpath('//input[@name="username"]'));
await browser.findElement(By.xpath('//input[@name="username"]')).sendKeys(username);
await browser.findElement(By.xpath('//input[@name="password"]')).sendKeys(password);
await browser.sleep(2000);
await browser.findElement(By.xpath('//button[@type="submit" and contains(text(), "Sign in")]')).click();
await browser.sleep(2000);
athenticated_by_oidc = true;
}
await waitForElement(By.xpath('//a[contains(., "Edit profile")] | //strong[text()="Successfully authenticated from Cloudron account."]'));
}
async function logout() { async function logout() {
await browser.get('https://' + app.fqdn + '/settings/preferences/appearance'); // there is also separate login page at /users/sign_in await browser.get('https://' + app.fqdn + '/settings/preferences/appearance'); // there is also separate login page at /users/sign_in
await browser.wait(until.elementLocated(By.id('logout')), TEST_TIMEOUT); await browser.wait(until.elementLocated(By.id('logout')), TEST_TIMEOUT);
@ -97,43 +129,6 @@ describe('Application life cycle test', function () {
} }
xit('build app', function () { execSync('cloudron build', EXEC_ARGS); }); xit('build app', function () { execSync('cloudron build', EXEC_ARGS); });
it('install app', function () { execSync('cloudron install --location ' + LOCATION, EXEC_ARGS); });
it('can get app information', getAppInfo);
it('registration is disabled', checkRegistration.bind(null, 'none'));
it('can LDAP login', login.bind(null, username, password));
it('can dismiss help', dismissHelp);
it('can see timeline', checkTimeline);
it('can logout', logout);
it('backup app', function () { execSync('cloudron backup create --app ' + app.id, EXEC_ARGS); });
it('restore app', function () {
const backups = JSON.parse(execSync('cloudron backup list --raw'));
execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS);
execSync('cloudron install --location ' + LOCATION, EXEC_ARGS);
getAppInfo();
execSync(`cloudron restore --backup ${backups[0].id} --app ${app.id}`, EXEC_ARGS);
});
it('can LDAP login', login.bind(null, username, password));
it('can see timeline', checkTimeline);
it('can restart app', function () { execSync('cloudron restart --app ' + app.id, EXEC_ARGS); });
it('can see timeline', checkTimeline);
it('move to different location', async function () {
await browser.get('about:blank');
execSync('cloudron configure --location ' + LOCATION + '2 --app ' + app.id, EXEC_ARGS);
});
it('can get app information', getAppInfo);
it('can LDAP login', login.bind(null, username, password));
it('can see timeline', checkTimeline);
it('uninstall app', async function () {
await browser.get('about:blank');
execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS);
});
// No SSO // No SSO
it('install app (no sso)', function () { execSync('cloudron install --no-sso --location ' + LOCATION, EXEC_ARGS); }); it('install app (no sso)', function () { execSync('cloudron install --no-sso --location ' + LOCATION, EXEC_ARGS); });
@ -161,9 +156,49 @@ describe('Application life cycle test', function () {
execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS);
}); });
// SSO
it('install app (sso)', function () { execSync('cloudron install --location ' + LOCATION, EXEC_ARGS); });
it('can get app information', getAppInfo);
it('registration is disabled', checkRegistration.bind(null, 'none'));
it('can OIDC login', loginOIDC.bind(null, username, password));
it('can dismiss help', dismissHelp);
it('can see timeline', checkTimeline);
it('can logout', logout);
it('backup app', function () { execSync('cloudron backup create --app ' + app.id, EXEC_ARGS); });
it('restore app', function () {
const backups = JSON.parse(execSync('cloudron backup list --raw'));
execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS);
execSync('cloudron install --location ' + LOCATION, EXEC_ARGS);
getAppInfo();
execSync(`cloudron restore --backup ${backups[0].id} --app ${app.id}`, EXEC_ARGS);
});
it('can OIDC login', loginOIDC.bind(null, username, password));
it('can see timeline', checkTimeline);
it('can restart app', function () { execSync('cloudron restart --app ' + app.id, EXEC_ARGS); });
it('can see timeline', checkTimeline);
it('move to different location', async function () {
await browser.get('about:blank');
execSync('cloudron configure --location ' + LOCATION + '2 --app ' + app.id, EXEC_ARGS);
});
it('can get app information', getAppInfo);
it('can OIDC login', loginOIDC.bind(null, username, password));
it('can see timeline', checkTimeline);
it('uninstall app', async function () {
await browser.get('about:blank');
execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS);
});
// test update // test update
it('can install app', function () { execSync('cloudron install --appstore-id ' + manifest.id + ' --location ' + LOCATION, EXEC_ARGS); }); it('can install app', function () { execSync('cloudron install --appstore-id ' + manifest.id + ' --location ' + LOCATION, EXEC_ARGS); });
it('can get app information', getAppInfo); it('can get app information', getAppInfo);
// needs to be changed to loginOIDC on the next release
it('can LDAP login', login.bind(null, username, password)); it('can LDAP login', login.bind(null, username, password));
it('can logout', logout); it('can logout', logout);
@ -172,7 +207,7 @@ describe('Application life cycle test', function () {
execSync('cloudron update --app ' + LOCATION, EXEC_ARGS); execSync('cloudron update --app ' + LOCATION, EXEC_ARGS);
}); });
it('can LDAP login', login.bind(null, username, password)); it('can OIDC login', loginOIDC.bind(null, username, password));
it('uninstall app', function () { execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); }); it('uninstall app', function () { execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); });
}); });