152 lines · 5.5 KB · TypeScript
RawDownload
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 {  AuthTemplateSlug,  ConnectionName,  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"; const api = composePluginApi([openApiHttpPlugin()] as const); const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; const oauthIntegrationSpec = (oauth: {  readonly authorizationEndpoint: string;  readonly tokenEndpoint: string;}) =>  ({    spec: {      kind: "blob" as const,      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" } },            },          },        },      }),    },    baseUrl: "http://127.0.0.1:59999",    authenticationTemplate: [      {        slug: "oauth",        kind: "oauth2" as const,        authorizationUrl: oauth.authorizationEndpoint,        tokenUrl: oauth.tokenEndpoint,        scopes: ["read"],      },    ],  }) as const; scenario(  "OAuth callback · a signed-out callback uses login returnTo and resumes the connection",  {},  Effect.gen(function* () {    const target = yield* Target;    const { client: makeApiClient } = yield* Api;    const browser = yield* Browser;    const oauth = yield* serveOAuthTestServer();    const identity = yield* target.newIdentity();    const client = yield* makeApiClient(api, identity);     const integration = IntegrationSlug.make(unique("signedoutcb"));    yield* client.openapi.addSpec({      payload: { ...oauthIntegrationSpec(oauth), slug: integration },    });     const clientSlug = OAuthClientSlug.make(unique("signedoutc"));    yield* client.oauth.createClient({      payload: {        owner: "org",        slug: clientSlug,        authorizationUrl: oauth.authorizationEndpoint,        tokenUrl: oauth.tokenEndpoint,        grant: "authorization_code",        clientId: "test-client",        clientSecret: "test-secret",      },    });     const started = yield* client.oauth.start({      payload: {        client: clientSlug,        clientOwner: "org",        owner: "org",        name: ConnectionName.make("main"),        integration,        template: AuthTemplateSlug.make("oauth"),      },    });    expect(started.status, "oauth.start begins at the provider").toBe("redirect");    const authorizationUrl = started.status === "redirect" ? started.authorizationUrl : "";     const authorize = yield* Effect.promise(() => fetch(authorizationUrl, { redirect: "manual" }));    expect(authorize.status, "the provider asks the user to log in").toBe(302);    const consent = yield* Effect.promise(() =>      fetch(authorize.headers.get("location") ?? "", {        method: "POST",        redirect: "manual",        headers: {          authorization: `Basic ${Buffer.from("alice:password").toString("base64")}`,        },      }),    );    expect(consent.status, "provider consent redirects back to Executor").toBe(302);    const callback = new URL(consent.headers.get("location") ?? "");    const callbackPath = `${callback.pathname}${callback.search}`;     // No cookies: this mirrors a provider redirect reaching the callback after    // the user's web session is gone, expired, or otherwise missing an org.    const anonymous = { label: "anonymous" };     yield* browser.session(anonymous, async ({ page, step }) => {      await step("Provider sends a signed-out browser to the OAuth callback", async () => {        const response = await page.goto(callbackPath, { waitUntil: "commit" });        expect(response?.status(), "the callback redirects into the login flow").toBe(200);        await page.getByText("Sign in to manage your tools and sources").waitFor();      });       const loginUrl = new URL(page.url());      expect(loginUrl.pathname, "the signed-out callback lands on the sign-in page").toBe("/login");      expect(        loginUrl.searchParams.get("returnTo"),        "login preserves the callback so it can resume after sign-in",      ).toBe(callbackPath);       await step("Sign in resumes the original OAuth callback", async () => {        await page.getByRole("link", { name: "Sign in" }).click();        await page.getByPlaceholder("new-user@example.com").fill(identity.credentials!.email);        await page.getByRole("button", { name: /Continue/ }).click();        await page.waitForURL((url) => url.pathname === "/api/oauth/callback", {          timeout: 30_000,        });        await page.waitForFunction(() => document.body.innerText.includes("Connected"), null, {          timeout: 30_000,        });      });       const body = (await page.locator("body").textContent())?.trim() ?? "";      expect(new URL(page.url()).pathname, "the login returnTo lands back on the callback").toBe(        "/api/oauth/callback",      );      expect(body, "the callback completes after the sign-in recovery").toContain("Connected");      expect(body, "the raw protected API JSON is not shown").not.toContain(        '"code":"no_organization"',      );    });  }).pipe(Effect.scoped),);