import { reportSavedUpgradeFailure, reservationFailure, } from "./fixtures/installation-diagnostics.mjs"; import { upgradeAssetHash } from "./fixtures/upgrade-artifact.mjs"; import assert from "node:assert/strict"; import { randomBytes, generateKeyPairSync } from "node:crypto"; import { execFileSync } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { createServer } from "node:net"; import { createServer as httpsServer } from "node:https"; import { request as httpRequest } from "node:http"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { test } from "node:test"; import { parse } from "jsonc-parser"; import { chromium } from "playwright"; import { unstable_dev } from "wrangler"; import { oauthBindings, oauthConfig } from "./fixtures/oauth-config.mjs"; import "./fixtures/config.mjs"; const id = () => randomBytes(16).toString("hex"); const CP = "https://publisher.test"; async function freePort() { const server = createServer(); await new Promise((r) => server.listen(0, "127.0.0.1", r)); const port = server.address().port; await new Promise((r) => server.close(r)); return port; } test( "built installation UI uses real owner routes, durable Workflow progress, frozen replay and explicit recovery", { timeout: 240_000 }, async (t) => { const temporary = await mkdtemp( join(tmpdir(), "flarebot-installation-ui-"), ); const port = await freePort(); const origin = `http://127.0.0.1:${port}`; const base = parse(await readFile("wrangler.control-plane.jsonc", "utf8")); const config = join(temporary, "wrangler.json"); await writeFile( config, JSON.stringify({ ...base, name: "flarebot-installation-ui-test", main: resolve("tests/fixtures/orchestrator-worker.ts"), assets: { directory: resolve("dist/control-plane/client"), binding: "ASSETS", }, durable_objects: { bindings: [ ...base.durable_objects.bindings, { name: "PROVIDER", class_name: "Provider" }, ], }, exports: { ...base.exports, Provider: { type: "durable-object", storage: "sqlite" }, }, }), ); const keys = generateKeyPairSync("ed25519"); const publicKey = keys.publicKey .export({ type: "spki", format: "der" }) .subarray(-32) .toString("base64url"); let worker, browser, proxy; t.after(async () => { await browser?.close(); proxy?.closeAllConnections(); await new Promise((r) => (proxy ? proxy.close(r) : r())); await worker?.stop(); await rm(temporary, { recursive: true, force: true }); }); const call = (path, options = {}) => fetch(origin + path, { redirect: "manual", ...options, headers: { Origin: CP, ...options.headers }, }); const admin = async (path, body = {}, session = "") => ( await call("/__test__/" + path, { method: "POST", headers: { Cookie: session, "Content-Type": "application/json" }, body: JSON.stringify(body), }) ).json(); worker = await unstable_dev("tests/fixtures/orchestrator-worker.ts", { config, vars: { ...oauthBindings(CP), UPGRADE_ASSET_HASH: await upgradeAssetHash(), FLAREBOT_CONTROL_PLANE: JSON.stringify({ ...oauthConfig(CP), bridge: { keyId: "fixture-key", publicKey }, }), FLAREBOT_BRIDGE_SIGNING_KEY: keys.privateKey .export({ type: "pkcs8", format: "der" }) .toString("base64url"), }, local: true, ip: "127.0.0.1", port, inspectorPort: 0, persist: true, persistTo: temporary, logLevel: "error", experimental: { disableExperimentalWarning: true, watch: false }, }); execFileSync( "openssl", [ "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", join(temporary, "key.pem"), "-out", join(temporary, "cert.pem"), "-days", "1", "-subj", "/CN=publisher.test", ], { stdio: "ignore" }, ); let ownerMode = "normal"; let delayedUpgrade; proxy = httpsServer( { key: await readFile(join(temporary, "key.pem")), cert: await readFile(join(temporary, "cert.pem")), }, async (req, res) => { if (req.headers.host === "dash.cloudflare.com") { const url = new URL(req.url, "https://dash.cloudflare.com"); const code = id(); await admin("code", { code, challenge: url.searchParams.get("code_challenge"), mode: ownerMode, }); const callback = new URL(url.searchParams.get("redirect_uri")); callback.search = new URLSearchParams({ code, state: url.searchParams.get("state"), }); res.writeHead(303, { location: callback.href, "cache-control": "no-store", "referrer-policy": "no-referrer", }); res.end(); return; } const headers = { ...req.headers }; delete headers.host; if (delayedUpgrade?.path === req.url && req.method === "POST") headers["accept-encoding"] = "identity"; const outgoing = httpRequest( { host: "127.0.0.1", port, path: req.url, method: req.method, headers, }, async (incoming) => { if ( delayedUpgrade && req.method === "POST" && req.url === delayedUpgrade.path ) { const held = delayedUpgrade; delayedUpgrade = null; const chunks = []; for await (const chunk of incoming) chunks.push(chunk); const body = Buffer.concat(chunks); held.accepted.resolve({ status: incoming.statusCode, body: JSON.parse(body.toString()), }); // Keep the genuine accepted response beyond the former UI // deadline, without substituting a success or masking a failure. await new Promise((resolve) => setTimeout(resolve, 16_000)); if (!res.destroyed) { res.writeHead(incoming.statusCode, incoming.headers); res.end(body); } held.released.resolve(); return; } res.writeHead(incoming.statusCode, incoming.headers); incoming.pipe(res); }, ); outgoing.on("error", () => { res.writeHead(502); res.end(); }); req.pipe(outgoing); }, ); await new Promise((r) => proxy.listen(0, "127.0.0.1", r)); browser = await chromium.launch({ args: [ "--no-proxy-server", `--host-resolver-rules=MAP publisher.test 127.0.0.1:${proxy.address().port}, MAP dash.cloudflare.com 127.0.0.1:${proxy.address().port}`, ], }); const context = await browser.newContext({ ignoreHTTPSErrors: true }); const page = await context.newPage(); page.setDefaultTimeout(15_000); const errors = []; const reads = []; page.on("response", async (response) => { const path = new URL(response.url()).pathname; if ( ["/api/connection", "/api/installations"].includes(path) && response.request().method() === "GET" ) { const result = await response.json().catch(() => ({})); reads.push({ path, status: response.status(), error: result.error }); } }); page.on("pageerror", (e) => errors.push(e.message)); const posts = []; page.on("request", (r) => { if ( r.method() === "POST" && new URL(r.url()).pathname.startsWith("/api/installations") ) posts.push({ path: new URL(r.url()).pathname, body: r.postData() }); }); const session = async () => (await context.cookies()) .filter((c) => c.name === "__Host-flarebot-control-session") .map((c) => `${c.name}=${c.value}`) .join("; "); const list = async () => ( await call("/api/installations", { headers: { Cookie: await session() }, }) ).json(); await admin("provider/options", { missingOAuthCapabilities: true }); await page.goto(CP + "/connect"); await page .getByRole("alert") .filter({ hasText: "publisher needs to verify" }) .waitFor() .catch(async (error) => { throw new Error( `${error.message}\nPage errors: ${JSON.stringify(errors)}\nReads: ${JSON.stringify(reads)}\nPublisher: ${await page.locator("body").innerText()}`, ); }); assert.equal( await page .getByRole("button", { name: "Connect Cloudflare", exact: true }) .count(), 0, ); await admin("provider/options", {}); await page.getByRole("button", { name: "Try again", exact: true }).click(); await page .getByRole("button", { name: "Connect Cloudflare", exact: true }) .click(); await page .getByRole("button", { name: "Select Personal account", exact: true }) .click(); await page .getByText("Account selected. New installations will use this account.") .waitFor(); // Lose a genuine committed reservation response; reload must replay its exact request. await page.route(CP + "/api/installations", async (route) => { if (route.request().method() !== "POST") return route.continue(); await route.fetch({ url: origin + "/api/installations" }); await route.abort("timedout"); }); await page .getByRole("button", { name: "Install Flarebot", exact: true }) .dblclick(); await page .getByRole("button", { name: "Continue saved request" }) .waitFor(); assert.equal((await list()).installations.length, 1); const requestId = posts[0].body; await page.unroute(CP + "/api/installations"); await page.reload(); await page .getByRole("button", { name: "Reconnect Cloudflare", exact: true }) .click(); await page .getByRole("button", { name: "Select Personal account", exact: true }) .click(); await page .getByRole("button", { name: "Continue saved request" }) .waitFor(); const record = (await list()).installations[0]; await admin("provider/options", { holdProgress: true }); const releaseProgress = (phase) => admin("provider/release-progress", { name: record.resources.sandboxApplicationName, phase, }); try { await page .getByRole("button", { name: "Continue saved request" }) .click(); await page .getByRole("heading", { name: "Installing", exact: true }) .waitFor(); assert.equal( posts.findLast((p) => p.path === "/api/installations").body, requestId, ); assert.equal((await list()).installations[0].installedRelease, null); await page.reload(); await page .getByRole("heading", { name: "Installing", exact: true }) .waitFor(); for (const [phase, label] of [ ["provisioning", "Provisioning your Sandbox"], ["verifying", "Verifying installation"], ]) { await page .locator('[aria-current="step"]') .filter({ hasText: label }) .waitFor({ timeout: 30_000 }); const active = (await list()).installations[0]; assert.equal(active.progress, phase); assert.equal(active.installedRelease, null); assert.equal((await releaseProgress(phase)).released, true); } } finally { await Promise.all([ releaseProgress("provisioning"), releaseProgress("verifying"), ]); } await page .getByRole("heading", { name: "Flarebot is ready" }) .waitFor({ timeout: 30_000 }); assert.equal((await list()).installations.length, 1); const ready = (await list()).installations[0]; assert.equal(ready.progress, "complete"); assert.ok(ready.installedRelease); assert.equal( await page .getByRole("link", { name: "Open Flarebot" }) .getAttribute("href"), `${ready.resources.runtimeOrigin}/auth/login`, ); assert.equal( await page .locator(".progress .step-state") .allTextContents() .then((a) => a.every((v) => v === "Complete")), true, ); await page.getByText("Up to date", { exact: true }).waitFor(); await admin("provider/options", { upgradeLatest: true }); await page.getByRole("button", { name: "Refresh status" }).click(); await page .getByRole("button", { name: "Upgrade Flarebot", exact: true }) .waitFor(); const upgradePath = `/api/installations/${ready.installationId}/upgrade`; const upgradeAborted = Promise.withResolvers(); await page.route(CP + upgradePath, async (route) => { try { await route.abort("timedout"); upgradeAborted.resolve(); } catch (error) { upgradeAborted.reject(error); } }); await page .getByRole("button", { name: "Upgrade Flarebot", exact: true }) .click(); const abortDeadline = setTimeout( () => upgradeAborted.reject(new Error("Upgrade interception did not abort")), 15_000, ); try { await upgradeAborted.promise; } finally { clearTimeout(abortDeadline); } await page .getByRole("button", { name: "Continue saved request" }) .waitFor(); const upgradeRequest = posts.findLast((p) => p.path === upgradePath); assert.equal( JSON.parse(new URLSearchParams(upgradeRequest.body).get("target")) .version, "0.1.0-fixture.2", ); const unsubmitted = (await list()).installations[0]; assert.equal(unsubmitted.status, "ready"); assert.deepEqual(unsubmitted.installedRelease, ready.installedRelease); await page.unroute(CP + upgradePath); await admin("provider/options", { upgradeLatest: false }); await page.reload(); const heldUpgrade = { path: upgradePath, accepted: Promise.withResolvers(), released: Promise.withResolvers(), }; delayedUpgrade = heldUpgrade; try { await page .getByRole("button", { name: "Continue saved request" }) .click(); } catch (error) { await reportSavedUpgradeFailure(page, errors.length); throw error; } const acceptedUpgrade = await heldUpgrade.accepted.promise; assert.equal(acceptedUpgrade.status, 202); assert.equal( acceptedUpgrade.body.installation.installationId, ready.installationId, ); await heldUpgrade.released.promise; assert.equal( await page .getByRole("alert") .filter({ hasText: "Flarebot could not be reached" }) .count(), 0, "a genuine accepted upgrade response held for 16 seconds must not be reported as a network failure", ); await page .getByText("Installed version 0.1.0-fixture.2", { exact: true }) .waitFor({ timeout: 30000 }); assert.deepEqual( posts.findLast((p) => p.path === upgradePath), upgradeRequest, ); const upgraded = (await list()).installations.find( (r) => r.installationId === ready.installationId, ); assert.deepEqual(upgraded.resources, ready.resources); await admin("provider/options", { upgradeLatest: true }); await page.getByRole("button", { name: "Refresh status" }).click(); await page.getByText("Up to date", { exact: true }).waitFor(); await admin("provider/options", {}); const navigation = page.getByRole("navigation", { name: "Control plane navigation", }); const selectedAccount = await page .locator('.account-list [data-selected="true"]') .innerText(); const selectedInstallation = await page .locator('.installation-list [data-selected="true"]') .innerText(); for (const width of [1440, 900, 390]) { await page.setViewportSize({ width, height: 900 }); for (const [label, target] of [ ["Cloudflare account", "cloudflare-account"], ["Installations", "installations"], ["Permissions and access", "permissions"], ]) { await navigation .getByRole("link", { name: label, exact: true }) .focus(); await page.keyboard.press("Enter"); await page.waitForFunction((id) => { const section = document.getElementById(id); const rect = section.getBoundingClientRect(); return ( document.activeElement === section && rect.top >= 48 && rect.top < innerHeight ); }, target); assert.equal(new URL(page.url()).hash, `#${target}`); } assert.equal( await page.locator('.account-list [data-selected="true"]').innerText(), selectedAccount, ); assert.equal( await page .locator('.installation-list [data-selected="true"]') .innerText(), selectedInstallation, ); assert.ok( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), `Control plane fits at ${width}px`, ); if (process.env.FLAREBOT_INSTALLATION_SCREENSHOT) { await page .getByRole("heading", { name: "Your personal Flarebot", exact: true }) .click(); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: process.env.FLAREBOT_INSTALLATION_SCREENSHOT.replace( ".png", `-overview-${width}.png`, ), }); if (width === 1440) { await navigation .getByRole("link", { name: "Installations", exact: true }) .click(); await page.screenshot({ path: process.env.FLAREBOT_INSTALLATION_SCREENSHOT.replace( ".png", "-installations-desktop.png", ), }); } } } await page.setViewportSize({ width: 390, height: 844 }); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, ); assert.deepEqual( await page .locator("p, button, label, a, summary, .step-state") .evaluateAll((elements) => elements .filter( (e) => e.getClientRects().length && parseFloat(getComputedStyle(e).fontSize) < 14, ) .map((e) => e.textContent), ), [], ); if (process.env.FLAREBOT_INSTALLATION_SCREENSHOT) await page.screenshot({ path: process.env.FLAREBOT_INSTALLATION_SCREENSHOT, fullPage: true, }); await admin("provider/options", { rejectWorker: true }); await page .getByRole("button", { name: "Install another Flarebot", exact: true }) .click(); await page .getByRole("heading", { name: "Needs attention", exact: true }) .waitFor({ timeout: 30_000 }); await page .getByRole("button", { name: "Recover installation", exact: true }) .waitFor(); const failed = (await list()).installations.find( (r) => r.status === "failed", ); assert.equal(failed.progress, "deploying"); assert.equal(failed.errorCode, "recovery_required"); await admin("provider/options", {}); await page .getByRole("button", { name: "Recover installation", exact: true }) .click(); await page .getByRole("heading", { name: "Flarebot is ready" }) .waitFor({ timeout: 30_000 }); assert.equal((await list()).installations.length, 2); assert.ok( posts.some( (p) => p.path === `/api/installations/${failed.installationId}/recover`, ), ); // Another tab changes selected account before reserve commits. Preserve the // acknowledged reservation and require an explicit selection before deploy. await call("/api/account", { method: "POST", headers: { Cookie: await session(), "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ accountId: "b".repeat(32) }), }); const beforeMismatch = posts.length; await page .getByRole("button", { name: "Install another Flarebot", exact: true }) .click(); await page .getByRole("alert") .filter({ hasText: "Select the account shown" }) .waitFor(); const mismatch = (await list()).installations.find( (r) => r.accountId === "b".repeat(32), ); assert.equal(mismatch.status, "reserved"); assert.equal( posts.slice(beforeMismatch).some((p) => p.path.endsWith("/start")), false, ); assert.equal( await page .getByRole("button", { name: "Continue installation", exact: true }) .isDisabled(), true, ); assert.equal( await page .getByRole("button", { name: "Continue saved request" }) .count(), 0, ); await page .getByRole("button", { name: "Select Team account