Fix OpenAPI importer issues found by spec review (#599)

This commit is contained in:
Gregory Schier
2026-08-20 07:16:44 -07:00
committed by GitHub
parent c4f96f3f11
commit 4eebd606ef
3 changed files with 617 additions and 152 deletions
@@ -163,18 +163,13 @@ Responses:
"model": "http_request",
"name": "Retrieve one version of a particular API",
"sortPriority": 5,
"url": "\${[baseUrl]}/specs/:provider/:api.json",
"url": "\${[baseUrl]}/specs/:provider/2.1.0.json",
"urlParameters": [
{
"enabled": true,
"name": ":provider",
"value": "apis.guru",
},
{
"enabled": true,
"name": ":api",
"value": "2.1.0",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
},
@@ -207,7 +202,7 @@ Responses:
"model": "http_request",
"name": "Retrieve one version of a particular API with a serviceName.",
"sortPriority": 6,
"url": "\${[baseUrl]}/specs/:provider/:service/:api.json",
"url": "\${[baseUrl]}/specs/:provider/:service/2.1.0.json",
"urlParameters": [
{
"enabled": true,
@@ -219,11 +214,6 @@ Responses:
"name": ":service",
"value": "graph",
},
{
"enabled": true,
"name": ":api",
"value": "2.1.0",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
},
@@ -256,14 +246,8 @@ Responses:
"model": "http_request",
"name": "List all APIs for a particular provider",
"sortPriority": 7,
"url": "\${[baseUrl]}/:provider.json",
"urlParameters": [
{
"enabled": true,
"name": ":provider",
"value": "apis.guru",
},
],
"url": "\${[baseUrl]}/apis.guru.json",
"urlParameters": [],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
},
{
@@ -631,7 +615,7 @@ Responses:
{
"enabled": true,
"name": ":anything",
"value": "",
"value": "anything",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -660,7 +644,7 @@ Responses:
{
"enabled": true,
"name": ":anything",
"value": "",
"value": "anything",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -689,7 +673,7 @@ Responses:
{
"enabled": true,
"name": ":anything",
"value": "",
"value": "anything",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -718,7 +702,7 @@ Responses:
{
"enabled": true,
"name": ":anything",
"value": "",
"value": "anything",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -747,7 +731,7 @@ Responses:
{
"enabled": true,
"name": ":anything",
"value": "",
"value": "anything",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -776,7 +760,7 @@ Responses:
{
"enabled": true,
"name": ":anything",
"value": "",
"value": "anything",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -836,12 +820,12 @@ Responses:
{
"enabled": true,
"name": ":user",
"value": "",
"value": "user",
},
{
"enabled": true,
"name": ":passwd",
"value": "",
"value": "passwd",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -860,13 +844,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",
@@ -1093,12 +1071,12 @@ Responses:
{
"enabled": true,
"name": ":name",
"value": "",
"value": "name",
},
{
"enabled": true,
"name": ":value",
"value": "",
"value": "value",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -1364,17 +1342,17 @@ Responses:
{
"enabled": true,
"name": ":qop",
"value": "",
"value": "qop",
},
{
"enabled": true,
"name": ":user",
"value": "",
"value": "user",
},
{
"enabled": true,
"name": ":passwd",
"value": "",
"value": "passwd",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -1407,17 +1385,17 @@ Responses:
{
"enabled": true,
"name": ":qop",
"value": "",
"value": "qop",
},
{
"enabled": true,
"name": ":user",
"value": "",
"value": "user",
},
{
"enabled": true,
"name": ":passwd",
"value": "",
"value": "passwd",
},
{
"enabled": true,
@@ -1457,17 +1435,17 @@ Responses:
{
"enabled": true,
"name": ":qop",
"value": "",
"value": "qop",
},
{
"enabled": true,
"name": ":user",
"value": "",
"value": "user",
},
{
"enabled": true,
"name": ":passwd",
"value": "",
"value": "passwd",
},
{
"enabled": true,
@@ -1587,7 +1565,7 @@ Responses:
{
"enabled": true,
"name": ":etag",
"value": "",
"value": "etag",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -1678,12 +1656,12 @@ Responses:
{
"enabled": true,
"name": ":user",
"value": "",
"value": "user",
},
{
"enabled": true,
"name": ":passwd",
"value": "",
"value": "passwd",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -2317,7 +2295,7 @@ Responses:
{
"enabled": true,
"name": ":codes",
"value": "",
"value": "codes",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -2350,7 +2328,7 @@ Responses:
{
"enabled": true,
"name": ":codes",
"value": "",
"value": "codes",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -2383,7 +2361,7 @@ Responses:
{
"enabled": true,
"name": ":codes",
"value": "",
"value": "codes",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -2416,7 +2394,7 @@ Responses:
{
"enabled": true,
"name": ":codes",
"value": "",
"value": "codes",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -2449,7 +2427,7 @@ Responses:
{
"enabled": true,
"name": ":codes",
"value": "",
"value": "codes",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -2482,7 +2460,7 @@ Responses:
{
"enabled": true,
"name": ":codes",
"value": "",
"value": "codes",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
@@ -2777,7 +2755,7 @@ exports[`importer-openapi > Snapshots real-world fixture xkcd.yaml 1`] = `
"variables": [
{
"name": "baseUrl",
"value": "http://xkcd.com/",
"value": "http://xkcd.com",
},
],
"workspaceId": "GENERATE_ID::WORKSPACE_0",
+323 -2
View File
@@ -787,7 +787,8 @@ describe("importer-openapi", () => {
expect(imported?.resources.httpRequests[0]).toEqual(
expect.objectContaining({
bodyType: "application/xml",
// Yaak's XML body type; the header keeps the spec's media type
bodyType: "text/xml",
headers: expect.arrayContaining([
{ enabled: true, name: "Content-Type", value: "application/xml" },
]),
@@ -840,7 +841,7 @@ describe("importer-openapi", () => {
expect.objectContaining({
name: "Server 1",
variables: [
{ name: "baseUrl", value: "https://example.com/" },
{ name: "baseUrl", value: "https://example.com" },
{ name: "auth_basic_auth_username", value: "" },
{ name: "auth_basic_auth_password", value: "" },
{ name: "auth_cookie_key_key", value: "" },
@@ -987,6 +988,326 @@ describe("importer-openapi", () => {
]);
});
test("Resolves references that point into arrays", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Array Ref Test", version: "1.0.0" },
paths: {
"/a": {
get: {
parameters: [
{ name: "limit", in: "query", required: true, schema: { example: "42" } },
],
responses: {},
},
},
"/b": {
get: { parameters: [{ $ref: "#/paths/~1a/get/parameters/0" }], responses: {} },
},
},
}),
);
expect(imported?.resources.httpRequests[1]?.urlParameters).toEqual([
{ enabled: true, name: "limit", value: "42" },
]);
});
test("Resolves example references and 3.1 example arrays", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.1.0",
info: { title: "Examples Test", version: "1.0.0" },
paths: {
"/a": {
post: {
parameters: [
{ name: "q", in: "query", schema: { type: "string", examples: ["hello"] } },
],
requestBody: {
content: {
"application/json": {
schema: { type: "object" },
examples: { main: { $ref: "#/components/examples/Main" } },
},
},
},
responses: {},
},
},
},
components: { examples: { Main: { value: { x: "from-example" } } } },
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify({ x: "from-example" }, null, 2),
});
expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([
{ enabled: false, name: "q", value: "hello" },
]);
});
test("Keeps template-looking braces in descriptions and examples", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Braces Test", version: "1.0.0" },
paths: {
"/a": {
post: {
description: "Use {{placeholders}} in the template",
requestBody: {
content: {
"application/json": { schema: { type: "string", example: "Hi {{name}}" } },
},
},
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.description).toContain("{{placeholders}}");
expect(imported?.resources.httpRequests[0]?.body).toEqual({ text: "Hi {{name}}" });
});
test("Ignores header parameters the spec reserves for other mechanisms", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Reserved Headers Test", version: "1.0.0" },
paths: {
"/a": {
post: {
parameters: [
{ name: "Content-Type", in: "header", schema: { example: "application/xml" } },
{ name: "Accept", in: "header", schema: { example: "text/html" } },
{ name: "Authorization", in: "header", schema: { example: "custom" } },
{ name: "X-Custom", in: "header", schema: { example: "kept" } },
],
requestBody: { content: { "application/json": { schema: { type: "object" } } } },
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.headers).toEqual([
{ enabled: false, name: "X-Custom", value: "kept" },
{ enabled: true, name: "Content-Type", value: "application/json" },
]);
});
test("Imports cookie parameters as a Cookie header", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Cookie Test", version: "1.0.0" },
paths: {
"/a": {
get: {
parameters: [
{ name: "session", in: "cookie", required: true, schema: { example: "abc" } },
{ name: "theme", in: "cookie", schema: { example: "dark" } },
],
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.headers).toEqual([
{ enabled: true, name: "Cookie", value: "session=abc; theme=dark" },
]);
});
test("Inlines path templates that Yaak placeholders cannot express", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Mid-Segment Test", version: "1.0.0" },
paths: {
"/report.{format}": {
get: {
parameters: [
{ name: "format", in: "path", required: true, schema: { example: "csv" } },
],
responses: {},
},
},
"/tasks/{id}:cancel": {
post: {
parameters: [{ name: "id", in: "path", required: true, schema: { example: "7" } }],
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests.map((r) => [r.url, r.urlParameters])).toEqual([
["${[baseUrl]}/report.csv", []],
["${[baseUrl]}/tasks/:id:cancel", [{ enabled: true, name: ":id", value: "7" }]],
]);
});
test("Enables path parameters even when required is omitted", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Sloppy Path Test", version: "1.0.0" },
paths: {
"/users/{userId}": {
get: {
parameters: [{ name: "userId", in: "path", schema: { type: "string" } }],
responses: {},
},
},
},
}),
);
// No example either, so the name stands in for an empty path segment
expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([
{ enabled: true, name: ":userId", value: "userId" },
]);
});
test("Coerces examples to the declared schema type", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Coercion Test", version: "1.0.0" },
paths: {
"/a": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
password: { type: "string", example: 12345 },
count: { type: "integer", example: "3" },
note: { example: 7 },
},
},
},
},
},
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify({ password: "12345", count: 3, note: 7 }, null, 2),
});
});
test("Merges allOf branches with sibling properties", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.1.0",
info: { title: "AllOf Test", version: "1.0.0" },
paths: {
"/a": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
allOf: [
{ type: "object", properties: { fromAllOf: { example: "a" } } },
],
properties: { sibling: { example: "b" } },
},
},
},
},
responses: {},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify({ fromAllOf: "a", sibling: "b" }, null, 2),
});
});
test("Accepts unquoted YAML version numbers", async () => {
const imported = await convertOpenApi(
["swagger: 2.0", "info:", " title: Unquoted Test", ' version: "1"', "host: example.com", "paths:", " /a:", " get:", " responses: {}"].join("\n"),
);
expect(imported?.resources.httpRequests[0]?.url).toBe("${[baseUrl]}/a");
// The numeric version must feed server detection too, or the selectable
// environment overrides baseUrl with an empty value
expect(imported?.resources.environments[1]).toEqual(
expect.objectContaining({
name: "Server 1",
variables: [{ name: "baseUrl", value: "https://example.com" }],
}),
);
});
test("Normalizes body types to Yaak's editors", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Body Type Test", version: "1.0.0" },
paths: {
"/vnd-json": {
post: {
requestBody: {
content: { "application/vnd.api+json": { schema: { type: "object" } } },
},
responses: {},
},
},
"/plain": {
post: {
requestBody: { content: { "text/plain": { schema: { type: "string" } } } },
responses: {},
},
},
},
}),
);
expect(
imported?.resources.httpRequests.map((r) => [r.bodyType, r.headers?.[0]?.value]),
).toEqual([
["application/json", "application/vnd.api+json"],
["other", "text/plain"],
]);
});
test("Trims trailing slashes from server URLs", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Trailing Slash Test", version: "1.0.0" },
servers: [{ url: "https://api.example.com/v1/" }],
paths: { "/pets": { get: { responses: {} } } },
}),
);
expect(imported?.resources.environments[1]?.variables).toEqual([
{ name: "baseUrl", value: "https://api.example.com/v1" },
]);
});
test("Reports references that point outside the document", async () => {
const imported = await convertOpenApi(
JSON.stringify({