Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
POST /api/v1/recon/scan probes fleet ports from the server host, crawls owned HTTP targets, maps findings to spread lanes, and records optional oath ledger rows.
118 lines
4.1 KiB
TypeScript
118 lines
4.1 KiB
TypeScript
import { expect, type APIRequestContext, type Locator, type Page } from '@playwright/test';
|
|
|
|
/** Matches server/internal/api/integration_test.go testAuthUser / testAuthPass. */
|
|
export const E2E_USER = process.env.AETHERFORGE_E2E_USER || 'testuser';
|
|
export const E2E_PASS = process.env.AETHERFORGE_E2E_PASS || 'testpass';
|
|
|
|
/** Seed this into the server data dir as users.json before first start (see tests/README.md). */
|
|
export const E2E_USERS_JSON = JSON.stringify({ [E2E_USER]: E2E_PASS });
|
|
|
|
export function e2eAuthHeaders(): Record<string, string> {
|
|
const token = Buffer.from(`${E2E_USER}:${E2E_PASS}`).toString('base64');
|
|
return {
|
|
Authorization: `Basic ${token}`,
|
|
'X-AetherForge-Client': 'dashboard',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads fleet_secret for stub agent auth.
|
|
* Prefer AETHERFORGE_FLEET_SECRET (test-suite.ps1 seeds from data/config.json).
|
|
*/
|
|
export async function fetchFleetSecret(request: APIRequestContext): Promise<string> {
|
|
if (process.env.AETHERFORGE_E2E !== '1') {
|
|
const fromEnv = process.env.AETHERFORGE_FLEET_SECRET?.trim();
|
|
if (fromEnv) return fromEnv;
|
|
}
|
|
|
|
const res = await request.get('/api/v1/config', {
|
|
headers: e2eAuthHeaders(),
|
|
timeout: 10_000,
|
|
});
|
|
if (!res.ok()) {
|
|
throw new Error(`config fetch failed: ${res.status()}`);
|
|
}
|
|
const body = (await res.json()) as { server?: { fleet_secret?: string } };
|
|
const secret = body.server?.fleet_secret?.trim() ?? '';
|
|
if (!secret) {
|
|
throw new Error(
|
|
'fleet_secret missing — set AETHERFORGE_FLEET_SECRET from data/config.json in the E2E runner',
|
|
);
|
|
}
|
|
return secret;
|
|
}
|
|
|
|
/** Poll /api/v1/health until status ok or timeout (mirrors test-suite.ps1 phase 8). */
|
|
export async function waitForServerHealth(
|
|
request: APIRequestContext,
|
|
opts?: { timeoutMs?: number; intervalMs?: number },
|
|
): Promise<boolean> {
|
|
const timeoutMs = opts?.timeoutMs ?? 30_000;
|
|
const intervalMs = opts?.intervalMs ?? 1_000;
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const res = await request.get('/api/v1/health', { timeout: 2_000 });
|
|
if (res.ok()) {
|
|
const body = (await res.json()) as { status?: string };
|
|
if (body.status === 'ok') return true;
|
|
}
|
|
} catch {
|
|
/* retry */
|
|
}
|
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 6px status dots fail Playwright visibility — assert card online via class instead. */
|
|
export async function expectOnlineCrucibleCard(page: Page, hostname: string): Promise<Locator> {
|
|
const card = page.locator('.crucible-node-card').filter({
|
|
has: page.locator('.cn-name', { hasText: hostname }),
|
|
});
|
|
await expect(card).toBeVisible({ timeout: 20_000 });
|
|
await expect(card).not.toHaveClass(/offline/, { timeout: 20_000 });
|
|
await expect(card.locator('.cn-status-dot')).toHaveClass(/on/, { timeout: 5_000 });
|
|
return card;
|
|
}
|
|
|
|
export async function loginToDashboard(page: Page): Promise<void> {
|
|
const commandDeck = page.getByRole('heading', { name: 'Command Deck' });
|
|
const loginHeading = page.getByRole('heading', { name: 'AetherForge' });
|
|
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
await page.goto('/', { waitUntil: 'load', timeout: 30_000 });
|
|
|
|
if (await page.getByText('No frontend configured').isVisible().catch(() => false)) {
|
|
throw new Error(
|
|
'Dashboard SPA missing — run `npm run build` in server/web (webroot must exist before phase 8)',
|
|
);
|
|
}
|
|
|
|
await page.locator('#root').waitFor({ state: 'attached', timeout: 15_000 }).catch(() => {});
|
|
|
|
if (await commandDeck.isVisible().catch(() => false)) {
|
|
return;
|
|
}
|
|
try {
|
|
await expect(loginHeading).toBeVisible({ timeout: 25_000 });
|
|
} catch (err) {
|
|
if (attempt === 2) throw err;
|
|
await page.waitForTimeout(2_000);
|
|
continue;
|
|
}
|
|
|
|
await page.getByLabel('Username').fill(E2E_USER);
|
|
await page.getByLabel('Password').fill(E2E_PASS);
|
|
await page.getByRole('button', { name: /enter command deck/i }).click();
|
|
|
|
try {
|
|
await expect(commandDeck).toBeVisible({ timeout: 25_000 });
|
|
return;
|
|
} catch (err) {
|
|
if (attempt === 2) throw err;
|
|
await page.waitForTimeout(2_000);
|
|
}
|
|
}
|
|
}
|