// 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, createSourceGroup, runSourceGroup, 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(); // Per-host Run-now is gone; backups are dispatched per // source-group now. Create one that maps to the agent's // /source mount, then kick it via the JSON API. const groupID = await createSourceGroup(request, cookie, readyHost.id, { name: 'default', includes: ['/source'], }); await runSourceGroup(request, cookie, readyHost.id, groupID); // Wait for the host's last_backup_status to flip to 'succeeded'. // The host record is the source of truth: it's what the // dashboard projects from job-completion events on the WS // channel. 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{'); }); });