1175 lines · 41.5 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Dispatch tests for non-JSON request bodies.//// Each case spins up an Effect HttpApi-backed test server, derives the// OpenAPI spec from that API, and asserts both the wire-level content type// and body shape the plugin actually sent.//// The scenarios mirror what real specs commonly carry — multipart uploads// (files + scalar fields), XML bodies declared as pre-serialized strings,// text/plain payloads, and raw octet-stream byte uploads.//// v2: tools are produced per-connection, so each case adds the integration via// `addSpec` AND creates a connection before executing the full tool address.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Effect, Exit, Schema } from "effect";import { FetchHttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";import {  HttpApi,  HttpApiBuilder,  HttpApiEndpoint,  HttpApiGroup,  HttpApiSchema,} from "effect/unstable/httpapi"; import { createExecutor } from "@executor-js/sdk";import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";import {  addOpenApiTestConnection,  serveOpenApiHttpApiTestServer,} from "@executor-js/plugin-openapi/testing"; import { openApiPlugin } from "./plugin"; const JsonNameBody = Schema.fromJsonString(  Schema.Struct({    name: Schema.String,  }),);const decodeJsonNameBody = Schema.decodeUnknownSync(JsonNameBody); const testPlugins = () =>  [openApiPlugin({ httpClientLayer: FetchHttpClient.layer }), memoryCredentialsPlugin()] as const; type Captured = {  contentType: string;  body: Buffer;}; const Ok = Schema.Struct({ ok: Schema.Boolean }); const startEchoServer = (options: {  readonly name?: string;  readonly path?: `/${string}`;  readonly payload: Schema.Top | readonly Schema.Top[];  readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;}) =>  Effect.gen(function* () {    const captured: Captured = { contentType: "", body: Buffer.alloc(0) };    const endpointName = options.name ?? "submit";    const path = options.path ?? "/submit";    const group = HttpApiGroup.make("body").add(      HttpApiEndpoint.post(endpointName, path, {        payload: options.payload,        success: Ok,      }),    );    const api = HttpApi.make(`bodyTest_${endpointName}`).add(group);    const handlersLayer = HttpApiBuilder.group(api, "body", (handlers) =>      handlers.handleRaw(endpointName, () =>        Effect.gen(function* () {          const request = yield* HttpServerRequest.HttpServerRequest;          captured.contentType = request.headers["content-type"] ?? "";          const body = yield* request.arrayBuffer.pipe(            Effect.catch(() => Effect.succeed(new ArrayBuffer(0))),          );          captured.body = Buffer.from(body);          return HttpServerResponse.jsonUnsafe({ ok: true });        }),      ),    );    const server = yield* serveOpenApiHttpApiTestServer({      api,      handlersLayer,      transformSpec: options.transformSpec,    });    return { server, captured };  }); const ObjectBody = Schema.Struct({  name: Schema.optional(Schema.String),  flag: Schema.optional(Schema.Boolean),  count: Schema.optional(Schema.Number),}); const JsonNameObject = Schema.Struct({ name: Schema.String }); const contentFor = (contentType: string) => ({  [contentType]: {    schema: { type: "object" },  },}); const replaceResponseContent =  (path: string, operation: string, content: Record<string, unknown>) =>  (spec: Record<string, unknown>): Record<string, unknown> => {    const paths = { ...(spec.paths as Record<string, unknown>) };    const pathItem = { ...(paths[path] as Record<string, unknown>) };    const operationSpec = { ...(pathItem[operation] as Record<string, unknown>) };    const responses = { ...(operationSpec.responses as Record<string, unknown>) };    const ok = { ...(responses["200"] as Record<string, unknown>) };    responses["200"] = { ...ok, content };    pathItem[operation] = { ...operationSpec, responses };    paths[path] = pathItem;    return { ...spec, paths };  }; const replaceOperationServers =  (path: string, operation: string, servers: readonly Record<string, unknown>[]) =>  (spec: Record<string, unknown>): Record<string, unknown> => {    const paths = { ...(spec.paths as Record<string, unknown>) };    const pathItem = { ...(paths[path] as Record<string, unknown>) };    const operationSpec = { ...(pathItem[operation] as Record<string, unknown>) };    pathItem[operation] = { ...operationSpec, servers };    paths[path] = pathItem;    return { ...spec, paths };  }; const replaceRequestBodyContent =  (    path: string,    operation: string,    content: Record<string, unknown>,    encoding?: Record<string, unknown>,  ) =>  (spec: Record<string, unknown>): Record<string, unknown> => {    const paths = { ...(spec.paths as Record<string, unknown>) };    const pathItem = { ...(paths[path] as Record<string, unknown>) };    const operationSpec = { ...(pathItem[operation] as Record<string, unknown>) };    const requestBody = { ...(operationSpec.requestBody as Record<string, unknown>) };    pathItem[operation] = {      ...operationSpec,      requestBody: {        ...requestBody,        content: encoding          ? Object.fromEntries(              Object.entries(content).map(([key, value]) => [                key,                { ...(value as Record<string, unknown>), encoding },              ]),            )          : content,      },    };    paths[path] = pathItem;    return { ...spec, paths };  }; describe("OpenAPI non-JSON request body dispatch", () => {  it.effect("multipart/form-data: object body is encoded as real multipart", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: ObjectBody.pipe(HttpApiSchema.asMultipart()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, { slug: "mp" });       yield* executor.execute(conn.address("body.submit"), {        body: { name: "Acme", flag: true, count: 7 },      });       expect(captured.contentType).toMatch(/^multipart\/form-data; boundary=/);      const body = captured.body.toString("utf8");      expect(body).toContain('name="name"');      expect(body).toContain("Acme");      expect(body).toContain('name="flag"');      expect(body).toContain("true");      expect(body).toContain('name="count"');      expect(body).toContain("7");      // Regression guard: never ship [object Object] over multipart.      expect(body).not.toContain("[object Object]");    }),  );   it.effect("application/xml: string body passes through with xml content-type", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.String.pipe(HttpApiSchema.asText({ contentType: "application/xml" })),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, { slug: "xml" });       const xml = '<?xml version="1.0"?><root><name>Acme</name></root>';      yield* executor.execute(conn.address("body.submit"), { body: xml });       expect(captured.contentType).toBe("application/xml");      expect(captured.body.toString("utf8")).toBe(xml);    }),  );   it.effect("text/xml: object body is JSON-stringified (never '[object Object]')", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: JsonNameObject,        transformSpec: replaceRequestBodyContent("/submit", "post", contentFor("text/xml")),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, { slug: "tx" });       yield* executor.execute(conn.address("body.submit"), { body: { name: "Acme" } });       expect(captured.contentType).toBe("text/xml");      const body = captured.body.toString("utf8");      expect(body).not.toBe("[object Object]");      expect(decodeJsonNameBody(body)).toEqual({ name: "Acme" });    }),  );   it.effect("text/plain: string body passes through with text/plain", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.String.pipe(HttpApiSchema.asText()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, { slug: "tp" });       yield* executor.execute(conn.address("body.submit"), { body: "hello, world" });       expect(captured.contentType).toBe("text/plain");      expect(captured.body.toString("utf8")).toBe("hello, world");    }),  );   it.effect("application/octet-stream: Uint8Array passes through as bytes", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, { slug: "bin" });       const payload = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0x00, 0x01, 0x02]);      yield* executor.execute(conn.address("body.submit"), { body: payload });       expect(captured.contentType).toBe("application/octet-stream");      expect(captured.body.length).toBe(payload.length);      expect(Array.from(captured.body)).toEqual(Array.from(payload));    }),  );   it.effect("application/octet-stream: bodyBase64 passes through as bytes", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, { slug: "bin_b64" });       yield* executor.execute(conn.address("body.submit"), {        bodyBase64: "3q2+7w==",      });       expect(captured.contentType).toBe("application/octet-stream");      expect(Array.from(captured.body)).toEqual([0xde, 0xad, 0xbe, 0xef]);    }),  );   it.effect("application/octet-stream: invalid bodyBase64 fails before dispatch", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, { slug: "bin_b64_bad" });       const exit = yield* executor        .execute(conn.address("body.submit"), {          bodyBase64: "@@",        })        .pipe(Effect.exit);       expect(Exit.isFailure(exit)).toBe(true);      expect(captured.contentType).toBe("");      expect(captured.body.length).toBe(0);    }),  );   it.effect("application/octet-stream: invalid nested bodyBase64 fails before dispatch", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, {        slug: "bin_nested_b64_bad",      });       const exit = yield* executor        .execute(conn.address("body.submit"), {          body: { bodyBase64: "@@" },        })        .pipe(Effect.exit);       expect(Exit.isFailure(exit)).toBe(true);      expect(captured.contentType).toBe("");      expect(captured.body.length).toBe(0);    }),  );   it.effect("application/octet-stream: required body fails before dispatch when missing", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, {        slug: "bin_b64_missing",      });       const exit = yield* executor.execute(conn.address("body.submit"), {}).pipe(Effect.exit);       expect(Exit.isFailure(exit)).toBe(true);      expect(captured.contentType).toBe("");      expect(captured.body.length).toBe(0);    }),  );   it.effect("application/octet-stream: object body fails before dispatch", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, {        slug: "bin_object_body",      });       const exit = yield* executor        .execute(conn.address("body.submit"), {          body: { name: "photo.png" },        })        .pipe(Effect.exit);       expect(Exit.isFailure(exit)).toBe(true);      expect(captured.contentType).toBe("");      expect(captured.body.length).toBe(0);    }),  );   it.effect("application/octet-stream: string body passes through as UTF-8 bytes", () =>    Effect.gen(function* () {      const { server, captured } = yield* startEchoServer({        payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),      });       const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));       const conn = yield* addOpenApiTestConnection(executor, server, {        slug: "bin_string_body",      });       // A plain string `body` is the long-standing way callers upload text      // content to an octet-stream endpoint, so it must reach the wire rather      // than failing. It is sent verbatim as UTF-8 bytes (not base64-decoded),      // which preserves the pre-bodyBase64 behavior; binary uploads still use      // `bodyBase64`.      const text = "plain file contents, not base64";      yield* executor.execute(conn.address("body.submit"), { body: text });       expect(captured.contentType).toBe("application/octet-stream");      expect(captured.body.toString("utf8")).toBe(text);    }),  );   it.effect(    "format: byte response: Gmail-style image attachment data is exposed as a file artifact",    () =>      Effect.gen(function* () {        const attachmentBytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]);        const attachmentBase64Url = "_9j_4AAQ";        const MessagePartBody = Schema.Struct({          data: Schema.String,          size: Schema.Number,        });        const group = HttpApiGroup.make("gmail").add(          HttpApiEndpoint.get("getAttachment", "/attachments/:id", {            success: MessagePartBody,          }),        );        const api = HttpApi.make("gmailAttachmentTest").add(group);        const handlersLayer = HttpApiBuilder.group(api, "gmail", (handlers) =>          handlers.handleRaw("getAttachment", () =>            Effect.succeed(              HttpServerResponse.jsonUnsafe({                data: attachmentBase64Url,                size: attachmentBytes.byteLength,              }),            ),          ),        );        const server = yield* serveOpenApiHttpApiTestServer({          api,          handlersLayer,          transformSpec: replaceResponseContent("/attachments/{id}", "get", {            "application/json": {              schema: {                type: "object",                properties: {                  data: {                    type: "string",                    format: "byte",                    description: "The body data as a base64url encoded string.",                  },                  size: {                    type: "integer",                    format: "int32",                    description: "Number of bytes for the message part data.",                  },                },                required: ["data", "size"],              },            },          }),        });         const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));        const conn = yield* addOpenApiTestConnection(executor, server, { slug: "gmail" });         const schema = yield* executor.tools.schema(conn.address("gmail.getAttachment"));        expect(schema?.outputSchema).toMatchObject({          properties: {            _tag: { enum: ["ToolFile"] },            data: { contentEncoding: "base64" },          },        });         const result = yield* executor.execute(conn.address("gmail.getAttachment"), {          id: "att_1",        });         expect(result).toMatchObject({          ok: true,          data: {            _tag: "ToolFile",            encoding: "base64",            mimeType: "image/jpeg",            data: "/9j/4AAQ",            byteLength: attachmentBytes.byteLength,          },        });      }),  );   it.effect("format: byte response: UTF-8 attachment data falls back to text/plain", () =>    Effect.gen(function* () {      const attachmentText = [        "id,name,status,amount",        "1,Ada,confirmed,42.50",        "2,Grace,pending,13.75",        "3,Linus,cancelled,0.00",        "",      ].join("\n");      const attachmentBase64Url =        "aWQsbmFtZSxzdGF0dXMsYW1vdW50CjEsQWRhLGNvbmZpcm1lZCw0Mi41MAoyLEdyYWNlLHBlbmRpbmcsMTMuNzUKMyxMaW51cyxjYW5jZWxsZWQsMC4wMAo";      const MessagePartBody = Schema.Struct({        data: Schema.String,        size: Schema.Number,      });      const group = HttpApiGroup.make("gmailText").add(        HttpApiEndpoint.get("getAttachment", "/attachments/:id", {          success: MessagePartBody,        }),      );      const api = HttpApi.make("gmailTextAttachmentTest").add(group);      const handlersLayer = HttpApiBuilder.group(api, "gmailText", (handlers) =>        handlers.handleRaw("getAttachment", () =>          Effect.succeed(            HttpServerResponse.jsonUnsafe({              data: attachmentBase64Url,

Lines 1–500 of 1175.