167 lines · 5.0 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Layer } from "effect";import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import { OpenApiParseError } from "./errors";import {  MAX_JSON_SPEC_CHARS,  MAX_SPEC_TEXT_CHARS,  MAX_YAML_SPEC_LINES,  fetchSpecText,  parse,} from "./parse"; describe("OpenAPI parse", () => {  it.effect("parses JSON OpenAPI documents", () =>    Effect.gen(function* () {      const doc = yield* parse(        JSON.stringify({          openapi: "3.1.0",          info: { title: "Test", version: "1.0.0" },          paths: {},        }),      );       expect(doc.openapi).toBe("3.1.0");    }),  );   it.effect("parses YAML OpenAPI documents", () =>    Effect.gen(function* () {      const doc = yield* parse(`openapi: 3.0.0info:  title: Test  version: 1.0.0paths: {}`);       expect(doc.openapi).toBe("3.0.0");    }),  );   it.effect("falls back to YAML for flow-style YAML documents", () =>    Effect.gen(function* () {      const doc = yield* parse(`{  openapi: 3.0.0,  info: { title: Test, version: 1.0.0 },  paths: {}}`);       expect(doc.openapi).toBe("3.0.0");    }),  );   it.effect("returns a stable parse error for empty documents", () =>    Effect.gen(function* () {      const error = yield* parse("").pipe(Effect.flip);       expect(error).toBeInstanceOf(OpenApiParseError);      expect(error).toHaveProperty("message", "OpenAPI document is empty");    }),  );   it.effect("returns a stable parse error for non-object documents", () =>    Effect.gen(function* () {      const error = yield* parse("[]").pipe(Effect.flip);       expect(error).toBeInstanceOf(OpenApiParseError);      expect(error).toHaveProperty("message", "OpenAPI document must parse to an object");    }),  );   it.effect("parses Graph-sized YAML OpenAPI documents", () =>    Effect.gen(function* () {      const largeDescription = "x".repeat(36 * 1024 * 1024);      const doc = yield* parse(        `openapi: 3.0.0info:  title: Large Test  version: 1.0.0  description: "${largeDescription}"paths: {}`,      );       expect(doc.info.title).toBe("Large Test");      expect(doc.info.description).toHaveLength(largeDescription.length);    }),  );   it.effect("rejects structure-dense YAML documents above the line cap", () =>    Effect.gen(function* () {      // Three lines per path-item; well past the cap while only a few MB of      // text — the shape (not the size) is what a whole parse cannot survive.      const pathItems = "  /a:\n    get: {}\n".repeat(Math.ceil(MAX_YAML_SPEC_LINES / 2));      const error = yield* parse(        `openapi: 3.0.0\ninfo:\n  title: Dense\n  version: 1.0.0\npaths:\n${pathItems}`,      ).pipe(Effect.flip);       expect(error).toBeInstanceOf(OpenApiParseError);      expect(error).toHaveProperty("message", expect.stringMatching(/too large to parse whole/));      expect(error).toHaveProperty("message", expect.stringMatching(/lines/));    }),  );   it.effect("rejects JSON documents above the JSON size cap", () =>    Effect.gen(function* () {      const padded = `{"openapi":"3.1.0","x-pad":"${"x".repeat(MAX_JSON_SPEC_CHARS)}"}`;      const error = yield* parse(padded).pipe(Effect.flip);       expect(error).toBeInstanceOf(OpenApiParseError);      expect(error).toHaveProperty("message", expect.stringMatching(/too large to parse/));    }),  );   it.effect("rejects any document above the text ceiling", () =>    Effect.gen(function* () {      const padded = `openapi: 3.0.0\ninfo:\n  description: "${"x".repeat(MAX_SPEC_TEXT_CHARS)}"\n`;      const error = yield* parse(padded).pipe(Effect.flip);       expect(error).toBeInstanceOf(OpenApiParseError);      expect(error).toHaveProperty("message", expect.stringMatching(/too large to parse/));    }),  );}); describe("OpenAPI fetchSpecText", () => {  const specUrl = "https://example.com/openapi.yaml";   const layerWithResponse = (response: Response) =>    Layer.succeed(HttpClient.HttpClient)(      HttpClient.make((request: HttpClientRequest.HttpClientRequest) =>        Effect.succeed(HttpClientResponse.fromWeb(request, response)),      ),    );   it.effect("rejects a document whose declared length is above the text ceiling", () =>    Effect.gen(function* () {      const error = yield* fetchSpecText(specUrl).pipe(        Effect.provide(          layerWithResponse(            new Response("openapi: 3.0.0", {              status: 200,              headers: { "content-length": String(MAX_SPEC_TEXT_CHARS + 1) },            }),          ),        ),        Effect.flip,      );       expect(error).toBeInstanceOf(OpenApiParseError);      expect(error).toHaveProperty("message", expect.stringMatching(/too large to parse/));    }),  );   it.effect("fetches a document with an in-range declared length", () =>    Effect.gen(function* () {      const specText = yield* fetchSpecText(specUrl).pipe(        Effect.provide(layerWithResponse(new Response("openapi: 3.0.0", { status: 200 }))),      );       expect(specText).toBe("openapi: 3.0.0");    }),  );});