253 lines · 9.5 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Option } from "effect";import { FetchHttpClient } from "effect/unstable/http"; import { extract } from "./extract";import { resolveServerUrl } from "./openapi-utils";import { parse } from "./parse";import { previewSpec as previewSpecRaw } from "./preview";import { ServerVariable } from "./types"; const previewSpec = (input: string) =>  previewSpecRaw(input).pipe(Effect.provide(FetchHttpClient.layer)); const serverVariable = (defaultValue: string): ServerVariable =>  ServerVariable.make({ default: defaultValue, enum: Option.none(), description: Option.none() }); // ---------------------------------------------------------------------------// resolveServerUrl — invoke-time template resolution// --------------------------------------------------------------------------- describe("resolveServerUrl", () => {  const vars = {    tenant: serverVariable("default-tenant"),    region: serverVariable("us-east-1"),  };   it("fills placeholders from variable defaults when no override is given", () => {    expect(resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, {})).toBe(      "https://default-tenant.us-east-1.api.example.com",    );  });   it("prefers connection overrides over defaults", () => {    expect(      resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, {        tenant: "acme",        region: "eu-west-1",      }),    ).toBe("https://acme.eu-west-1.api.example.com");  });   it("ignores empty overrides and keeps the default", () => {    expect(      resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, { tenant: "" }),    ).toBe("https://default-tenant.us-east-1.api.example.com");  });   it("returns a URL with no placeholders unchanged", () => {    expect(resolveServerUrl("https://api.example.com", undefined, { tenant: "acme" })).toBe(      "https://api.example.com",    );  });}); // ---------------------------------------------------------------------------// Operation applicable servers — operation/path override else document servers// --------------------------------------------------------------------------- // Raw JSON so the path-level `servers` override is easy to express.const specWithPathOverride = {  openapi: "3.0.0",  info: { title: "Example", version: "1.0" },  servers: [{ url: "https://api.example.com" }],  paths: {    "/items": {      get: { operationId: "listItems", responses: { "200": { description: "ok" } } },    },    "/query": {      servers: [        {          url: "https://{tenant}.{region}.api.example.com",          variables: { tenant: { default: "default-tenant" }, region: { default: "us-east-1" } },        },      ],      post: { operationId: "runQuery", responses: { "200": { description: "ok" } } },    },  },}; describe("extract — operation applicable servers", () => {  it.effect("inherits the document servers when there is no override", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(specWithPathOverride));      const result = yield* extract(doc);       const listItems = result.operations.find((op) => op.operationId === "listItems")!;      expect(listItems.servers.map((s) => s.url)).toEqual(["https://api.example.com"]);    }),  );   it.effect("carries a path-level override's servers as templates with variables", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(specWithPathOverride));      const result = yield* extract(doc);       const runQuery = result.operations.find((op) => op.operationId === "runQuery")!;      expect(runQuery.servers.map((s) => s.url)).toEqual([        "https://{tenant}.{region}.api.example.com",      ]);      const vars = Option.getOrThrow(runQuery.servers[0]!.variables);      expect(vars.tenant?.default).toBe("default-tenant");      expect(vars.region?.default).toBe("us-east-1");       // The host resolves per call: defaults, or call-supplied overrides.      const server = runQuery.servers[0]!;      expect(resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {})).toBe(        "https://default-tenant.us-east-1.api.example.com",      );      expect(        resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {          tenant: "acme",          region: "eu-west-1",        }),      ).toBe("https://acme.eu-west-1.api.example.com");    }),  );}); // ---------------------------------------------------------------------------// Preview surfaces each top-level server with its own variables// --------------------------------------------------------------------------- describe("previewSpec — server variables", () => {  it.effect("carries variables per top-level server, excluding operation overrides", () =>    Effect.gen(function* () {      const spec = {        openapi: "3.0.0",        info: { title: "Example", version: "1.0" },        servers: [          { url: "https://api.example.com", description: "Control plane" },          {            url: "https://{branch}.{region}.example.com",            variables: { branch: { default: "main" }, region: { default: "us-east-1" } },          },        ],        paths: {          "/query": {            servers: [              {                url: "https://{tenant}.gw.example.com",                variables: { tenant: { default: "default-tenant" } },              },            ],            post: { operationId: "runQuery", responses: { "200": { description: "ok" } } },          },        },      };      // @effect-diagnostics-next-line preferSchemaOverJson:off      const preview = yield* previewSpec(JSON.stringify(spec));       // The plain control-plane server has no variables.      const control = preview.servers.find((s) => s.url === "https://api.example.com")!;      expect(Option.getOrNull(control.variables)).toBeNull();       // The gateway server carries only its own branch/region.      const gateway = preview.servers.find((s) => s.url.startsWith("https://{branch}"))!;      expect(Object.keys(Option.getOrThrow(gateway.variables))).toEqual(["branch", "region"]);       // The operation override's `tenant` never appears in the top-level servers.      const names = preview.servers.flatMap((s) =>        Object.keys(Option.getOrElse(s.variables, () => ({}))),      );      expect(names).not.toContain("tenant");    }),  );}); // ---------------------------------------------------------------------------// The `server` input property exposes per-call host selection + variables// --------------------------------------------------------------------------- type ServerInputSchema = {  readonly required?: readonly string[];  readonly properties: {    readonly server?: {      readonly properties: {        readonly url?: { readonly enum: readonly string[]; readonly default: string };        readonly variables?: {          readonly properties: Record<            string,            { readonly default?: string; readonly enum?: readonly string[] }          >;        };      };    };  };}; describe("buildInputSchema — server property", () => {  it.effect("exposes a server picker and variables for multiple/templated servers", () =>    Effect.gen(function* () {      const spec = {        openapi: "3.0.0",        info: { title: "Example", version: "1.0" },        servers: [          { url: "https://api.example.com" },          {            url: "https://{branch}.{region}.example.com",            variables: {              branch: { default: "main" },              region: { default: "us", enum: ["us", "eu"] },            },          },        ],        paths: {          "/items": {            get: { operationId: "listItems", responses: { "200": { description: "ok" } } },          },        },      };      // @effect-diagnostics-next-line preferSchemaOverJson:off      const result = yield* extract(yield* parse(JSON.stringify(spec)));      const op = result.operations.find((o) => o.operationId === "listItems")!;      const schema = Option.getOrThrow(op.inputSchema) as ServerInputSchema;       expect(schema.properties.server?.properties.url?.enum).toEqual([        "https://api.example.com",        "https://{branch}.{region}.example.com",      ]);      const vars = schema.properties.server?.properties.variables?.properties;      expect(vars?.branch?.default).toBe("main");      expect(vars?.region?.enum).toEqual(["us", "eu"]);      // The host stays optional — defaults apply when the call omits `server`.      expect(schema.required ?? []).not.toContain("server");    }),  );   it.effect("omits the server property for a single concrete server", () =>    Effect.gen(function* () {      const spec = {        openapi: "3.0.0",        info: { title: "Example", version: "1.0" },        servers: [{ url: "https://api.example.com" }],        paths: {          "/items/{id}": {            get: {              operationId: "getItem",              parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],              responses: { "200": { description: "ok" } },            },          },        },      };      // @effect-diagnostics-next-line preferSchemaOverJson:off      const result = yield* extract(yield* parse(JSON.stringify(spec)));      const op = result.operations.find((o) => o.operationId === "getItem")!;      const schema = Option.getOrThrow(op.inputSchema) as ServerInputSchema;      expect(schema.properties.server).toBeUndefined();    }),  );});