Compare commits

..
3 changed files with 526 additions and 206 deletions
+273 -105
View File
@@ -20,7 +20,6 @@ type ImportResources = {
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
httpRequests: AtLeast<HttpRequest, "name" | "id" | "model" | "workspaceId">[];
};
type ServerOverrideVariable = { name: string; value: string };
const HTTP_METHODS = ["delete", "get", "head", "options", "patch", "post", "put", "query", "trace"];
const BODY_CONTENT_TYPE_PREFERENCE = [
@@ -63,7 +62,6 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
folders: [],
httpRequests: [],
};
const serverOverrides = new Map<string, ServerOverrideVariable>();
const baseUrl = importBaseUrl(spec);
// A local spec has no document URL against which OpenAPI's implicit "/"
// server can resolve. Keep the shared variable even when its initial value
@@ -122,7 +120,6 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
pathItem,
pathParameters,
requestBaseUrl,
serverOverrides,
spec,
workspaceId: workspace.id,
folderId,
@@ -132,36 +129,6 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
}
}
if (serverOverrides.size > 0) {
let environment = resources.environments[0];
if (environment == null) {
environment = {
model: "environment",
id: importState.generateId("environment"),
workspaceId: workspace.id,
name: "Global Variables",
variables: [],
parentModel: "workspace",
parentId: null,
sortPriority: importState.nextSortPriority(),
};
resources.environments.push(environment);
}
environment.variables.push(...serverOverrides.values());
}
resources.environments.push(
...importServerEnvironments(spec).map(({ name, url }) => ({
model: "environment" as const,
id: importState.generateId("environment"),
workspaceId: workspace.id,
name,
variables: [{ name: "baseUrl", value: url }],
parentModel: "environment",
parentId: null,
sortPriority: importState.nextSortPriority(),
})),
);
if (resources.httpRequests.length === 0) return undefined;
disambiguateNames(resources.httpRequests, routeLabels);
@@ -230,7 +197,6 @@ function importOperation({
pathItem,
pathParameters,
requestBaseUrl,
serverOverrides,
spec,
workspaceId,
folderId,
@@ -242,7 +208,6 @@ function importOperation({
pathItem: UnknownRecord;
pathParameters: unknown[];
requestBaseUrl: string;
serverOverrides: Map<string, ServerOverrideVariable>;
spec: UnknownRecord;
workspaceId: string;
folderId: string | null;
@@ -278,10 +243,7 @@ function importOperation({
name: importOperationName(operation, method, path),
description,
method: method.toUpperCase(),
url: buildOperationUrl(
operationBaseUrl({ operation, pathItem, requestBaseUrl, serverOverrides }),
path,
),
url: buildOperationUrl(operationBaseUrl({ operation, pathItem, requestBaseUrl }), path),
urlParameters,
headers,
body: body.body,
@@ -336,26 +298,18 @@ function operationBaseUrl({
operation,
pathItem,
requestBaseUrl,
serverOverrides,
}: {
operation: UnknownRecord;
pathItem: UnknownRecord;
requestBaseUrl: string;
serverOverrides: Map<string, ServerOverrideVariable>;
}): string {
for (const servers of [operation.servers, pathItem.servers]) {
const override = toArray(servers)
.map((s) => interpolateServerUrl(toRecord(s)))
.find((url) => url.length > 0);
if (override != null) {
let variable = serverOverrides.get(override);
if (variable == null) {
const suffix = serverOverrides.size === 0 ? "" : String(serverOverrides.size + 1);
variable = { name: `serverUrl${suffix}`, value: override };
serverOverrides.set(override, variable);
}
return `\${[${variable.name}]}`;
}
// Overrides are inlined rather than shared, since only the spec-level base
// URL becomes the baseUrl variable
if (override != null) return override;
}
return requestBaseUrl;
}
@@ -608,24 +562,6 @@ function importBaseUrl(spec: UnknownRecord): string {
return joinUrlParts(`${scheme}://${host}`, stringAt(spec, "basePath") ?? "");
}
function importServerEnvironments(spec: UnknownRecord): { name: string; url: string }[] {
const servers = toArray(spec.servers)
.map(toRecord)
.map((server, index) => ({
name: stringAt(server, "description")?.trim() || `Server ${index + 1}`,
url: interpolateServerUrl(server),
}))
.filter(({ url }) => url.length > 0);
if (servers.length < 2) return [];
const nameCounts = new Map<string, number>();
return servers.map((server) => {
const count = (nameCounts.get(server.name) ?? 0) + 1;
nameCounts.set(server.name, count);
return { ...server, name: count === 1 ? server.name : `${server.name} ${count}` };
});
}
function interpolateServerUrl(server: UnknownRecord): string {
let url = stringAt(server, "url") ?? "";
for (const [name, variable] of Object.entries(toRecord(server.variables))) {
@@ -662,17 +598,70 @@ function importUrlParameters({
.map((p) => importState.resolve(p))
.filter(isRecord)
.filter((p) => stringAt(p, "in") === "query" || stringAt(p, "in") === "path")
.map((p) => ({
enabled: p.required === true,
name:
stringAt(p, "in") === "path"
? `:${stringAt(p, "name") ?? ""}`
: (stringAt(p, "name") ?? ""),
value: parameterExample(p, importState),
}))
.flatMap((p) => serializeUrlParameter(p, importState))
.filter(({ name }) => name.length > 0);
}
function serializeUrlParameter(
parameter: UnknownRecord,
importState: ImportState,
): HttpUrlParameter[] {
const name = stringAt(parameter, "name") ?? "";
const location = stringAt(parameter, "in");
const enabled = parameter.required === true;
const value = parameterExampleValue(parameter, importState);
if (isRecord(parameter.content)) {
return [
{
enabled,
name: location === "path" ? `:${name}` : name,
value: serializeContentParameter(parameter, importState),
},
];
}
if (location === "path") {
return [{ enabled, name: `:${name}`, value: serializePathParameter(name, value, parameter) }];
}
if (isRecord(value)) {
const entries = Object.entries(value);
const style = stringAt(parameter, "style") ?? "form";
const explode = parameter.explode !== false;
if (style === "deepObject") {
return entries.map(([key, entryValue]) => ({
enabled,
name: `${name}[${key}]`,
value: stringifyExampleValue(entryValue),
}));
}
if (style === "form" && explode) {
return entries.map(([key, entryValue]) => ({
enabled,
name: key,
value: stringifyExampleValue(entryValue),
}));
}
const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
return [{ enabled, name, value: entries.flat().map(stringifyExampleValue).join(separator) }];
}
if (Array.isArray(value)) {
const style = stringAt(parameter, "style") ?? "form";
const explode = parameter.explode !== false;
if (style === "form" && explode) {
return value.map((entryValue) => ({
enabled,
name,
value: stringifyExampleValue(entryValue),
}));
}
const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
return [{ enabled, name, value: value.map(stringifyExampleValue).join(separator) }];
}
return [{ enabled, name, value: stringifyExampleValue(value) }];
}
function importHeaderParameters({
importState,
parameters,
@@ -684,18 +673,112 @@ function importHeaderParameters({
.map((p) => importState.resolve(p))
.filter(isRecord)
.filter((p) => stringAt(p, "in") === "header")
.filter(
(p) =>
!["accept", "authorization", "content-type"].includes(
(stringAt(p, "name") ?? "").toLowerCase(),
),
)
.map((p) => ({
enabled: p.required === true,
name: stringAt(p, "name") ?? "",
value: parameterExample(p, importState),
value: serializeParameterValue(p, importState),
}))
.filter(({ name }) => name.length > 0)
.concat(importCookieHeader(parameters, importState));
}
function importCookieHeader(parameters: unknown[], importState: ImportState): HttpRequestHeader[] {
const cookies = parameters
.map((p) => importState.resolve(p))
.filter(isRecord)
.filter((p) => stringAt(p, "in") === "cookie")
.map((p) => ({
enabled: p.required === true,
name: stringAt(p, "name") ?? "",
value: serializeParameterValue(p, importState),
}))
.filter(({ name }) => name.length > 0);
if (cookies.length === 0) return [];
return [
{
enabled: cookies.some(({ enabled }) => enabled),
name: "Cookie",
value: cookies.map(({ name, value }) => `${name}=${value}`).join("; "),
},
];
}
function serializeParameterValue(parameter: UnknownRecord, importState: ImportState): string {
if (isRecord(parameter.content)) return serializeContentParameter(parameter, importState);
return serializeSimpleParameter(parameterExampleValue(parameter, importState), parameter);
}
function serializeContentParameter(parameter: UnknownRecord, importState: ImportState): string {
const [contentType, rawMediaType] = Object.entries(toRecord(parameter.content))[0] ?? [];
const value = mediaTypeExample(toRecord(rawMediaType), importState);
return contentType?.toLowerCase().includes("json")
? (JSON.stringify(value) ?? "")
: stringifyExampleValue(value);
}
function serializePathParameter(name: string, value: unknown, parameter: UnknownRecord): string {
const style = stringAt(parameter, "style") ?? "simple";
const explode = parameter.explode === true;
const values = Array.isArray(value)
? value.map(stringifyExampleValue)
: isRecord(value)
? Object.entries(value).flatMap(([key, entryValue]) => [
key,
stringifyExampleValue(entryValue),
])
: [stringifyExampleValue(value)];
if (style === "label") {
if (explode && isRecord(value)) {
return `.${Object.entries(value)
.map(([key, entryValue]) => `${key}=${stringifyExampleValue(entryValue)}`)
.join(".")}`;
}
return `.${values.join(explode ? "." : ",")}`;
}
if (style === "matrix") {
if (explode && Array.isArray(value)) {
return value.map((entryValue) => `;${name}=${stringifyExampleValue(entryValue)}`).join("");
}
if (explode && isRecord(value)) {
return Object.entries(value)
.map(([key, entryValue]) => `;${key}=${stringifyExampleValue(entryValue)}`)
.join("");
}
return `;${name}=${values.join(",")}`;
}
return serializeSimpleParameter(value, parameter);
}
function serializeSimpleParameter(value: unknown, parameter: UnknownRecord): string {
if (Array.isArray(value)) return value.map(stringifyExampleValue).join(",");
if (isRecord(value)) {
const entries = Object.entries(value);
return parameter.explode === true
? entries.map(([key, entryValue]) => `${key}=${stringifyExampleValue(entryValue)}`).join(",")
: entries.flat().map(stringifyExampleValue).join(",");
}
return stringifyExampleValue(value);
}
function parameterExample(parameter: UnknownRecord, importState: ImportState): string {
return stringifyExampleValue(parameterExampleValue(parameter, importState));
}
function parameterExampleValue(parameter: UnknownRecord, importState: ImportState): unknown {
const directExample = firstPresent(parameter.example, firstExampleValue(parameter.examples));
if (directExample != null) return stringifyExampleValue(directExample);
return stringifyExampleValue(schemaToExample(importState.resolve(parameter.schema), importState));
if (directExample != null) return directExample;
if (isRecord(parameter.content)) {
const mediaType = toRecord(Object.values(parameter.content)[0]);
return mediaTypeExample(mediaType, importState);
}
return schemaToExample(importState.resolve(parameter.schema), importState);
}
function importBody({
@@ -726,14 +809,13 @@ function importBody({
(c): c is string => typeof c === "string",
);
const bodyType = contentType ?? "application/json";
const schema = importState.resolve(bodyParameter.schema);
const example = schemaToExample(schema, importState);
const isBinary = stringAt(schema, "format") === "binary";
return {
headers: [{ enabled: true, name: "Content-Type", value: bodyType }],
bodyType,
body: {
text: formatBodyText(
schemaToExample(importState.resolve(bodyParameter.schema), importState),
),
},
bodyType: isBinary ? "binary" : bodyType,
body: isBinary ? {} : { text: formatMediaTypeBody(bodyType, example, schema, importState) },
};
}
@@ -751,11 +833,15 @@ function importBody({
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
bodyType: contentType,
body: {
form: formParameters.map((p) => ({
enabled: p.required === true,
name: stringAt(p, "name") ?? "",
value: parameterExample(p, importState),
})),
form: formParameters.map((p) => {
const base = {
enabled: p.required === true,
name: stringAt(p, "name") ?? "",
};
return stringAt(p, "type") === "file"
? { ...base, file: "" }
: { ...base, value: parameterExample(p, importState) };
}),
},
};
}
@@ -778,45 +864,125 @@ function importBodyFromContent(importState: ImportState, content: UnknownRecord)
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
bodyType: contentType,
body: {
form: schemaToFormParameters(importState.resolve(mediaType.schema), importState),
form: schemaToFormParameters(
importState.resolve(mediaType.schema),
importState,
isRecord(example) ? example : undefined,
),
},
};
}
const schema = importState.resolve(mediaType.schema);
const isBinary =
contentType === "application/octet-stream" || stringAt(schema, "format") === "binary";
return {
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
bodyType: contentType === "application/octet-stream" ? "binary" : contentType,
body: contentType === "application/octet-stream" ? {} : { text: formatBodyText(example) },
bodyType: isBinary ? "binary" : contentType,
body: isBinary ? {} : { text: formatMediaTypeBody(contentType, example, schema, importState) },
};
}
function chooseContentType(contentTypes: string[]): string | null {
const jsonType = contentTypes.find((contentType) => {
const normalized = contentType.toLowerCase().split(";", 1)[0];
return normalized?.endsWith("+json") === true;
});
for (const preference of BODY_CONTENT_TYPE_PREFERENCE) {
const exact = contentTypes.find((c) => c.toLowerCase() === preference);
if (exact != null) return exact;
if (preference === "application/json" && jsonType != null) return jsonType;
}
return contentTypes[0] ?? null;
}
function formatMediaTypeBody(
contentType: string,
example: unknown,
schema: unknown,
importState: ImportState,
): string {
const normalized = contentType.toLowerCase().split(";", 1)[0] ?? "";
if (normalized === "application/json" || normalized.endsWith("+json")) {
return JSON.stringify(example, null, 2) ?? "";
}
if (
normalized === "application/xml" ||
normalized === "text/xml" ||
normalized.endsWith("+xml")
) {
return typeof example === "string"
? example
: valueToXml(example, schema, importState, stringAt(toRecord(schema).xml, "name") ?? "root");
}
return formatBodyText(example);
}
function valueToXml(
value: unknown,
schema: unknown,
importState: ImportState,
elementName: string,
): string {
const resolvedSchema = toRecord(importState.resolve(schema));
if (Array.isArray(value)) {
const itemSchema = importState.resolve(resolvedSchema.items);
const itemName = stringAt(toRecord(itemSchema).xml, "name") ?? "item";
const items = value.map((item) => valueToXml(item, itemSchema, importState, itemName)).join("");
return `<${elementName}>${items}</${elementName}>`;
}
if (isRecord(value)) {
const properties = toRecord(resolvedSchema.properties);
const attributes: string[] = [];
const children: string[] = [];
for (const [name, propertyValue] of Object.entries(value)) {
const propertySchema = toRecord(importState.resolve(properties[name]));
const xml = toRecord(propertySchema.xml);
const xmlName = stringAt(xml, "name") ?? name;
if (xml.attribute === true) {
attributes.push(`${xmlName}="${escapeXml(stringifyExampleValue(propertyValue))}"`);
} else {
children.push(valueToXml(propertyValue, propertySchema, importState, xmlName));
}
}
const attributeText = attributes.length > 0 ? ` ${attributes.join(" ")}` : "";
return `<${elementName}${attributeText}>${children.join("")}</${elementName}>`;
}
return `<${elementName}>${escapeXml(stringifyExampleValue(value))}</${elementName}>`;
}
function escapeXml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;");
}
function mediaTypeExample(mediaType: UnknownRecord, importState: ImportState): unknown {
const directExample = firstPresent(mediaType.example, firstExampleValue(mediaType.examples));
if (directExample != null) return directExample;
return schemaToExample(importState.resolve(mediaType.schema), importState);
}
function schemaToFormParameters(schema: unknown, importState: ImportState) {
function schemaToFormParameters(
schema: unknown,
importState: ImportState,
example?: UnknownRecord,
) {
const resolvedSchema = toRecord(importState.resolve(schema));
const required = toArray(resolvedSchema.required).filter(
(name): name is string => typeof name === "string",
);
const properties = Object.entries(toRecord(resolvedSchema.properties)).slice(
0,
MAX_EXAMPLE_PROPERTIES,
);
const properties = Object.entries(toRecord(resolvedSchema.properties))
.filter(([, property]) => toRecord(importState.resolve(property)).readOnly !== true)
.slice(0, MAX_EXAMPLE_PROPERTIES);
return properties.map(([name, property]) => {
const resolvedProperty = toRecord(importState.resolve(property));
const example = schemaToExample(resolvedProperty, importState);
const propertyExample = example?.[name] ?? schemaToExample(resolvedProperty, importState);
const base = {
enabled: required.includes(name),
name,
@@ -824,7 +990,7 @@ function schemaToFormParameters(schema: unknown, importState: ImportState) {
if (stringAt(resolvedProperty, "format") === "binary") {
return { ...base, file: "" };
}
return { ...base, value: stringifyExampleValue(example) };
return { ...base, value: stringifyExampleValue(propertyExample) };
});
}
@@ -871,11 +1037,13 @@ function schemaToExample(
const required = toArray(resolved.required).filter(
(name): name is string => typeof name === "string",
);
const properties = Object.entries(toRecord(resolved.properties)).sort(([a], [b]) => {
const aRequired = required.includes(a);
const bRequired = required.includes(b);
return aRequired === bRequired ? 0 : aRequired ? -1 : 1;
});
const properties = Object.entries(toRecord(resolved.properties))
.filter(([, property]) => toRecord(importState.resolve(property)).readOnly !== true)
.sort(([a], [b]) => {
const aRequired = required.includes(a);
const bRequired = required.includes(b);
return aRequired === bRequired ? 0 : aRequired ? -1 : 1;
});
return Object.fromEntries(
properties
@@ -840,13 +840,7 @@ Responses:
- 200: Sucessful authentication.
- 401: Unsuccessful authentication.",
"folderId": "GENERATE_ID::FOLDER_1",
"headers": [
{
"enabled": false,
"name": "Authorization",
"value": "",
},
],
"headers": [],
"id": "GENERATE_ID::HTTP_REQUEST_15",
"method": "GET",
"model": "http_request",
@@ -2620,36 +2614,6 @@ exports[`importer-openapi > Snapshots real-world fixture nasa-apod.yaml 1`] = `
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
},
{
"id": "GENERATE_ID::ENVIRONMENT_1",
"model": "environment",
"name": "Server 1",
"parentId": null,
"parentModel": "environment",
"sortPriority": 3,
"variables": [
{
"name": "baseUrl",
"value": "https://api.nasa.gov/planetary",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
},
{
"id": "GENERATE_ID::ENVIRONMENT_2",
"model": "environment",
"name": "Server 2",
"parentId": null,
"parentModel": "environment",
"sortPriority": 4,
"variables": [
{
"name": "baseUrl",
"value": "http://api.nasa.gov/planetary",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
},
],
"folders": [
{
+252 -64
View File
@@ -301,70 +301,8 @@ describe("importer-openapi", () => {
expect(imported?.resources.httpRequests.map((r) => r.url)).toEqual([
"${[baseUrl]}/root",
"${[serverUrl]}/path-level",
"${[serverUrl2]}/operation-level",
]);
expect(imported?.resources.environments[0]?.variables).toEqual([
{ name: "baseUrl", value: "https://root.example.com" },
{ name: "serverUrl", value: "https://path.example.com" },
{ name: "serverUrl2", value: "https://operation.example.com" },
]);
});
test("Creates selectable environments for multiple OpenAPI servers", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.4",
info: { title: "Server Environments Test", version: "1.0.0" },
servers: [
{ url: "https://api.example.com/v1", description: "Production" },
{ url: "https://sandbox.example.com/v1", description: "Sandbox" },
],
paths: { "/items": { get: { responses: {} } } },
}),
);
expect(imported?.resources.environments).toEqual([
expect.objectContaining({
name: "Global Variables",
variables: [{ name: "baseUrl", value: "https://api.example.com/v1" }],
}),
expect.objectContaining({
name: "Production",
parentModel: "environment",
variables: [{ name: "baseUrl", value: "https://api.example.com/v1" }],
}),
expect.objectContaining({
name: "Sandbox",
parentModel: "environment",
variables: [{ name: "baseUrl", value: "https://sandbox.example.com/v1" }],
}),
]);
});
test("Creates variables for path servers without a top-level server", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.4",
info: { title: "Path Server Test", version: "1.0.0" },
paths: {
"/items": {
servers: [{ url: "https://path.example.com" }],
get: { responses: {} },
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.url).toBe("${[serverUrl]}/items");
expect(imported?.resources.environments).toEqual([
expect.objectContaining({
name: "Global Variables",
variables: [
{ name: "baseUrl", value: "" },
{ name: "serverUrl", value: "https://path.example.com" },
],
}),
"https://path.example.com/path-level",
"https://operation.example.com/operation-level",
]);
});
@@ -586,6 +524,126 @@ describe("importer-openapi", () => {
]);
});
test("Imports cookie and content-based parameters", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.4",
info: { title: "Parameter Test", version: "1.0.0" },
paths: {
"/items": {
get: {
parameters: [
{
name: "session",
in: "cookie",
required: true,
schema: { type: "string", example: "abc" },
},
{
name: "X-Filter",
in: "header",
required: true,
content: { "text/plain": { example: "active" } },
},
],
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.headers).toEqual([
{ enabled: true, name: "X-Filter", value: "active" },
{ enabled: true, name: "Cookie", value: "session=abc" },
]);
});
test("Serializes structured query parameters according to style and explode", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.4",
info: { title: "Serialization Test", version: "1.0.0" },
paths: {
"/items": {
get: {
parameters: [
{
name: "filter",
in: "query",
required: true,
style: "deepObject",
explode: true,
schema: {
type: "object",
properties: {
role: { example: "admin" },
active: { example: true },
},
},
},
{
name: "tags",
in: "query",
style: "form",
explode: true,
schema: { type: "array", example: ["one", "two"] },
},
],
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([
{ enabled: true, name: "filter[role]", value: "admin" },
{ enabled: true, name: "filter[active]", value: "true" },
{ enabled: false, name: "tags", value: "one" },
{ enabled: false, name: "tags", value: "two" },
]);
});
test("Serializes label and matrix path parameters", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.4",
info: { title: "Path Serialization Test", version: "1.0.0" },
paths: {
"/labels/{labels}/matrix/{coordinates}": {
get: {
parameters: [
{
name: "labels",
in: "path",
required: true,
style: "label",
explode: true,
schema: { type: "array", example: ["one", "two"] },
},
{
name: "coordinates",
in: "path",
required: true,
style: "matrix",
explode: true,
schema: { type: "object", example: { x: 1, y: 2 } },
},
],
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([
{ enabled: true, name: ":labels", value: ".one.two" },
{ enabled: true, name: ":coordinates", value: ";x=1;y=2" },
]);
});
test("Prefers operation-level consumes for Swagger bodies", async () => {
const imported = await convertOpenApi(
JSON.stringify({
@@ -615,6 +673,136 @@ describe("importer-openapi", () => {
);
});
test("Serializes request examples according to their media type", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.4",
info: { title: "Media Type Test", version: "1.0.0" },
paths: {
"/xml": {
post: {
requestBody: {
content: {
"application/xml": {
schema: {
type: "object",
xml: { name: "user" },
properties: { name: { type: "string", example: "Ada" } },
},
},
},
},
responses: {},
},
},
"/json-string": {
post: {
requestBody: {
content: { "application/json": { schema: { type: "string", example: "hello" } } },
},
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: "<user><name>Ada</name></user>",
});
expect(imported?.resources.httpRequests[1]?.body).toEqual({ text: '"hello"' });
});
test("Omits read-only properties from generated request bodies", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.4",
info: { title: "Read Only Test", version: "1.0.0" },
paths: {
"/users": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: { type: "string", readOnly: true, example: "server-id" },
name: { type: "string", example: "Ada" },
},
},
},
},
},
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify({ name: "Ada" }, null, 2),
});
});
test("Imports Swagger 2 file parameters as file form entries", async () => {
const imported = await convertOpenApi(
JSON.stringify({
swagger: "2.0",
info: { title: "File Upload Test", version: "1.0.0" },
host: "example.com",
consumes: ["multipart/form-data"],
paths: {
"/upload": {
post: {
parameters: [{ name: "upload", in: "formData", required: true, type: "file" }],
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
form: [{ enabled: true, name: "upload", file: "" }],
});
});
test("Serializes Swagger 2 XML request bodies as XML", async () => {
const imported = await convertOpenApi(
JSON.stringify({
swagger: "2.0",
info: { title: "Swagger XML Test", version: "1.0.0" },
host: "example.com",
consumes: ["application/xml"],
paths: {
"/users": {
post: {
parameters: [
{
name: "user",
in: "body",
required: true,
schema: {
type: "object",
xml: { name: "user" },
properties: { name: { type: "string", example: "Ada" } },
},
},
],
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: "<user><name>Ada</name></user>",
});
});
test("Imports Swagger 2 basic auth and cookie API keys", async () => {
const imported = await convertOpenApi(
JSON.stringify({