#!/usr/bin/env node 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 { app, clearCache, click, cloudronCli, executeScript, goto, loginOIDC, scrollIntoView, sendKeys, setInputFiles, setupBrowser, takeScreenshot, teardownBrowser, username, waitForElement, waitForPath } from '@cloudron/charlie'; /* global it, describe, before, after, afterEach */ const SSH_PORT = 29420; const INSTALL_TCP_FLAGS = { SSH_PORT }; const repodir = '/tmp/testrepo'; const reponame = 'testrepo'; describe('Application life cycle test', function () { before(setupBrowser); after(async function () { await teardownBrowser(); fs.rmSync(repodir, { recursive: true, force: true }); }); afterEach(async function () { await takeScreenshot(this.currentTest.title); }); async function setAvatar() { 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 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(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('//nav//img[contains(@class, "avatar")]'); } async function adminLogin() { await login('root', 'changeme'); } async function loginGiteaOIDC() { await clearCache(); await goto(`https://${app.fqdn}/user/login`, '//a[@href="/user/oauth2/cloudron"]'); await click('//a[@href="/user/oauth2/cloudron"]'); await loginOIDC('//nav//img[contains(@class, "avatar")]'); } async function loginGiteaOIDCOld() { 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('//nav//img[contains(@class, "avatar")]'); } async function logout() { await goto(`https://${app.fqdn}`, '//nav//img[contains(@class, "avatar")]'); await click('//nav//img[contains(@class, "avatar")]'); await waitForElement('//a[@href="/user/logout"]'); await click('//a[@href="/user/logout"]'); } async function addPublicKey() { const keyPath = path.join(import.meta.dirname, 'id_ed25519'); fs.chmodSync(keyPath, 0o600); 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 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 }); 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 = 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); } async function sendMail() { 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")]'); } 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', loginGiteaOIDC); it('can set avatar', setAvatar); it('can get avatar', checkAvatar); it('can add public key', addPublicKey); it('can create repo', createRepo); it('can clone the url', cloneRepo); it('can add and push a file', pushFile); it('can restart app', function () { cloudronCli.restart(); }); it('can clone the url', cloneRepo); it('file exists in repo', fileExists); it('backup app', async function () { await cloudronCli.createBackup(); }); it('restore app', async function () { await cloudronCli.restoreFromLatestBackup(); }); 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('move to different location', async function () { await cloudronCli.changeLocation(); }); 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 () { await cloudronCli.uninstall(); }); it('install app (no sso)', function () { cloudronCli.install({ noSso: true, tcpPortFlags: INSTALL_TCP_FLAGS }); }); it('can admin login (no sso)', adminLogin); it('can logout', logout); it('uninstall app (no sso)', async function () { await cloudronCli.uninstall(); }); it('can install app', function () { cloudronCli.appstoreInstall({ tcpPortFlags: INSTALL_TCP_FLAGS }); }); it('can login', loginGiteaOIDCOld); it('can set avatar', setAvatar); it('can get avatar', checkAvatar); it('can add public key', addPublicKey); it('can create repo', createRepo); it('can clone the url', cloneRepo); it('can add and push a file', pushFile); 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', loginGiteaOIDC); it('can get avatar', checkAvatar); it('can clone the url', cloneRepo); it('file exists in cloned repo', fileExists); it('uninstall app', async function () { await cloudronCli.uninstall(); }); });