// End-to-end smoke: bootstrap → accept pending host → run backup → see succeeded. // // The compose stack stands up a server, a sibling rest-server, and an // agent in announce-and-approve mode. This test drives the operator // path through the UI (login + dashboard) and the API // (accept + run-now + poll for terminal) — UI for the human surfaces, // API for the deterministic ones. import { test, expect } from '@playwright/test'; import { baseURL, bootstrapAdmin, loginViaUI, waitForPendingHostID, acceptPending, waitForHostStatus, getSessionCookie, } from './lib/server'; test.describe('smoke: enrol-via-announce → backup', () => { test('happy path: enrol → accept → backup → succeeded', async ({ page, request }) => { const { username, password } = await bootstrapAdmin(request); await loginViaUI(page, username, password); // Dashboard renders. await expect(page.locator('main')).toContainText(/host|fleet|pending/i, { timeout: 10_000 }); // Pending host appears (the agent container has been // announcing since startup). const pendingID = await waitForPendingHostID(page); const cookie = await getSessionCookie(page); // Accept with the rest-server creds. compose's rest-server runs // --no-auth, so any credentials work; restic still demands a // password to encrypt the repo. await acceptPending(request, cookie, pendingID, { url: 'rest:http://rest-server:8000/', password: 'e2e-repo-password', }); // Wait for the host to come online AND for auto-init to // finish. Coming online happens as soon as the agent's // bearer-authed WS attaches (~1s after accept); repo_status // flips to 'ready' once the auto-init job completes (a // couple of seconds later). Loading the host page before // that leaves the Run-backup button disabled because the // server-rendered HTML reflects the still-in-progress init, // and the page has no live-refresh on that field. const readyHost = await waitForHostStatus( request, cookie, (h) => h.status === 'online' && h.repo_status === 'ready', 90_000, ); expect(readyHost.id).toBeTruthy(); // Trigger a backup via the UI form-post (HX-Redirect to /jobs/{id}). await page.goto(`${baseURL}/hosts/${readyHost.id}`); await Promise.all([ page.waitForURL(/\/jobs\//), page.locator('form[action$="/run-backup"] button[type="submit"]').first().click(), ]); // Wait for the host's last_backup_status to flip to 'succeeded'. // The job page itself is harder to assert on (it uses // server-pushed updates and a reload-on-finish pattern); the // host record is the source of truth and is what the dashboard // surfaces. const finishedHost = await waitForHostStatus( request, cookie, (h) => h.id === readyHost.id && h.last_backup_status === 'succeeded', 120_000, ); expect(finishedHost.last_backup_status).toBe('succeeded'); }); }); test.describe('smoke: scrape /metrics', () => { test('metrics endpoint exposes the host gauge', async ({ request }) => { // Compose sets RM_METRICS_TRUSTED_CIDR=0.0.0.0/0 so the // endpoint is open to the test runner. const res = await request.get(`${baseURL}/metrics`); expect(res.status()).toBe(200); const body = await res.text(); expect(body).toContain('rm_hosts_total'); expect(body).toContain('rm_build_info{'); }); });