mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-07-21 13:42:54 +00:00
test(desec): end-to-end Playwright coverage against a local mock
- desec-mock.mjs: a dependency-free Node mock of the deSEC API endpoints the code uses, with the real status-code semantics (202 on account creation, 403 until verified then 200 + token, 201/409 on domain creation) plus /__control hooks to verify and reset state. - desec-register.spec.js / desec-existing.spec.js drive the real AIO UI through the register -> verify -> domain flow and the existing-account login flow; desec-helpers.js holds the shared login helper. Each scenario ends by setting a domain, so they run as separate CI steps with a re-seed in between. - seed-desec-mock-config.php points desec_api_base / desec_update_url at the mock (config key only, no env override, so production is unaffected) and seeds a known master password, since seeding configuration.json makes AIO consider itself already installed and /setup no longer shows a generated password. - Both Playwright workflows start the mock, mount ./community-containers so the caddy/dnsmasq definitions enabled by the flow are present, seed the config, and run the two deSEC specs with a re-seed between them. Co-Authored-By: szaimen <42591237+szaimen@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Simon L. <szaimen@e.mail.de>
This commit is contained in:
@@ -20,6 +20,10 @@ concurrency:
|
||||
|
||||
env:
|
||||
BASE_URL: https://localhost:8080
|
||||
# Master password seeded into configuration.json for the deSEC test. Seeding the config
|
||||
# makes AIO consider itself already installed, so /setup no longer generates/shows a
|
||||
# password; the seed helper writes this one and the test logs in with it directly.
|
||||
AIO_TEST_PASSWORD: correct horse battery staple aircraft
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -116,7 +120,7 @@ jobs:
|
||||
|
||||
- name: Run Playwright tests for backup restore
|
||||
run: |
|
||||
cd php/tests
|
||||
cd php/tests
|
||||
export DEBUG=pw:api
|
||||
if ! npx playwright test tests/restore-instance.spec.js; then
|
||||
docker logs nextcloud-aio-mastercontainer
|
||||
@@ -124,6 +128,66 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Start deSEC mock and fresh development server
|
||||
run: |
|
||||
# The deSEC flow must not touch the real deSEC API, so point it at a local mock.
|
||||
node php/tests/desec-mock.mjs 8090 > desec-mock.log 2>&1 &
|
||||
echo $! > desec-mock.pid
|
||||
docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup,caddy,dnsmasq} || true
|
||||
docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true
|
||||
# SKIP_DOMAIN_VALIDATION=false so the deSEC registration form is rendered; the deSEC
|
||||
# flow sets the domain with validation skipped internally regardless of this.
|
||||
docker run \
|
||||
-d \
|
||||
--init \
|
||||
--name nextcloud-aio-mastercontainer \
|
||||
--restart always \
|
||||
--publish 8080:8080 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \
|
||||
--volume ./php:/var/www/docker-aio/php \
|
||||
--volume ./community-containers:/var/www/docker-aio/community-containers \
|
||||
--volume ./Containers/mastercontainer/internal.Caddyfile:/internal.Caddyfile \
|
||||
--volume ./Containers/mastercontainer/headers.Caddyfile:/headers.Caddyfile \
|
||||
--volume ./Containers/mastercontainer/start.sh:/start.sh \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||
--env SKIP_DOMAIN_VALIDATION=false \
|
||||
--env APACHE_PORT=11000 \
|
||||
ghcr.io/nextcloud-releases/all-in-one:develop
|
||||
echo Waiting for 10 seconds for the development container to start ...
|
||||
sleep 10
|
||||
# Point the running interface at the deSEC mock via configuration.json (config-only,
|
||||
# no env override). The container runs PHP and has ./php mounted, so use the helper.
|
||||
# AIO_TEST_PASSWORD is forwarded so the helper can seed the master password too.
|
||||
docker exec --env AIO_TEST_PASSWORD="$AIO_TEST_PASSWORD" nextcloud-aio-mastercontainer \
|
||||
php /var/www/docker-aio/php/tests/seed-desec-mock-config.php \
|
||||
http://host.docker.internal:8090/api/v1 http://host.docker.internal:8090/update
|
||||
|
||||
- name: Run Playwright tests for deSEC domain registration
|
||||
run: |
|
||||
export DEBUG=pw:api
|
||||
export DESEC_MOCK_URL=http://localhost:8090
|
||||
|
||||
# Each deSEC scenario ends by registering a domain, which persists in
|
||||
# configuration.json and would hide the registration UI for the next scenario.
|
||||
# Run each spec as its own step and re-seed (reset the deSEC state) in between.
|
||||
reseed() {
|
||||
docker exec --env AIO_TEST_PASSWORD="$AIO_TEST_PASSWORD" nextcloud-aio-mastercontainer \
|
||||
php /var/www/docker-aio/php/tests/seed-desec-mock-config.php \
|
||||
http://host.docker.internal:8090/api/v1 http://host.docker.internal:8090/update
|
||||
}
|
||||
dump_logs() {
|
||||
docker logs nextcloud-aio-mastercontainer
|
||||
echo "--- deSEC mock log ---"
|
||||
cat "$GITHUB_WORKSPACE/desec-mock.log" || true
|
||||
}
|
||||
|
||||
cd php/tests
|
||||
if ! npx playwright test tests/desec-register.spec.js; then dump_logs; exit 1; fi
|
||||
reseed
|
||||
if ! npx playwright test tests/desec-existing.spec.js; then dump_logs; exit 1; fi
|
||||
kill "$(cat "$GITHUB_WORKSPACE/desec-mock.pid")" || true
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
|
||||
env:
|
||||
BASE_URL: https://localhost:8080
|
||||
# Master password seeded into configuration.json for the deSEC test. Seeding the config
|
||||
# makes AIO consider itself already installed, so /setup no longer generates/shows a
|
||||
# password; the seed helper writes this one and the test logs in with it directly.
|
||||
AIO_TEST_PASSWORD: correct horse battery staple aircraft
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -74,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Run Playwright tests for backup restore
|
||||
run: |
|
||||
cd php/tests
|
||||
cd php/tests
|
||||
export DEBUG=pw:api
|
||||
if ! npx playwright test tests/restore-instance.spec.js; then
|
||||
docker logs nextcloud-aio-mastercontainer
|
||||
@@ -82,6 +86,63 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Start deSEC mock and fresh development server
|
||||
run: |
|
||||
# The deSEC flow must not touch the real deSEC API, so point it at a local mock.
|
||||
node php/tests/desec-mock.mjs 8090 > desec-mock.log 2>&1 &
|
||||
echo $! > desec-mock.pid
|
||||
docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup,caddy,dnsmasq} || true
|
||||
docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true
|
||||
# SKIP_DOMAIN_VALIDATION=false so the deSEC registration form is rendered; the deSEC
|
||||
# flow sets the domain with validation skipped internally regardless of this.
|
||||
# Mount ./php/tests so the locally-checked-out DesecManager is exercised.
|
||||
docker run \
|
||||
-d \
|
||||
--init \
|
||||
--name nextcloud-aio-mastercontainer \
|
||||
--restart always \
|
||||
--publish 8080:8080 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \
|
||||
--volume ./php/tests:/var/www/docker-aio/php/tests \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||
--env SKIP_DOMAIN_VALIDATION=false \
|
||||
--env APACHE_PORT=11000 \
|
||||
ghcr.io/nextcloud-releases/all-in-one:develop
|
||||
echo Waiting for 10 seconds for the development container to start ...
|
||||
sleep 10
|
||||
# Point the running interface at the deSEC mock via configuration.json (config-only,
|
||||
# no env override). The container runs PHP and has ./php mounted, so use the helper.
|
||||
# AIO_TEST_PASSWORD is forwarded so the helper can seed the master password too.
|
||||
docker exec --env AIO_TEST_PASSWORD="$AIO_TEST_PASSWORD" nextcloud-aio-mastercontainer \
|
||||
php /var/www/docker-aio/php/tests/seed-desec-mock-config.php \
|
||||
http://host.docker.internal:8090/api/v1 http://host.docker.internal:8090/update
|
||||
|
||||
- name: Run Playwright tests for deSEC domain registration
|
||||
run: |
|
||||
export DEBUG=pw:api
|
||||
export DESEC_MOCK_URL=http://localhost:8090
|
||||
|
||||
# Each deSEC scenario ends by registering a domain, which persists in
|
||||
# configuration.json and would hide the registration UI for the next scenario.
|
||||
# Run each spec as its own step and re-seed (reset the deSEC state) in between.
|
||||
reseed() {
|
||||
docker exec --env AIO_TEST_PASSWORD="$AIO_TEST_PASSWORD" nextcloud-aio-mastercontainer \
|
||||
php /var/www/docker-aio/php/tests/seed-desec-mock-config.php \
|
||||
http://host.docker.internal:8090/api/v1 http://host.docker.internal:8090/update
|
||||
}
|
||||
dump_logs() {
|
||||
docker logs nextcloud-aio-mastercontainer
|
||||
echo "--- deSEC mock log ---"
|
||||
cat "$GITHUB_WORKSPACE/desec-mock.log" || true
|
||||
}
|
||||
|
||||
cd php/tests
|
||||
if ! npx playwright test tests/desec-register.spec.js; then dump_logs; exit 1; fi
|
||||
reseed
|
||||
if ! npx playwright test tests/desec-existing.spec.js; then dump_logs; exit 1; fi
|
||||
kill "$(cat "$GITHUB_WORKSPACE/desec-mock.pid")" || true
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Minimal mock of the deSEC API, implementing exactly the endpoints that
|
||||
// php/src/Desec/DesecManager.php calls, with the same HTTP status-code semantics
|
||||
// as the real service (see https://desec.readthedocs.io). It exists so the deSEC
|
||||
// register -> verify -> domain flow can be exercised end-to-end in CI without
|
||||
// touching the real deSEC service (which would create real accounts, be flaky and
|
||||
// rate-limited, and require a human to click an email verification link).
|
||||
//
|
||||
// We do not probe deSEC's live API (their ToS discourages automated requests, and the
|
||||
// /api/v1/ interface is contractually stable). Instead, the scheduled workflow
|
||||
// .github/workflows/desec-api-version-watch.yml watches deSEC's published API-version
|
||||
// table for changes; if it goes red, review the change and update this mock (plus
|
||||
// DesecManager) to match.
|
||||
//
|
||||
// Usage: node desec-mock.mjs [port] (default port 8090)
|
||||
// Routes:
|
||||
// POST /api/v1/auth/ -> 202 (account requested; verify email)
|
||||
// POST /api/v1/auth/login/ -> 403 until verified, then 200 + {token}
|
||||
// POST /api/v1/domains/ -> 201 (new name) | 409 (name already taken)
|
||||
// POST /api/v1/domains/{name}/rrsets/ -> 201 (wildcard CNAME)
|
||||
// GET /update -> 200 "good" (dyndns update endpoint)
|
||||
// POST /__control/verify -> test hook: mark accounts email-verified
|
||||
// POST /__control/reset -> test hook: wipe in-memory state
|
||||
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
const port = Number(process.argv[2] ?? process.env.DESEC_MOCK_PORT ?? 8090)
|
||||
|
||||
// In-memory state. Reset between independent test scenarios via /__control/reset.
|
||||
const state = {
|
||||
accounts: new Map(), // email -> { password }
|
||||
domains: new Set(), // fully-qualified domain names that have been created
|
||||
verified: false, // simulates whether the (most recent) account's email was verified
|
||||
}
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
let data = ''
|
||||
req.on('data', (chunk) => { data += chunk })
|
||||
req.on('end', () => {
|
||||
if (data === '') return resolve({})
|
||||
try { resolve(JSON.parse(data)) } catch { resolve({}) }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function send(res, code, body) {
|
||||
const payload = typeof body === 'string' ? body : JSON.stringify(body)
|
||||
res.writeHead(code, { 'Content-Type': typeof body === 'string' ? 'text/plain' : 'application/json' })
|
||||
res.end(payload)
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://localhost:${port}`)
|
||||
const path = url.pathname
|
||||
const method = req.method ?? 'GET'
|
||||
|
||||
// --- Test control hooks (not part of the real deSEC API) ---
|
||||
if (method === 'POST' && path === '/__control/verify') {
|
||||
state.verified = true
|
||||
return send(res, 200, { verified: true })
|
||||
}
|
||||
if (method === 'POST' && path === '/__control/reset') {
|
||||
state.accounts.clear()
|
||||
state.domains.clear()
|
||||
state.verified = false
|
||||
return send(res, 200, { reset: true })
|
||||
}
|
||||
|
||||
// --- dyndns update endpoint (DesecManager::updateIpIfDesecDomain) ---
|
||||
if (method === 'GET' && path === '/update') {
|
||||
return send(res, 200, 'good')
|
||||
}
|
||||
|
||||
// --- Account registration: deSEC always answers 202 and emails a link; ---
|
||||
// --- it returns 202 even for an already-registered email (anti-enumeration). ---
|
||||
if (method === 'POST' && path === '/api/v1/auth/') {
|
||||
const body = await readBody(req)
|
||||
if (body.email && !state.accounts.has(body.email)) {
|
||||
state.accounts.set(body.email, { password: body.password ?? '' })
|
||||
// A freshly created account starts unverified; the test flips this via /__control/verify.
|
||||
state.verified = false
|
||||
}
|
||||
return send(res, 202, '')
|
||||
}
|
||||
|
||||
// --- Login: 403 until the email is verified, then 200 + a token object. ---
|
||||
if (method === 'POST' && path === '/api/v1/auth/login/') {
|
||||
const body = await readBody(req)
|
||||
const account = body.email ? state.accounts.get(body.email) : undefined
|
||||
const passwordMatches = account && account.password === body.password
|
||||
if (!state.verified || !passwordMatches) {
|
||||
return send(res, 403, { detail: 'Invalid email or password, or email not verified.' })
|
||||
}
|
||||
return send(res, 200, {
|
||||
token: 'mock-token-' + Buffer.from(body.email).toString('hex').slice(0, 16),
|
||||
id: 'mock-token-id',
|
||||
created: '2024-01-01T00:00:00.000000Z',
|
||||
max_age: '7 00:00:00',
|
||||
})
|
||||
}
|
||||
|
||||
// --- Domain creation: 201 for a new name, 409 if the same name already exists. ---
|
||||
if (method === 'POST' && path === '/api/v1/domains/') {
|
||||
const body = await readBody(req)
|
||||
const name = String(body.name ?? '')
|
||||
if (state.domains.has(name)) {
|
||||
return send(res, 409, { detail: 'This domain name is unavailable.' })
|
||||
}
|
||||
state.domains.add(name)
|
||||
return send(res, 201, { name })
|
||||
}
|
||||
|
||||
// --- RRset creation (wildcard CNAME): 201. ---
|
||||
if (method === 'POST' && /^\/api\/v1\/domains\/[^/]+\/rrsets\/$/.test(path)) {
|
||||
const body = await readBody(req)
|
||||
return send(res, 201, { subname: body.subname ?? '', type: body.type ?? '', records: body.records ?? [] })
|
||||
}
|
||||
|
||||
return send(res, 404, { detail: 'Not found in mock: ' + method + ' ' + path })
|
||||
})
|
||||
|
||||
server.listen(port, '0.0.0.0', () => {
|
||||
console.log(`deSEC mock listening on http://0.0.0.0:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// Test helper: point the running mastercontainer's deSEC integration at a local mock by
|
||||
// writing the (config-only) desec_api_base / desec_update_url keys into configuration.json.
|
||||
// Run inside the mastercontainer via `docker exec` from the Playwright CI workflows; there
|
||||
// is no env override for these on purpose (see ConfigurationManager::$desecApiBase).
|
||||
//
|
||||
// Writing configuration.json makes Setup::CanBeInstalled() return false, so /setup no longer
|
||||
// renders the initial-password page and no master password is generated. To keep the deSEC
|
||||
// Playwright test able to log in, this helper also seeds a known master password (passed via
|
||||
// the AIO_TEST_PASSWORD env var) that the test then uses directly. The password is stored in
|
||||
// plaintext, which matches how AIO compares it (AuthManager::CheckCredentials uses
|
||||
// hash_equals against the plaintext config value).
|
||||
//
|
||||
// Re-running this helper also RESETS the deSEC-specific state (domain, desec_email and the
|
||||
// DESEC_TOKEN / DESEC_PASSWORD secrets) so that a deSEC flow which already registered a domain
|
||||
// in a previous test does not bleed into the next test (where the registration UI only renders
|
||||
// while no domain is set). The Playwright deSEC scenarios are therefore each run as a separate
|
||||
// CI step with a re-seed in between (see the Playwright workflows). Other config and secrets
|
||||
// are preserved.
|
||||
//
|
||||
// Usage: AIO_TEST_PASSWORD=... php seed-desec-mock-config.php <api_base> <update_url>
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$apiBase = $argv[1] ?? '';
|
||||
$updateUrl = $argv[2] ?? '';
|
||||
if ($apiBase === '' || $updateUrl === '') {
|
||||
fwrite(STDERR, "usage: php seed-desec-mock-config.php <api_base> <update_url>\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$password = getenv('AIO_TEST_PASSWORD');
|
||||
if ($password === false || $password === '') {
|
||||
fwrite(STDERR, "AIO_TEST_PASSWORD env var must be set to the master password to seed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$file = '/mnt/docker-aio-config/data/configuration.json';
|
||||
$config = is_file($file) ? json_decode((string)file_get_contents($file), true) : [];
|
||||
if (!is_array($config)) {
|
||||
$config = [];
|
||||
}
|
||||
$config['desec_api_base'] = $apiBase;
|
||||
$config['desec_update_url'] = $updateUrl;
|
||||
$config['password'] = $password;
|
||||
|
||||
// Reset any deSEC state from a previous test run so the registration UI renders again.
|
||||
unset($config['domain'], $config['desec_email']);
|
||||
if (isset($config['secrets']) && is_array($config['secrets'])) {
|
||||
unset($config['secrets']['DESEC_TOKEN'], $config['secrets']['DESEC_PASSWORD']);
|
||||
}
|
||||
|
||||
file_put_contents($file, json_encode($config, JSON_PRETTY_PRINT));
|
||||
echo "Seeded deSEC mock config into $file\n";
|
||||
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { DESEC_MOCK_URL, logInToContainersPage } from './desec-helpers.js';
|
||||
|
||||
// Exercises the deSEC "I already have a verified account" login path: supplying a valid
|
||||
// password logs straight in and registers the domain in one step (no email-verification
|
||||
// round-trip). See desec-register.spec.js for the full setup notes.
|
||||
//
|
||||
// This flow also ends by registering a domain, so it is run as its own CI step with the
|
||||
// deSEC state re-seeded beforehand (configuration.json must have no domain set for the
|
||||
// registration UI to render).
|
||||
|
||||
test('deSEC existing-account login flow', async ({ page: setupPage }) => {
|
||||
test.setTimeout(5 * 60 * 1000);
|
||||
|
||||
// Pre-create a verified account in the mock so the password (login) path is exercised
|
||||
// directly, without the verification round-trip.
|
||||
await fetch(`${DESEC_MOCK_URL}/__control/reset`, { method: 'POST' });
|
||||
const email = `existing-${Math.floor(Math.random() * 2147483647)}@example.com`;
|
||||
const password = 'correct horse battery staple';
|
||||
await fetch(`${DESEC_MOCK_URL}/api/v1/auth/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
await fetch(`${DESEC_MOCK_URL}/__control/verify`, { method: 'POST' });
|
||||
|
||||
const containersPage = await logInToContainersPage(setupPage);
|
||||
|
||||
const slug = `aio-existing-${Math.floor(Math.random() * 2147483647)}`;
|
||||
|
||||
await containersPage.getByText("Don't have a domain? Get a free one from deSEC").click();
|
||||
await containersPage.locator('input[name="desec_email"]').fill(email);
|
||||
await containersPage.locator('input[name="desec_password"]').fill(password);
|
||||
await containersPage.locator('input[name="desec_slug"]').first().fill(slug);
|
||||
await containersPage.getByRole('button', { name: 'Register free domain via deSEC' }).click();
|
||||
|
||||
// Supplying a valid password logs straight in and registers the domain in one step.
|
||||
await expect(containersPage.getByRole('button', { name: 'Download and start containers' })).toBeVisible({ timeout: 60 * 1000 });
|
||||
await expect(
|
||||
containersPage.getByText("Don't have a domain? Get a free one from deSEC"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
// Shared helpers for the deSEC Playwright scenarios.
|
||||
//
|
||||
// The deSEC mock is wired up by seeding configuration.json (see seed-desec-mock-config.php),
|
||||
// which makes AIO consider itself already installed: /setup no longer renders the
|
||||
// initial-password page. The seed step therefore writes a known master password (AIO_TEST_PASSWORD)
|
||||
// that we log in with directly here instead of scraping it from /setup.
|
||||
|
||||
export const DESEC_MOCK_URL = process.env.DESEC_MOCK_URL ?? 'http://localhost:8090';
|
||||
|
||||
const AIO_PASSWORD = process.env.AIO_TEST_PASSWORD;
|
||||
|
||||
export async function logInToContainersPage(page) {
|
||||
if (!AIO_PASSWORD) {
|
||||
throw new Error('AIO_TEST_PASSWORD must be set to the master password seeded into configuration.json');
|
||||
}
|
||||
await page.goto('./');
|
||||
await page.locator('#master-password').click();
|
||||
await page.locator('#master-password').fill(AIO_PASSWORD);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL('./containers');
|
||||
return page;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { DESEC_MOCK_URL, logInToContainersPage } from './desec-helpers.js';
|
||||
|
||||
// Drives the real AIO interface through the full deSEC "register a free domain" flow
|
||||
// against the local mock (php/tests/desec-mock.mjs). The mastercontainer must be started
|
||||
// with SKIP_DOMAIN_VALIDATION=false (so the deSEC registration form is rendered) and its
|
||||
// configuration.json must point desec_api_base / desec_update_url at the mock (see the
|
||||
// Playwright CI workflow). The mock's control endpoint is reachable from the test runner
|
||||
// on the host at DESEC_MOCK_URL (default http://localhost:8090).
|
||||
//
|
||||
// This flow ends by registering a domain, which persists in configuration.json. It is
|
||||
// therefore run as its own CI step; the deSEC state is reset (re-seeded) before the
|
||||
// separate existing-account flow runs.
|
||||
|
||||
test('deSEC register -> verify -> domain flow', async ({ page: setupPage }) => {
|
||||
test.setTimeout(5 * 60 * 1000);
|
||||
|
||||
// Start from a clean mock state so this test is independent of any other run.
|
||||
await fetch(`${DESEC_MOCK_URL}/__control/reset`, { method: 'POST' });
|
||||
|
||||
const containersPage = await logInToContainersPage(setupPage);
|
||||
|
||||
const email = `e2e-${Math.floor(Math.random() * 2147483647)}@example.com`;
|
||||
const slug = `aio-e2e-${Math.floor(Math.random() * 2147483647)}`;
|
||||
|
||||
// Open the deSEC registration section.
|
||||
await containersPage.getByText("Don't have a domain? Get a free one from deSEC").click();
|
||||
|
||||
// 1) Submit email only -> a new account is "created" (mock 202) and AIO asks the user
|
||||
// to verify their email. This is a normal (non-error) state transition: the page
|
||||
// reloads into the awaiting-verification UI, which renders inside <main> and shows
|
||||
// the dedicated re-submit button.
|
||||
await containersPage.locator('input[name="desec_email"]').fill(email);
|
||||
await containersPage.locator('input[name="desec_slug"]').first().fill(slug);
|
||||
await containersPage.getByRole('button', { name: 'Register free domain via deSEC' }).click();
|
||||
await expect(containersPage.getByRole('main')).toContainText('check your inbox', { timeout: 30 * 1000 });
|
||||
await expect(
|
||||
containersPage.getByRole('button', { name: 'I have verified my email – register domain' }),
|
||||
).toBeVisible();
|
||||
|
||||
// 2) Re-submit BEFORE verifying -> login still fails (mock 403) -> friendly hint shown.
|
||||
await containersPage.getByRole('button', { name: 'I have verified my email – register domain' }).click();
|
||||
// Same as above: the failure hint is a transient toast, not <main> content.
|
||||
await expect(containersPage.locator('.toast.error')).toContainText('Could not log in to deSEC', { timeout: 8 * 1000 });
|
||||
|
||||
// 3) Simulate the user clicking the verification link in their email.
|
||||
const verifyResponse = await fetch(`${DESEC_MOCK_URL}/__control/verify`, { method: 'POST' });
|
||||
expect(verifyResponse.status).toBe(200);
|
||||
|
||||
// 4) Re-submit after verification -> login succeeds, domain is registered, the deSEC
|
||||
// section disappears and the container-start UI appears.
|
||||
await containersPage.getByRole('button', { name: 'I have verified my email – register domain' }).click();
|
||||
await expect(containersPage.getByRole('button', { name: 'Download and start containers' })).toBeVisible({ timeout: 60 * 1000 });
|
||||
await expect(
|
||||
containersPage.getByText("Don't have a domain? Get a free one from deSEC"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user