scripts/verify-connected-apps.ts
Rama main · réplica de lectura del repositorio original.
// Actual Apps modal/card in Chromium, with fake authorize responses only.
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { createServer } from "node:https";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { credentialStoreFlags } from "../server/browser/egress/gate.ts";
import { launchVerificationServer, runControlOmb } from "./control-omb.ts";
import { agentBrowser, ensureUiBrowser, sessionEnv } from "./testing/control-omb-ui.ts";
import { mountPreview, type MountedPreview } from "./testing/preview-fixture.ts";
const fixture = await launchVerificationServer();
let preview: MountedPreview | undefined;
let closeBrowser: (() => Promise<unknown>) | undefined;
let authorizationServer: ReturnType<typeof createServer> | undefined;
let authorizationBase = "";
const visitedAuthorizationPages: string[] = [];
let release!: () => void;
let hold: Promise<void> = Promise.resolve();
let failure = false;
let attempts = 0;
const requestBodies: Array<{ alias?: string }> = [];
const deletedAccounts: string[] = [];
const services: Record<string, { connected: boolean; pending: boolean; status: string; accounts: Array<{ id: string; alias: string; status: string }> }> = {};
const evidence = resolve(process.env.OMB_UI_EVIDENCE_DIR ?? ".omb-scratch/connected-apps-evidence");
mkdirSync(evidence, { recursive: true });
const pauseAuthorize = (fail = false) => {
failure = fail;
hold = new Promise<void>((done) => { release = done; });
};
try {
await runControlOmb(["doctor", "--url", fixture.info.url]);
// All native popups use this dedicated test hostname mapped to loopback.
// Its ephemeral certificate and ignored trust live only in this fixture.
const key = join(fixture.info.dataDir, "oauth-fixture.key");
const certificate = join(fixture.info.dataDir, "oauth-fixture.crt");
await promisify(execFile)("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", key, "-out", certificate, "-subj", "/CN=auth.example.test", "-days", "1"]);
authorizationServer = createServer({ key: readFileSync(key), cert: readFileSync(certificate) }, (req, res) => {
visitedAuthorizationPages.push(req.url!);
res.setHeader("content-type", "text/html"); res.end("<!doctype html><title>Fixture authorization</title><p>syntheticAuthorization</p>");
});
await new Promise<void>((done) => authorizationServer!.listen(0, "127.0.0.1", done));
const address = authorizationServer.address();
assert.ok(address && typeof address === "object");
authorizationBase = `https://auth.example.test:${address.port}`;
preview = await mountPreview(fixture, {
entry: "/scripts/testing/connected-apps-preview.tsx", route: "/__connected-apps.html", title: "Connected Apps OAuth fixture",
extraRoutes: [
{ path: "/api/connectors/catalog", handler: (_req, res) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ configured: true, mode: "self-hosted", source: "api", cards: ["gmail", "slack"].map((slug) => ({ slug, label: slug === "gmail" ? "Gmail" : "Slack", blurb: "Synthetic account", logo: null, domain: null })) }));
} },
{ path: /^\/api\/connectors(?:\/connected)?$/, handler: (_req, res) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ services }));
} },
{ path: /^\/api\/(?:connectors\/(?:gmail|slack)|bots\/fixture-bot\/connector-cards\/connector-\d+)\/authorize$/, method: "POST", handler: async (req, res) => {
const attempt = ++attempts;
const failed = failure;
let text = "";
for await (const part of req) text += String(part);
const body: { alias?: string } = text ? JSON.parse(text) : {};
requestBodies.push(body);
const slug = req.url?.startsWith("/api/connectors/") ? req.url.split("/")[3]! : null;
if (!failed && slug) services[slug] = {
connected: false, pending: true, status: "INITIALIZING",
accounts: [...(services[slug]?.accounts ?? []), { id: `ca-${attempt}`, alias: body.alias ?? `omb-retry-${attempt}`, status: "INITIALIZING" }],
};
await hold;
res.writeHead(failed ? 503 : 200, { "content-type": "application/json" });
res.end(JSON.stringify(failed ? { error: "Synthetic authorization outage" } : { url: `${authorizationBase}/authorize?attempt=${attempt}` }));
} },
{ path: /^\/api\/connectors\/gmail\/accounts\/[\w-]+$/, method: "DELETE", handler: (req, res) => {
const id = req.url!.split("/").pop()!;
deletedAccounts.push(id);
services.gmail = { ...services.gmail!, accounts: services.gmail!.accounts.filter((account) => account.id !== id) };
res.setHeader("content-type", "application/json"); res.end('{"removed":1}');
} },
{ path: /^\/api\/bots\/fixture-bot\/connector-cards\/connector-\d+\/status$/, handler: (_req, res) => {
res.setHeader("content-type", "application/json"); res.end('{"connected":false}');
} },
],
});
const browser = await ensureUiBrowser();
mkdirSync(join(fixture.info.dataDir, "tmp"), { recursive: true });
const env = { ...sessionEnv({ home: fixture.info.dataDir, session: `ca${process.pid}`, chrome: browser.chrome }), AGENT_BROWSER_SOCKET_DIR: join(fixture.info.dataDir, "ab"), AGENT_BROWSER_ARGS: [...credentialStoreFlags(), "--host-resolver-rules=MAP auth.example.test 127.0.0.1"].join("\n"), AGENT_BROWSER_IGNORE_HTTPS_ERRORS: "1" };
const command = (...args: string[]) => agentBrowser(browser.binary, env, args);
closeBrowser = () => command("close");
const evaluate = async (js: string) => (await command("eval", js)).result;
const wait = async (js: string) => {
try { return await command("wait", "--fn", js); }
catch (error) {
const state = { waitingFor: js, tabs: await command("tab", "list"), snapshot: await command("snapshot") };
writeFileSync(join(evidence, "failure.json"), JSON.stringify(state, null, 2));
await command("screenshot", join(evidence, "failure.png"), "--full");
throw new Error(`Renderer did not reach ${js}; see ${join(evidence, "failure.json")}`, { cause: error });
}
};
const click = async (name: string) => {
await wait(`[...document.querySelectorAll('button')].some(b=>b.textContent.trim()===${JSON.stringify(name)}&&!b.disabled)`);
await command("find", "role", "button", "click", "--name", name, "--exact");
};
await command("open", preview.previewUrl);
await evaluate(`window.__nativeOpen=window.open.bind(window);window.__oauthTrace=[];window.__oauthPages=[];window.__oauthResponses=0;window.__blockOAuth=false;
window.open=(url,target)=>{window.__oauthTrace.push({event:'open',url,active:navigator.userActivation.isActive});const page=window.__blockOAuth?null:window.__nativeOpen(url,target);if(page)window.__oauthPages.push(page);return page;};
const originalFetch=window.fetch.bind(window);window.fetch=(input,init)=>{const auth=String(input).includes('/authorize');if(auth)window.__oauthTrace.push({event:'fetch'});return originalFetch(input,init).then(response=>{if(auth)window.__oauthResponses++;return response;});};`);
const assertReserved = async () => {
// Native popups become the browser's active tab; inspect the app tab.
await command("tab", "t1");
const trace = await evaluate("window.__oauthTrace.slice(-2)");
assert.deepEqual(trace, [{ event: "open", url: "", active: true }, { event: "fetch" }]);
assert.equal(await evaluate("window.__oauthPages.at(-1).location.href"), "about:blank");
assert.equal(await evaluate("window.__oauthPages.at(-1).opener===null"), true);
};
const checkLink = async (selector: string, attempt: number) => {
await wait(`document.querySelector(${JSON.stringify(selector)})!==null`);
assert.deepEqual(await evaluate(`(()=>{const a=document.querySelector(${JSON.stringify(selector)});return {href:a.href,target:a.target,rel:a.rel,visible:a.getClientRects().length>0};})()`), {
href: `${authorizationBase}/authorize?attempt=${attempt}`, target: "_blank", rel: "noopener noreferrer", visible: true,
});
};
const checkAuthorizationTab = async (attempt: number) => {
const url = `${authorizationBase}/authorize?attempt=${attempt}`;
const tabs = (await command("tab", "list")).tabs as Array<{ tabId: string; url: string }>;
const page = tabs.findLast((tab) => tab.url === url) ?? tabs.findLast((tab) => tab.url === "about:blank");
assert.ok(page, "the browser must create an authorization tab");
await command("tab", page.tabId);
await wait(`location.href===${JSON.stringify(url)}&&document.body.textContent.includes('syntheticAuthorization')`);
assert.ok(visitedAuthorizationPages.includes(`/authorize?attempt=${attempt}`));
assert.equal(await evaluate("window.opener===null"), true);
await command("tab", "t1");
};
// Settings reserves synchronously even though the returned URL is delayed.
await click("Settings Apps");
await wait("document.querySelector('[data-app-tile=gmail] button[type=button]:not([aria-label]):not(:disabled)')!==null");
await command("click", "[data-app-tile=gmail] button[type=button]:not([aria-label])");
await command("fill", "[data-app-tile=gmail] form input", "fixture-work");
pauseAuthorize(); await command("click", "[data-app-tile=gmail] form button[type=submit]");
await assertReserved(); assert.equal(attempts, 1); release();
await checkLink("[data-app-tile=gmail] a[target=_blank]", 1);
await checkAuthorizationTab(1);
await command("screenshot", join(evidence, "settings-authorizing.png"), "--full");
// The same real modal remains usable when a later popup is blocked.
await evaluate("window.__blockOAuth=true");
await command("click", "[data-app-tile=gmail] button[type=button]:not([aria-label])");
await wait("document.querySelector('[role=alert]').textContent.includes('blocked')");
assert.equal(attempts, 1, "Settings Continue must reuse its issued flow");
await checkLink("[data-app-tile=gmail] a[target=_blank]", 1);
const beforeSettingsLink = ((await command("tab", "list")).tabs as unknown[]).length;
await command("click", "[data-app-tile=gmail] a[target=_blank]");
assert.equal(((await command("tab", "list")).tabs as unknown[]).length, beforeSettingsLink + 1);
await checkAuthorizationTab(1);
await command("screenshot", join(evidence, "settings-popup-blocked.png"), "--full");
// Closing Settings while another toolkit request is pending cancels its tab.
await evaluate("window.__blockOAuth=false");
await command("click", "[data-app-tile=slack] button[type=button]:not([aria-label])");
await command("fill", "[data-app-tile=slack] form input", "fixture-slack");
pauseAuthorize(); await command("click", "[data-app-tile=slack] form button[type=submit]");
await assertReserved(); assert.equal(attempts, 2);
await command("find", "role", "button", "click", "--name", "Close plugins", "--exact");
release();
await wait("window.__oauthResponses===2&&window.__oauthPages.at(-1).closed");
assert.equal(await evaluate("document.querySelector('[data-tour=apps-panel]')===null"), true);
// Reopening loses the memory-only URL, not the unfinished upstream account.
await click("Settings Apps");
await command("find", "role", "button", "click", "--name", "Refresh connection status", "--exact");
await wait("document.querySelector('[data-app-tile=gmail]').textContent.includes('fixture-work')");
await wait("document.querySelector('[data-app-tile=gmail] button[type=button]:not([aria-label])')?.textContent.trim()==='Check status'");
pauseAuthorize(); await command("click", "[data-app-tile=gmail] button[type=button]:not([aria-label])");
await assertReserved(); assert.equal(attempts, 3); assert.equal(requestBodies[2]!.alias, undefined); release();
await checkLink("[data-app-tile=gmail] a[target=_blank]", 3); await checkAuthorizationTab(3);
// A stale anchor must authorize again without replaying its old alias.
await evaluate("window.__realNow=Date.now;Date.now=()=>window.__realNow()+600001");
pauseAuthorize(); await command("click", "[data-app-tile=gmail] a[target=_blank]");
await assertReserved(); assert.equal(attempts, 4); assert.equal(requestBodies[3]!.alias, undefined); release();
await checkLink("[data-app-tile=gmail] a[target=_blank]", 4); await checkAuthorizationTab(4);
await command("find", "role", "button", "click", "--name", "Close plugins", "--exact");
await evaluate("Date.now=window.__realNow");
// Deliberately blocked window.open leaves a native, safe authorization link.
await evaluate("window.__blockOAuth=true");
pauseAuthorize(); await click("Connect securely");
assert.equal(attempts, 5); release();
await checkLink("[data-tour=connector] a[target=_blank]", 5);
await wait("document.body.textContent.includes('blocked')");
await command("screenshot", join(evidence, "card-popup-blocked.png"), "--full");
await click("Card authorizing"); await click("Open again");
assert.equal(attempts, 5, "Open again must reuse the issued flow");
const beforeCardLink = ((await command("tab", "list")).tabs as unknown[]).length;
await command("click", "[data-tour=connector] a[target=_blank]");
assert.equal(((await command("tab", "list")).tabs as unknown[]).length, beforeCardLink + 1);
await checkAuthorizationTab(5);
// A failed request closes only its reserved blank; the real retry can launch.
await click("Replace card target"); await evaluate("window.__blockOAuth=false");
pauseAuthorize(true); await click("Connect securely"); await assertReserved(); release();
await wait("document.body.textContent.includes('Synthetic authorization outage')");
assert.equal(await evaluate("window.__oauthPages.at(-1).closed"), true);
await click("Card failed");
pauseAuthorize(); await click("Try again"); await assertReserved(); release();
await click("Card authorizing");
await checkLink("[data-tour=connector] a[target=_blank]", 7);
// Replacement/unmount cannot late-open the old account after its response.
await click("Replace card target"); pauseAuthorize(); await click("Connect securely"); await assertReserved();
await click("Replace card target"); release();
await wait("window.__oauthResponses===8&&window.__oauthPages.at(-1).closed");
assert.equal(await evaluate("document.querySelector('[data-tour=connector] a[target=_blank]')===null"), true);
pauseAuthorize(); await click("Connect securely"); await assertReserved();
await click("Remove card"); release();
await wait("window.__oauthResponses===9&&window.__oauthPages.at(-1).closed");
assert.equal(await evaluate("document.querySelector('[data-tour=connector]')===null"), true);
// An app whose only account expired reads as signed out; Reconnect replaces that account under its own label.
services.gmail = { connected: true, pending: false, status: "EXPIRED", accounts: [{ id: "ca-expired", alias: "fixture-old", status: "EXPIRED" }] };
await click("Settings Apps");
await command("find", "role", "button", "click", "--name", "Refresh connection status", "--exact");
await wait("document.querySelector('[data-app-tile=gmail]').textContent.includes('Signed out')");
assert.equal(await evaluate("document.querySelector('[data-app-tile=gmail] [aria-label=Connected]')===null"), true);
assert.equal(await evaluate("document.querySelector('[data-app-tile=gmail] button[type=button]:not([aria-label])').textContent.trim()"), "Reconnect");
await command("screenshot", join(evidence, "settings-reconnect.png"), "--full");
const traced = await evaluate("window.__oauthTrace.length") as number;
pauseAuthorize(); await command("click", "[data-app-tile=gmail] button[type=button]:not([aria-label])");
// The old account is removed before the link is asked for, so the request trails the reserved tab.
await command("tab", "t1"); await wait(`window.__oauthTrace.length>=${traced + 2}`);
await assertReserved(); release();
assert.deepEqual(deletedAccounts, ["ca-expired"]);
assert.equal(requestBodies.at(-1)!.alias, "fixture-old");
await checkLink("[data-app-tile=gmail] a[target=_blank]", 10); await checkAuthorizationTab(10);
await command("find", "role", "button", "click", "--name", "Close plugins", "--exact");
// A card refused for want of a label asks for one instead of sending the same request again.
await click("Replace card target"); await click("Card needs label");
await wait("document.querySelector('[data-tour=connector] input')!==null");
await command("screenshot", join(evidence, "card-label-needed.png"), "--full");
await command("fill", "[data-tour=connector] input", "fixture-label");
pauseAuthorize(); await click("Try again"); await assertReserved(); release();
assert.equal(requestBodies.at(-1)!.alias, "fixture-label");
await wait("window.__oauthResponses===11");
const errors = await command("errors");
assert.deepEqual(errors.errors, []);
const result = { ok: true, fixtureUrl: fixture.info.url, logPath: fixture.info.logPath, attempts, evidence, tested: ["Settings synchronous reservation", "Settings blocked-popup fallback and Continue", "Settings close cancels late opening", "Settings pending inventory after reopen", "expired anchor renewal without stale alias", "card blocked-popup fallback", "native safe link navigation", "Open again reuses flow", "failure closes blank and Try again", "replacement and unmount cancel late opening", "expired-only app reconnects under its label", "card asks for a label instead of looping"], limitation: "Synthetic authorize responses in Chromium; no live provider, Safari or Brave acceptance." };
writeFileSync(join(evidence, "result.json"), JSON.stringify(result, null, 2));
console.log(JSON.stringify(result));
} finally {
release?.();
await closeBrowser?.().catch(() => {});
await preview?.close();
await new Promise<void>((done) => authorizationServer ? authorizationServer.close(() => done()) : done());
await fixture.close();
}