diff --git a/test/test.js b/test/test.js index ac14d7d..e9f7eb3 100755 --- a/test/test.js +++ b/test/test.js @@ -1,209 +1,154 @@ #!/usr/bin/env node -import { execSync } from 'node:child_process'; import assert from 'node:assert/strict'; +import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import superagent from '@cloudron/superagent'; -import { Builder, By, until } from 'selenium-webdriver'; -import { Options } from 'selenium-webdriver/chrome'; +import { + app, + clearCache, + click, + cloudronCli, + executeScript, + goto, + loginOIDC, + scrollIntoView, + sendKeys, + setInputFiles, + setupBrowser, + takeScreenshot, + teardownBrowser, + username, + waitForElement, + waitForPath +} from '@cloudron/charlie'; -/* global it, xit, describe, before, after, afterEach */ +/* global it, describe, before, after, afterEach */ -if (!process.env.USERNAME || !process.env.PASSWORD || !process.env.EMAIL) { - console.log('USERNAME, PASSWORD and EMAIL env vars need to be set'); - process.exit(1); -} +const SSH_PORT = 29420; +const INSTALL_TCP_FLAGS = { SSH_PORT }; +const repodir = '/tmp/testrepo'; +const reponame = 'testrepo'; describe('Application life cycle test', function () { - this.timeout(0); - - const TIMEOUT = parseInt(process.env.TIMEOUT, 10) || 5000; - const EXEC_ARGS = { cwd: path.resolve(import.meta.dirname, '..'), stdio: 'inherit' }; - const LOCATION = process.env.LOCATION || 'test'; - const SSH_PORT = 29420; - - let app, browser; - - const repodir = '/tmp/testrepo'; - const reponame = 'testrepo'; - - const username = process.env.USERNAME; - const password = process.env.PASSWORD; - - before(function () { - const chromeOptions = new Options().windowSize({ width: 1280, height: 1024 }); - if (process.env.CI) chromeOptions.addArguments('no-sandbox', 'disable-dev-shm-usage', 'headless'); - browser = new Builder().forBrowser('chrome').setChromeOptions(chromeOptions).build(); - if (!fs.existsSync('./screenshots')) fs.mkdirSync('./screenshots'); - }); - - after(function () { - browser.quit(); + before(setupBrowser); + after(async function () { + await teardownBrowser(); fs.rmSync(repodir, { recursive: true, force: true }); }); afterEach(async function () { - if (!process.env.CI || !app) return; - - const currentUrl = await browser.getCurrentUrl(); - if (!currentUrl.includes(app.domain)) return; - assert.strictEqual(typeof this.currentTest.title, 'string'); - - const screenshotData = await browser.takeScreenshot(); - fs.writeFileSync(`./screenshots/${new Date().getTime()}-${this.currentTest.title.replaceAll(' ', '_')}.png`, screenshotData, 'base64'); + await takeScreenshot(this.currentTest.title); }); - function getAppInfo() { - const inspect = JSON.parse(execSync('cloudron inspect')); - app = inspect.apps.filter(function (a) { return a.location.indexOf(LOCATION) === 0; })[0]; - assert.ok(app && typeof app === 'object'); - } - - async function waitForElement(elem) { - await browser.wait(until.elementLocated(elem), TIMEOUT); - await browser.wait(until.elementIsVisible(browser.findElement(elem)), TIMEOUT); - } - - function sleep(millis) { - return new Promise(resolve => setTimeout(resolve, millis)); - } - async function setAvatar() { - await browser.get('https://' + app.fqdn + '/user/settings'); - - var button = await browser.findElement(By.xpath('//label[contains(text(), "Use Custom Avatar")]')); - await browser.executeScript('arguments[0].scrollIntoView(false)', button); - await browser.findElement(By.xpath('//label[contains(text(), "Use Custom Avatar")]')).click(); - await browser.findElement(By.xpath('//input[@type="file" and @name="avatar"]')).sendKeys(path.resolve(import.meta.dirname, '../logo.png')); - await browser.findElement(By.xpath('//button[contains(text(), "Update Avatar")]')).click(); - await browser.wait(until.elementLocated(By.xpath('//p[contains(text(),"Your avatar has been updated.")]')), TIMEOUT); + await goto(`https://${app.fqdn}/user/settings`, '//label[contains(text(), "Use Custom Avatar")]'); + await scrollIntoView('//label[contains(text(), "Use Custom Avatar")]'); + await click('//label[contains(text(), "Use Custom Avatar")]'); + await setInputFiles('//input[@type="file" and @name="avatar"]', path.resolve(import.meta.dirname, '../logo.png')); + await click('//button[contains(text(), "Update Avatar")]'); + await waitForElement('//p[contains(text(),"Your avatar has been updated.")]'); } async function checkAvatar() { - await browser.get(`https://${app.fqdn}/${username}`); - - const avatarSrc = await browser.findElement(By.xpath('//div[@id="profile-avatar"]/a/img')).getAttribute('src'); - - const response = await superagent.get(avatarSrc); + await goto(`https://${app.fqdn}/${username}`, '//div[@id="profile-avatar"]/a/img'); + const avatarSrc = await executeScript(() => { + const el = document.querySelector('#profile-avatar a img'); + return el ? el.getAttribute('src') : null; + }); + assert.ok(avatarSrc); + const avatarUrl = new URL(avatarSrc, `https://${app.fqdn}`).href; + const response = await superagent.get(avatarUrl); assert.strictEqual(response.status, 200); } - async function login(username, password) { - await browser.get('https://' + app.fqdn + '/user/login'); - - await browser.findElement(By.id('user_name')).sendKeys(username); - await browser.findElement(By.id('password')).sendKeys(password); - await browser.findElement(By.xpath('//form[@action="/user/login"]//button')).click(); - await browser.wait(until.elementLocated(By.xpath('//img[contains(@class, "avatar")]')), TIMEOUT); + async function login(user, passwd) { + await goto(`https://${app.fqdn}/user/login`, '#user_name'); + await sendKeys('#user_name', user); + await sendKeys('#password', passwd); + await click('//form[@action="/user/login"]//button'); + await waitForElement('//img[contains(@class, "avatar")]'); } async function adminLogin() { await login('root', 'changeme'); } - async function loginOIDC(username, password, alreadyAuthenticated = true) { - browser.manage().deleteAllCookies(); - await browser.get(`https://${app.fqdn}/user/login`); - await browser.sleep(2000); - - - await browser.findElement(By.xpath('//a[contains(@class, "openidConnect") and contains(., "Sign in with cloudron")]')).click(); - await browser.sleep(2000); - - if (!alreadyAuthenticated) { - await waitForElement(By.id('inputUsername')); - await browser.findElement(By.id('inputUsername')).sendKeys(username); - await browser.findElement(By.id('inputPassword')).sendKeys(password); - await browser.sleep(2000); - await browser.findElement(By.id('loginSubmitButton')).click(); - await browser.sleep(2000); - } - - await waitForElement(By.xpath('//img[contains(@class, "avatar")]')); + async function loginGiteaOIDC() { + await clearCache(); + await goto(`https://${app.fqdn}/user/login`, '//a[contains(@class, "openidConnect")]'); + await click('//a[contains(@class, "openidConnect") and contains(., "Sign in with cloudron")]'); + await loginOIDC('//img[contains(@class, "avatar")]'); } async function logout() { - await browser.get('https://' + app.fqdn); - - await browser.findElement(By.xpath('//img[contains(@class, "avatar")]')).click(); - await sleep(2000); - await browser.findElement(By.xpath('//a[@data-url="/user/logout"]')).click(); - await sleep(2000); + await goto(`https://${app.fqdn}`, '//img[contains(@class, "avatar")]'); + await click('//img[contains(@class, "avatar")]'); + await waitForElement('//a[@data-url="/user/logout"]'); + await click('//a[@data-url="/user/logout"]'); } async function addPublicKey() { - const publicKey = fs.readFileSync(import.meta.dirname + '/id_ed25519.pub', 'utf8'); - execSync(`chmod g-rw,o-rw ${import.meta.dirname}/id_ed25519`); // ssh will complain about perms later + const keyPath = path.join(import.meta.dirname, 'id_ed25519'); + fs.chmodSync(keyPath, 0o600); - await browser.get('https://' + app.fqdn + '/user/settings/keys'); - - await browser.wait(until.elementLocated(By.id('add-ssh-button')), TIMEOUT); - await browser.findElement(By.id('add-ssh-button')).click(); - await browser.findElement(By.id('ssh-key-title')).sendKeys('testkey'); - await browser.findElement(By.id('ssh-key-content')).sendKeys(publicKey.trim()); // #3480 - var button = browser.findElement(By.xpath('//button[contains(text(), "Add Key")]')); - await browser.executeScript('arguments[0].scrollIntoView(false)', button); - await browser.findElement(By.xpath('//form//button[contains(text(),"Add Key")]')).click(); - - await browser.wait(until.elementLocated(By.xpath('//p[contains(text(), "has been added.")]')), TIMEOUT); + await goto(`https://${app.fqdn}/user/settings/keys`, '#add-ssh-button'); + await click('#add-ssh-button'); + await sendKeys('#ssh-key-title', 'testkey'); + await sendKeys('#ssh-key-content', fs.readFileSync(`${import.meta.dirname}/id_ed25519.pub`, 'utf8').trim()); + await scrollIntoView('//button[contains(text(), "Add Key")]'); + await click('//form//button[contains(text(),"Add Key")]'); + await waitForElement('//p[contains(text(), "has been added.")]'); } async function createRepo() { - await browser.get(`https://${app.fqdn}/repo/create`); - await browser.wait(until.elementLocated(By.id('repo_name'))); - await browser.findElement(By.id('repo_name')).sendKeys(reponame); - var button = browser.findElement(By.xpath('//button[contains(text(), "Create Repository")]')); - await browser.executeScript('arguments[0].scrollIntoView(true)', button); - await browser.findElement(By.id('auto-init')).click(); - await browser.findElement(By.xpath('//button[contains(text(), "Create Repository")]')).click(); - - await browser.wait(function () { - return browser.getCurrentUrl().then(function (url) { - return url === 'https://' + app.fqdn + '/' + username + '/' + reponame; - }); - }, TIMEOUT); + await goto(`https://${app.fqdn}/repo/create`, '#repo_name'); + await sendKeys('#repo_name', reponame); + await scrollIntoView('//button[contains(text(), "Create Repository")]'); + await click('#auto-init'); + await click('//button[contains(text(), "Create Repository")]'); + await waitForPath(`/${username}/${reponame}`); } function cloneRepo() { fs.rmSync(repodir, { recursive: true, force: true }); - var env = Object.create(process.env); - env.GIT_SSH = import.meta.dirname + '/git_ssh_wrapper.sh'; - execSync(`git clone ssh://git@${app.fqdn}:${SSH_PORT}/${username}/${reponame}.git ${repodir}`, { env: env }); + const env = Object.create(process.env); + env.GIT_SSH = path.join(import.meta.dirname, 'git_ssh_wrapper.sh'); + execSync(`git clone ssh://git@${app.fqdn}:${SSH_PORT}/${username}/${reponame}.git ${repodir}`, { env }); } function pushFile() { const env = Object.create(process.env); - env.GIT_SSH = import.meta.dirname + '/git_ssh_wrapper.sh'; - execSync(`touch newfile && git add newfile && git commit -a -mx && git push ssh://git@${app.fqdn}:${SSH_PORT}/${username}/${reponame} main`, - { env: env, cwd: repodir }); + env.GIT_SSH = path.join(import.meta.dirname, 'git_ssh_wrapper.sh'); + execSync( + `touch newfile && git add newfile && git commit -a -mx && git push ssh://git@${app.fqdn}:${SSH_PORT}/${username}/${reponame} main`, + { env, cwd: repodir } + ); fs.rmSync(repodir, { recursive: true, force: true }); } function fileExists() { - assert.strictEqual(fs.existsSync(repodir + '/newfile'), true); + assert.strictEqual(fs.existsSync(`${repodir}/newfile`), true); } async function sendMail() { - await browser.get(`https://${app.fqdn}/-/admin/config`); - await browser.sleep(3000); - const button = await browser.findElement(By.xpath('//button[contains(., "Send")]')); - await browser.executeScript('arguments[0].scrollIntoView(true)', button); - await browser.findElement(By.xpath('//input[@name="email"]')).sendKeys('test@cloudron.io'); - await browser.findElement(By.xpath('//button[contains(., "Send")]')).click(); - await browser.wait(until.elementLocated(By.xpath('//p[contains(., "A testing email has been sent")]')), TIMEOUT); + await goto(`https://${app.fqdn}/-/admin/config`, '//button[contains(., "Send")]'); + await scrollIntoView('//button[contains(., "Send")]'); + await sendKeys('//input[@name="email"]', 'test@cloudron.io'); + await click('//button[contains(., "Send")]'); + await waitForElement('//p[contains(., "A testing email has been sent")]'); } - xit('build app', function () { execSync('cloudron build', EXEC_ARGS); }); - it('install app', function () { execSync(`cloudron install --location ${LOCATION} -p SSH_PORT=${SSH_PORT}`, EXEC_ARGS); }); - - it('can get app information', getAppInfo); + it('install app', function () { + cloudronCli.install({ tcpPortFlags: INSTALL_TCP_FLAGS }); + }); it('can admin login', adminLogin); it('can send mail', sendMail); it('can logout', logout); - it('can login', loginOIDC.bind(null, username, password, false)); + it('can login', loginGiteaOIDC); it('can set avatar', setAvatar); it('can get avatar', checkAvatar); @@ -215,57 +160,58 @@ describe('Application life cycle test', function () { it('can add and push a file', pushFile); - it('can restart app', function () { execSync('cloudron restart --app ' + app.id); }); + it('can restart app', function () { + cloudronCli.restart(); + }); - xit('can login', loginOIDC.bind(null, username, password)); // no need to relogin since session persists it('can clone the url', cloneRepo); it('file exists in repo', fileExists); - it('backup app', function () { execSync('cloudron backup create --app ' + app.id, EXEC_ARGS); }); - it('restore app', function () { execSync('cloudron restore --app ' + app.id, EXEC_ARGS); }); + it('backup app', async function () { + await cloudronCli.createBackup(); + }); + it('restore app', async function () { + await cloudronCli.restoreFromLatestBackup(); + }); - it('can login', loginOIDC.bind(null, username, password)); + it('can login', loginGiteaOIDC); it('can get avatar', checkAvatar); it('can clone the url', cloneRepo); - it('file exists in repo', function () { assert.strictEqual(fs.existsSync(repodir + '/newfile'), true); }); + it('file exists in repo', function () { + assert.strictEqual(fs.existsSync(`${repodir}/newfile`), true); + }); it('move to different location', async function () { - //browser.manage().deleteAllCookies(); // commented because of error "'Network.deleteCookie' wasn't found" - // ensure we don't hit NXDOMAIN in the mean time - await browser.get('about:blank'); - - execSync('cloudron configure --location ' + LOCATION + '2 --app ' + app.id, EXEC_ARGS); + await cloudronCli.changeLocation(); }); - it('can get app information', getAppInfo); - it('can login', loginOIDC.bind(null, username, password)); + it('can login', loginGiteaOIDC); it('can get avatar', checkAvatar); it('can clone the url', cloneRepo); - it('file exists in repo', function () { assert.strictEqual(fs.existsSync(repodir + '/newfile'), true); }); - - it('uninstall app', async function () { - // ensure we don't hit NXDOMAIN in the mean time - await browser.get('about:blank'); - execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); + it('file exists in repo', function () { + assert.strictEqual(fs.existsSync(`${repodir}/newfile`), true); }); - // No SSO - it('install app (no sso)', function () { execSync(`cloudron install --no-sso --location ${LOCATION} -p SSH_PORT=${SSH_PORT}`, EXEC_ARGS); }); + it('uninstall app', async function () { + await cloudronCli.uninstall(); + }); + + it('install app (no sso)', function () { + cloudronCli.install({ noSso: true, tcpPortFlags: INSTALL_TCP_FLAGS }); + }); - it('can get app information', getAppInfo); it('can admin login (no sso)', adminLogin); it('can logout', logout); it('uninstall app (no sso)', async function () { - await browser.get('about:blank'); - execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); + await cloudronCli.uninstall(); }); - // test update - it('can install app', function () { execSync(`cloudron install --appstore-id ${app.manifest.id} --location ${LOCATION} -p SSH_PORT=${SSH_PORT}`, EXEC_ARGS); }); + it('can install app', function () { + cloudronCli.appstoreInstall({ tcpPortFlags: INSTALL_TCP_FLAGS }); + }); - it('can get app information', getAppInfo); - it('can login', loginOIDC.bind(null, username, password)); + it('can login', loginGiteaOIDC); it('can set avatar', setAvatar); it('can get avatar', checkAvatar); it('can add public key', addPublicKey); @@ -273,21 +219,20 @@ describe('Application life cycle test', function () { it('can clone the url', cloneRepo); it('can add and push a file', pushFile); - it('can update', function () { execSync('cloudron update --app ' + app.id, EXEC_ARGS); }); - it('can get app information', getAppInfo); + it('can update', async function () { + await cloudronCli.update(); + }); it('can admin login', adminLogin); it('can send mail', sendMail); it('can logout', logout); - it('can login', loginOIDC.bind(null, username, password)); + it('can login', loginGiteaOIDC); it('can get avatar', checkAvatar); it('can clone the url', cloneRepo); it('file exists in cloned repo', fileExists); it('uninstall app', async function () { - // ensure we don't hit NXDOMAIN in the mean time - await browser.get('about:blank'); - execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); + await cloudronCli.uninstall(); }); });