Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982import { 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 <script>", exact: true, }) .click(); const supersededPath = `/api/installations/${mismatch.installationId}/start`; await page.route(CP + supersededPath, (route) => route.abort("timedout")); await page .getByRole("button", { name: "Continue installation", exact: true }) .click(); await page .getByRole("button", { name: "Continue saved request" }) .waitFor(); await page.unroute(CP + supersededPath); // A different real request completes first. The frozen request must get a // definitive409, acknowledge it, and reread without starting another attempt. assert.equal( ( await call(supersededPath, { method: "POST", headers: { Cookie: await session(), "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ requestId: id() }), }) ).status, 202, ); for (let n = 0; n < 50; n++) { if ( (await list()).installations.find( (r) => r.installationId === mismatch.installationId, )?.status === "ready" ) break; await new Promise((resolve) => setTimeout(resolve, 200)); } assert.equal( (await list()).installations.find( (r) => r.installationId === mismatch.installationId, ).status, "ready", ); const conflictReply = page.waitForResponse( (r) => new URL(r.url()).pathname === supersededPath && r.status() === 409, ); await page.getByRole("button", { name: "Continue saved request" }).click(); await conflictReply; await page .getByRole("button", { name: "Continue saved request" }) .waitFor({ state: "hidden" }); await page.getByRole("heading", { name: "Flarebot is ready" }).waitFor(); assert.equal(posts.filter((p) => p.path === supersededPath).length, 2); await page .getByRole("button", { name: "Select Personal account", exact: true }) .click(); await page .getByRole("button", { name: `View installation ${ready.installationId.slice(0, 8)}`, }) .click();
// A lost start reply stays frozen while a different ready installation is // selected, including across reload. Continue targets its original ID. A // renewed OAuth grant cannot rebind that operation; explicit recovery can. await admin("provider/options", { holdWorker: true }); await page.route(CP + "/api/installations/*/start", async (route) => { await route.fetch({ url: origin + new URL(route.request().url()).pathname, }); await route.abort("timedout"); }); await page .getByRole("button", { name: "Install another Flarebot", exact: true }) .click(); await page .getByRole("button", { name: "Continue saved request" }) .waitFor(); const lostStart = posts.findLast((p) => p.path.endsWith("/start")); const interruptedId = lostStart.path.split("/")[3]; for (let n = 0; n < 50; n++) { if ((await admin("provider/inspect"))[`worker:flarebot-${interruptedId}`]) break; await new Promise((resolve) => setTimeout(resolve, 200)); } assert.ok( (await admin("provider/inspect"))[`worker:flarebot-${interruptedId}`], ); await page.unroute(CP + "/api/installations/*/start"); await page .getByRole("button", { name: `View installation ${ready.installationId.slice(0, 8)}`, }) .click(); await page.reload(); await page .getByRole("button", { name: "Reconnect Cloudflare", exact: true }) .click(); await page .getByRole("button", { name: "Select Personal account", exact: true }) .click(); const protectedReplay = page.waitForResponse( (r) => new URL(r.url()).pathname === lostStart.path && r.status() === 503, ); await page.getByRole("button", { name: "Continue saved request" }).click(); await protectedReplay; const replayedStart = posts.findLast((p) => p.path.endsWith("/start")); assert.deepEqual(replayedStart, lostStart); await page .getByRole("button", { name: "Continue saved request" }) .waitFor(); await page .getByRole("button", { name: `View installation ${interruptedId.slice(0, 8)}`, }) .click(); await page .getByText("Installation appears interrupted", { exact: true }) .click(); await page .getByText( "This replaces your saved request with a new recovery attempt for this installation.", ) .waitFor(); await page .getByRole("button", { name: "Recover installation", exact: true }) .click(); await page .locator(".installation-card .intro > p") .filter({ hasText: `Installation ${interruptedId.slice(0, 8)}`, }) .waitFor(); await page .getByRole("heading", { name: "Flarebot is ready" }) .waitFor({ timeout: 30_000 }); await admin("provider/options", {});
// An expired deployment grant is not a signed-out control-plane identity. const inspect = await admin("inspect", {}, await session()); await admin("expire", { kind: "grant", ref: inspect.principal.grantRef }); await page.reload(); await page .getByRole("alert") .filter({ hasText: "expired or was revoked" }) .waitFor(); await page.getByRole("link", { name: "Open Flarebot" }).waitFor(); assert.equal((await list()).installations.length, 4); await page .getByRole("button", { name: "Connect Cloudflare", exact: true }) .click(); await page .getByRole("button", { name: "Select Personal account", exact: true }) .click(); await page.getByRole("link", { name: "Open Flarebot" }).waitFor();
// Owner list pagination uses real reservations, never forged browser statuses. const ownerCookie = await session(); for (let n = 0; n < 47; n++) { const response = await call("/api/installations", { method: "POST", headers: { Cookie: ownerCookie, "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ requestId: id() }), }); // Complete each accepted exchange before reusing the native HTTP transport. const data = await response.json().catch(() => null); const diagnostic = reservationFailure(n + 1, response, data); assert.equal(response.status, 200, diagnostic); assert.ok(data?.installation?.status === "reserved", diagnostic); } await page.getByRole("button", { name: "Refresh status" }).click(); const installationRows = page .getByRole("list", { name: "Installations", exact: true, }) .locator("li"); await page.getByRole("button", { name: "Next page" }).waitFor(); const firstPageIds = await installationRows .locator(".installation-id") .allTextContents(); assert.equal(firstPageIds.length, 50); const failNextPage = (route) => route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: "temporarily_unavailable" }), }); await page.route(CP + "/api/installations?cursor=*", failNextPage); try { await page.getByRole("button", { name: "Next page" }).click(); await page .getByRole("alert") .filter({ hasText: "Flarebot could not be reached", }) .waitFor(); assert.deepEqual( await installationRows.locator(".installation-id").allTextContents(), firstPageIds, "Failed pagination retains the displayed native page", ); assert.equal( await page.getByRole("button", { name: "Next page" }).isEnabled(), true, ); assert.equal( await page.getByRole("button", { name: "First page" }).count(), 0, ); } finally { await page.unroute(CP + "/api/installations?cursor=*", failNextPage); } await page.getByRole("button", { name: "Next page" }).click(); await page.getByRole("button", { name: "First page" }).waitFor(); assert.equal( await page .getByRole("list", { name: "Installations", exact: true }) .locator("li") .count(), 1, ); await page.getByRole("button", { name: "First page" }).click(); await page.getByRole("button", { name: "Next page" }).waitFor(); await page .getByRole("button", { name: "First page" }) .waitFor({ state: "hidden" }); await page.waitForFunction( () => ![...document.querySelectorAll("button")].find( (button) => button.textContent.trim() === "Refresh status", )?.disabled, );
// A held native status response retains the current page and shares focus reads. const statusCaptured = Promise.withResolvers(); const statusReleased = Promise.withResolvers(); let statusReads = 0; const holdStatus = async (route) => { if (route.request().method() !== "GET") return route.continue(); statusReads++; const url = new URL(route.request().url()); const response = await route.fetch({ url: origin + url.pathname + url.search, headers: { Origin: CP, Cookie: await session() }, }); assert.equal(response.status(), 200); assert.equal((await response.json()).installations.length, 50); statusCaptured.resolve(); await statusReleased.promise; await route.fulfill({ response }); }; await page.route(CP + "/api/installations", holdStatus); const commandCount = posts.length; try { await page.getByRole("button", { name: "Refresh status" }).click(); await statusCaptured.promise; await page.getByRole("link", { name: "Open Flarebot" }).waitFor(); assert.equal( await page .getByRole("list", { name: "Installations", exact: true }) .locator("li") .count(), 50, ); await page.evaluate(() => { window.dispatchEvent(new Event("focus")); return new Promise(requestAnimationFrame); }); assert.equal(statusReads, 1, "Focus shares the in-flight status read"); assert.equal( posts.length, commandCount, "Refreshing status never submits commands", ); } finally { statusReleased.resolve(); await page.unrouteAll({ behavior: "wait" }); } await page.waitForFunction( () => ![...document.querySelectorAll("button")].find( (button) => button.textContent.trim() === "Refresh status", )?.disabled, );
await context.setOffline(true); await page.getByRole("button", { name: "Refresh status" }).click(); await page .getByRole("alert") .filter({ hasText: "could not be reached" }) .waitFor(); await page.getByRole("link", { name: "Open Flarebot" }).waitFor(); await context.setOffline(false); await page .getByRole("alert") .filter({ hasText: "could not be reached" }) .waitFor({ state: "hidden" }); assert.doesNotMatch( await page.locator("body").innerText(), /oauth-secret|provider-private|accessToken|bootstrapSecret|verified-userinfo-subject/, ); assert.equal(await page.evaluate(() => localStorage.length), 0); assert.equal(await page.evaluate(() => document.cookie), "");
// Changing the real signed-in owner clears the prior owner and saved context. const disconnected = page.waitForResponse( (response) => new URL(response.url()).pathname === "/auth/disconnect" && response.request().method() === "POST", ); await page.getByRole("button", { name: "Disconnect", exact: true }).click(); assert.equal((await disconnected).status(), 303); await page .getByRole("button", { name: "Connect Cloudflare", exact: true }) .waitFor(); assert.equal( await session(), "", "Native disconnect clears the session cookie", ); assert.equal( await page.getByRole("link", { name: "Open Flarebot" }).count(), 0, ); ownerMode = "second-owner"; const otherPage = await context.newPage(); await otherPage.goto(CP + "/connect"); await otherPage .getByRole("button", { name: "Connect Cloudflare", exact: true }) .click(); await otherPage .getByRole("button", { name: "Select Personal account", exact: true }) .waitFor(); await otherPage.close(); await page.bringToFront(); await page.evaluate(() => window.dispatchEvent(new Event("focus"))); await page .getByRole("button", { name: "Select Personal account", exact: true }) .waitFor(); const other = await list(); assert.equal(other.ownerSubject, "verified-second-owner"); assert.deepEqual(other.installations, []); const foreign = await call(`/api/installations/${ready.installationId}`, { headers: { Cookie: await session() }, }); const missing = await call(`/api/installations/${id()}`, { headers: { Cookie: await session() }, }); assert.equal(foreign.status, 404); assert.equal(missing.status, 404); assert.deepEqual(await foreign.json(), await missing.json()); assert.deepEqual(errors, []); },);