101 lines · 3.9 KB · TypeScript
RawDownload
// Cross-target (browser): OpenAPI security strategies that require multiple// API key headers must collect one credential value per header. Cloudflare's// legacy API key auth is the concrete regression: one OpenAPI security object// requires both `api_email` and `api_key`, so Add connection must show two// credential inputs, not one shared token field.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 } from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario";import { Api, Browser, Target } from "../src/services";import { visit } from "../src/surfaces/browser"; const api = composePluginApi([openApiHttpPlugin()] as const); const cloudflareStyleSpec = (): string =>  JSON.stringify({    openapi: "3.0.3",    info: { title: "Cloudflare Auth Fixture", version: "1.0.0" },    servers: [{ url: "https://api.cloudflare.test/client/v4" }],    security: [{ api_email: [], api_key: [] }],    components: {      securitySchemes: {        api_email: { type: "apiKey", in: "header", name: "X-Auth-Email" },        api_key: { type: "apiKey", in: "header", name: "X-Auth-Key" },      },    },    paths: {      "/accounts": {        get: {          operationId: "listAccounts",          summary: "List accounts",          responses: { "200": { description: "ok" } },        },      },    },  }); scenario(  "OpenAPI · the Cloudflare email and key auth method shows separate credential fields",  {},  Effect.gen(function* () {    const target = yield* Target;    const { client: makeApiClient } = yield* Api;    const browser = yield* Browser;    const identity = yield* target.newIdentity();    const apiClient = yield* makeApiClient(api, identity);    const slug = `openapi_auth_fields_${randomBytes(4).toString("hex")}`;     yield* Effect.ensuring(      Effect.gen(function* () {        yield* apiClient.openapi.addSpec({          payload: {            spec: { kind: "blob", value: cloudflareStyleSpec() },            slug,          },        });         yield* browser.session(identity, async ({ page, step }) => {          await step("Open the Cloudflare-style integration", async () => {            await visit(page, `/integrations/${slug}`);            await page.getByText("Connections").first().waitFor();          });           await step("Open the add-connection modal", async () => {            await page.getByRole("button", { name: "Add connection" }).first().click();            await page.getByRole("dialog", { name: /Add connection/ }).waitFor();          });           await step("The legacy Cloudflare method asks for email and key separately", async () => {            await page.getByRole("tab", { name: "API key (X-Auth-Email)" }).click();            const dialog = page.getByRole("dialog", { name: /Add connection/ });             const credentialInputs = dialog.locator('input[type="password"]');            await dialog.getByRole("textbox", { name: "X-Auth-Email" }).waitFor();            await dialog.getByRole("textbox", { name: "X-Auth-Key" }).waitFor();            expect(              await credentialInputs.count(),              "the modal renders one secret input for each required header",            ).toBe(2);            expect(              await dialog.getByRole("textbox", { name: "X-Auth-Email" }).isVisible(),              "email has its own input",            ).toBe(true);            expect(              await dialog.getByRole("textbox", { name: "X-Auth-Key" }).isVisible(),              "API key has its own input",            ).toBe(true);          });        });      }),      apiClient.openapi        .removeSpec({ params: { slug: IntegrationSlug.make(slug) } })        .pipe(Effect.ignore),    );  }),);