158 lines · 6.8 KB · TypeScript
RawDownload
// Cloud (browser): abandoning an OAuth connection must not wedge the// add-connection modal. A user picks a registered OAuth app, clicks "Connect// with OAuth" (the modal opens the provider popup and flips to a busy// "Connecting…" state), then bails by closing the popup without granting consent.// The popup-closed signal is intentionally not polled (providers' COOP headers// make `popup.closed` unreliable), so the modal can't detect the abandonment on// its own. The guarantee under test: closing the modal afterwards RESETS it, so// reopening offers a fresh attempt instead of staying stuck on "Connecting…".//// Repro for the user report: "I bailed on finishing the OAuth connection … the// Executor app can't detect [it]. But closing the modal should reset the state// so I can try again."import { randomBytes } from "node:crypto"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { composePluginApi } from "@executor-js/api/server";import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";import { IntegrationSlug, OAuthClientSlug } from "@executor-js/sdk/shared";import { serveOAuthTestServer } from "@executor-js/sdk/testing"; import { scenario } from "../src/scenario";import { Api, Browser, Target } from "../src/services";import { visit, settle } from "../src/surfaces/browser"; const api = composePluginApi([openApiHttpPlugin()] as const); const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; scenario(  "Connections · closing the add-connection modal after abandoning OAuth lets you try again (not stuck on Connecting)",  { timeout: 120_000 },  Effect.scoped(    Effect.gen(function* () {      const target = yield* Target;      const browser = yield* Browser;      const { client: makeClient } = yield* Api;      // A real authorization server on 127.0.0.1: the modal's popup navigates to      // its authorize page, which we abandon by closing the window.      const oauth = yield* serveOAuthTestServer();      const identity = yield* target.newIdentity();      const client = yield* makeClient(api, identity);       // An integration that declares an OAuth auth method (no DCR: it carries      // explicit endpoints, no registration/discovery URL), so the modal shows      // the bring-your-own app picker.      const integration = IntegrationSlug.make(unique("oauthint"));      yield* client.openapi.addSpec({        payload: {          spec: {            kind: "blob",            value: JSON.stringify({              openapi: "3.0.3",              info: { title: "OAuth-protected API", version: "1.0.0" },              paths: {                "/me": {                  get: {                    operationId: "getMe",                    tags: ["default"],                    responses: { "200": { description: "the caller" } },                  },                },              },            }),          },          slug: integration,          baseUrl: "http://127.0.0.1:59999",          authenticationTemplate: [            {              slug: "oauth",              kind: "oauth2",              authorizationUrl: oauth.authorizationEndpoint,              tokenUrl: oauth.tokenEndpoint,              scopes: ["read"],            },          ],        },      });       // A registered OAuth app whose endpoints match the integration's, so the      // picker auto-selects it and the footer offers "Connect with OAuth".      const clientSlug = OAuthClientSlug.make(unique("oauthc"));      yield* client.oauth.createClient({        payload: {          owner: "org",          slug: clientSlug,          authorizationUrl: oauth.authorizationEndpoint,          tokenUrl: oauth.tokenEndpoint,          grant: "authorization_code",          clientId: "test-client",          clientSecret: "test-secret",        },      });       yield* browser.session(identity, async ({ page, step }) => {        const dialog = page.getByRole("dialog");        const addConnection = page.getByRole("button", { name: "Add connection", exact: true });        const connectWithOAuth = dialog.getByRole("button", { name: "Connect with OAuth" });        const connecting = dialog.getByRole("button", { name: "Connecting…" });         await step("Open the integration and start a new connection", async () => {          await visit(page, `/integrations/${integration}`);          await addConnection.click();          // The registered app is auto-selected, so the OAuth connect button is          // present and enabled.          await connectWithOAuth.waitFor({ state: "visible", timeout: 15_000 });          expect(            await connectWithOAuth.isDisabled(),            "the auto-selected app makes Connect with OAuth actionable",          ).toBe(false);        });         await step("Begin OAuth, then bail by closing the provider popup", async () => {          const [popup] = await Promise.all([page.waitForEvent("popup"), connectWithOAuth.click()]);          // The footer flips to the busy "Connecting…" state while the popup is          // open; the flow is genuinely in flight.          await connecting.waitFor({ state: "visible", timeout: 15_000 });          // Let the popup actually reach the authorize page so the OAuth session          // is live, then abandon it: the user closes the window without          // granting consent.          await popup.waitForURL((url) => !url.href.startsWith("about:"), { timeout: 15_000 });          await popup.close();        });         await step("Close the modal", async () => {          // The Close button is disabled while busy, so the user backs out with          // Escape, exactly the "bail" path from the report.          await page.keyboard.press("Escape");          await dialog.waitFor({ state: "hidden", timeout: 15_000 });        });         await step(          "Reopen the modal: it offers a fresh attempt, not a stuck Connecting",          async () => {            await addConnection.click();            await dialog.waitFor({ state: "visible", timeout: 15_000 });            await settle(page);             // The guarantee: the reopened modal is reset. Before the fix it stays            // wedged on "Connecting…" (the abandoned flow's busy state survived the            // close), so this count is 1 and the test fails, reproducing the bug.            expect(              await connecting.count(),              "the reopened modal must not be stuck in the Connecting state",            ).toBe(0);             // And a fresh OAuth attempt is actually offered and actionable again.            await connectWithOAuth.waitFor({ state: "visible", timeout: 15_000 });            expect(              await connectWithOAuth.isDisabled(),              "the reopened modal lets the user start OAuth again",            ).toBe(false);          },        );      });    }),  ),);