// 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 completes in under a minute', 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 + auto-init to land. const onlineHost = await waitForHostStatus( request, cookie, (h) => h.status === 'online', 60_000, ); expect(onlineHost.id).toBeTruthy(); // Trigger a backup via the UI form-post (HX-Redirect to /jobs/{id}). await page.goto(`${baseURL}/hosts/${onlineHost.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 === onlineHost.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{'); }); });