diff --git a/test/all-ref-types.test.ts b/test/all-ref-types.test.ts new file mode 100644 index 0000000..8212299 --- /dev/null +++ b/test/all-ref-types.test.ts @@ -0,0 +1,384 @@ +import { describe, expect, it } from "bun:test"; +import { parsers, printers } from "../src/index.js"; + +/** + * Comprehensive tests for all OpenAPI object types that can be referenced via $ref + * This covers all versions: Swagger 2.0, OpenAPI 3.0.x, 3.1.x, and 3.2.0 + */ +describe("All $ref Reference Types", () => { + const printer = printers?.["openapi-ast"]; + const parser = parsers?.["openapi-parser"]; + + describe("Schema Object (all versions)", () => { + it("should format schema object with $ref at root correctly", () => { + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + type: "object", + description: "User schema", + title: "User", + properties: { + id: { type: "integer" }, + name: { type: "string" }, + }, + $ref: "#/components/schemas/User", + required: ["id", "name"], + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const titleIndex = resultString.indexOf("title:"); + expect(refIndex).toBeLessThan(titleIndex); + }); + }); + + describe("Parameter Object (all versions)", () => { + it("should format parameter object with $ref at root correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + name: "offset", + in: "query", + description: "Pagination offset", + schema: { type: "integer" }, + $ref: "#/components/parameters/Offset", + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const nameIndex = resultString.indexOf("name:"); + expect(refIndex).toBeLessThan(nameIndex); + }); + }); + + describe("Response Object (all versions)", () => { + it("should format response object with $ref at root correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + description: "Success response", + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + $ref: "#/components/responses/Success", + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const descriptionIndex = resultString.indexOf("description:"); + expect(refIndex).toBeLessThan(descriptionIndex); + }); + }); + + describe("Header Object (OpenAPI 3.0+)", () => { + it("should format header object with $ref at root correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + description: "Rate limit header", + required: true, + schema: { type: "integer" }, + $ref: "#/components/headers/RateLimit", + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const descriptionIndex = resultString.indexOf("description:"); + expect(refIndex).toBeLessThan(descriptionIndex); + }); + }); + + describe("Path Item Object (all versions)", () => { + it("should format path item object with $ref at root correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + summary: "User operations", + description: "Path for user-related operations", + get: { + summary: "Get user", + responses: { "200": { description: "OK" } }, + }, + $ref: "#/components/pathItems/UserPath", + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const summaryIndex = resultString.indexOf("summary:"); + expect(refIndex).toBeLessThan(summaryIndex); + }); + }); + + describe("Link Object (OpenAPI 3.0+)", () => { + it("should format link object with $ref at root correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + operationId: "getUser", + description: "Link to get user operation", + parameters: { userId: "$response.body#/id" }, + $ref: "#/components/links/UserLink", + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const operationIdIndex = resultString.indexOf("operationId:"); + expect(refIndex).toBeLessThan(operationIdIndex); + }); + }); + + describe("Request Body Object (OpenAPI 3.0+)", () => { + it("should format request body object with $ref at root correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + description: "User data", + required: true, + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + $ref: "#/components/requestBodies/UserRequestBody", + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const descriptionIndex = resultString.indexOf("description:"); + expect(refIndex).toBeLessThan(descriptionIndex); + }); + }); + + describe("Callback Object (OpenAPI 3.0+)", () => { + it("should format callback object correctly", () => { + // Callbacks are maps where values are Path Item Objects + // Each path item already supports $ref, so we test the structure + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + "{$request.body#/callbackUrl}": { + $ref: "#/components/pathItems/CallbackPath", + post: { + requestBody: { + description: "Callback payload", + }, + responses: { + "200": { description: "Success" }, + }, + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // The path item should have $ref first + const refIndex = resultString.indexOf("$ref:"); + const postIndex = resultString.indexOf("post:"); + expect(refIndex).toBeLessThan(postIndex); + }); + }); + + describe("Components Object (OpenAPI 3.0+) - sub-objects", () => { + it("should format components/schemas object correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + type: "object", + $ref: "#/components/schemas/User", + properties: { id: { type: "integer" } }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const typeIndex = resultString.indexOf("type:"); + expect(refIndex).toBeLessThan(typeIndex); + }); + }); + + describe("Definitions Object (Swagger 2.0)", () => { + it("should format definitions object correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + type: "object", + $ref: "#/definitions/User", + properties: { id: { type: "integer" } }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const typeIndex = resultString.indexOf("type:"); + expect(refIndex).toBeLessThan(typeIndex); + }); + }); + + describe("Parameters Definitions Object (Swagger 2.0)", () => { + it("should format parameters definition object correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + name: "limit", + in: "query", + description: "Result limit", + $ref: "#/parameters/Limit", + type: "integer", + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const nameIndex = resultString.indexOf("name:"); + expect(refIndex).toBeLessThan(nameIndex); + }); + }); + + describe("Responses Definitions Object (Swagger 2.0)", () => { + it("should format responses definition object correctly", () => { + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + description: "Error response", + $ref: "#/responses/Error", + schema: { type: "object" }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) throw new Error("Result is undefined"); + + const resultString = result.toString(); + + // $ref should be first + const refIndex = resultString.indexOf("$ref:"); + const descriptionIndex = resultString.indexOf("description:"); + expect(refIndex).toBeLessThan(descriptionIndex); + }); + }); +}); diff --git a/test/ref-ordering.test.ts b/test/ref-ordering.test.ts new file mode 100644 index 0000000..6556c03 --- /dev/null +++ b/test/ref-ordering.test.ts @@ -0,0 +1,623 @@ +import { describe, expect, it } from "bun:test"; +import { printers } from "../src/index.js"; + +describe("Reference ($ref) Ordering Tests", () => { + describe("Operation key ordering with references", () => { + it("should sort operation keys correctly with $ref parameters", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + paths: { + "/test": { + get: { + // Intentionally out of order to test sorting + description: "Get a list of Access Model Metadata Attributes", + operationId: "listAccessModelMetadataAttribute", + summary: "List access model metadata attributes", + tags: ["Access Model Metadata"], + security: [{ userAuth: ["idn:access-model-metadata:read"] }], + parameters: [ + { + name: "filters", + description: "Filter results", + in: "query", + required: false, + schema: { type: "string" }, + }, + { + $ref: "../../v3/parameters/offset.yaml", + }, + { + $ref: "../../v3/parameters/limit.yaml", + }, + { + name: "sorters", + description: "Sort results", + in: "query", + required: false, + schema: { type: "string", format: "comma-separated" }, + }, + ], + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "../schemas/gov-attributes/AttributeDTO.yaml", + }, + }, + }, + }, + }, + "400": { + $ref: "../../v3/responses/400.yaml", + }, + "401": { + $ref: "../../v3/responses/401.yaml", + }, + "500": { + $ref: "../../v3/responses/500.yaml", + }, + }, + "x-sailpoint-userLevels": ["ORG_ADMIN"], + }, + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // Check operation keys appear in correct order + const operationIdIndex = resultString.indexOf("operationId"); + const summaryIndex = resultString.indexOf("summary"); + const tagsIndex = resultString.indexOf("tags"); + const descriptionIndex = resultString.indexOf("description"); + const securityIndex = resultString.indexOf("security"); + const parametersIndex = resultString.indexOf("parameters"); + const responsesIndex = resultString.indexOf("responses"); + const xSailpointIndex = resultString.indexOf("x-sailpoint-userLevels"); + + // Verify operation key order: operationId -> summary -> tags -> description -> security -> parameters -> responses + expect(operationIdIndex).toBeLessThan(summaryIndex); + expect(summaryIndex).toBeLessThan(tagsIndex); + expect(tagsIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(parametersIndex); + expect(parametersIndex).toBeLessThan(responsesIndex); + + // Custom extensions should come after all standard keys + expect(responsesIndex).toBeLessThan(xSailpointIndex); + }); + + it("should sort parameters array with $ref items first", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + paths: { + "/test": { + get: { + operationId: "test", + responses: { "200": { description: "OK" } }, + parameters: [ + { + name: "filter", + in: "query", + schema: { type: "string" }, + }, + { + $ref: "../../v3/parameters/offset.yaml", + }, + { + name: "sort", + in: "query", + schema: { type: "string" }, + }, + { + $ref: "../../v3/parameters/limit.yaml", + }, + ], + }, + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // Find indices of parameter items in the output + // $ref parameters should come before named parameters + const parametersSection = resultString.substring(resultString.indexOf("parameters:")); + + // Check that $ref items appear before named parameters + const offsetRefIndex = parametersSection.indexOf("offset.yaml"); + const limitRefIndex = parametersSection.indexOf("limit.yaml"); + const filterIndex = parametersSection.indexOf("name: filter"); + const sortIndex = parametersSection.indexOf("name: sort"); + + // $ref items should come first in parameters array + expect(offsetRefIndex).toBeLessThan(filterIndex); + expect(limitRefIndex).toBeLessThan(filterIndex); + expect(limitRefIndex).toBeLessThan(sortIndex); + }); + + it("should sort responses with $ref items correctly", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + paths: { + "/test": { + get: { + operationId: "test", + responses: { + "500": { + $ref: "../../v3/responses/500.yaml", + }, + "200": { + description: "OK", + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + "400": { + $ref: "../../v3/responses/400.yaml", + }, + "401": { + $ref: "../../v3/responses/401.yaml", + }, + }, + }, + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // Response codes should be sorted numerically: 200, 400, 401, 500 + const responsesSection = resultString.substring(resultString.indexOf("responses:")); + + const code200Index = responsesSection.indexOf('"200":'); + const code400Index = responsesSection.indexOf('"400":'); + const code401Index = responsesSection.indexOf('"401":'); + const code500Index = responsesSection.indexOf('"500":'); + + expect(code200Index).toBeLessThan(code400Index); + expect(code400Index).toBeLessThan(code401Index); + expect(code401Index).toBeLessThan(code500Index); + }); + + it("should handle parameter objects with $ref at the top", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + components: { + parameters: { + OffsetParam: { + description: "Pagination offset", + in: "query", + name: "offset", + schema: { type: "integer" }, + $ref: "../../v3/parameters/offset.yaml", + }, + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // In a parameter object, if both $ref and other keys exist, $ref should come first + // But typically in OpenAPI, if $ref exists, other keys (except $ref) are ignored + // However, we should still verify the ordering + const offsetParamSection = resultString.substring(resultString.indexOf("OffsetParam:")); + + // $ref should be at the top of the parameter object + const refIndex = offsetParamSection.indexOf("$ref:"); + const nameIndex = offsetParamSection.indexOf("name:"); + const descriptionIndex = offsetParamSection.indexOf("description:"); + + // When $ref is present, it should be first (though other keys should typically be removed) + if (refIndex !== -1 && nameIndex !== -1) { + expect(refIndex).toBeLessThan(nameIndex); + expect(refIndex).toBeLessThan(descriptionIndex); + } + }); + + it("should handle response objects with $ref correctly", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + components: { + responses: { + ErrorResponse: { + description: "Error response", + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + $ref: "../../v3/responses/error.yaml", + }, + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // In a response object with $ref, $ref should be first + const errorResponseSection = resultString.substring(resultString.indexOf("ErrorResponse:")); + + const refIndex = errorResponseSection.indexOf("$ref:"); + const descriptionIndex = errorResponseSection.indexOf("description:"); + const contentIndex = errorResponseSection.indexOf("content:"); + + if (refIndex !== -1 && descriptionIndex !== -1) { + expect(refIndex).toBeLessThan(descriptionIndex); + expect(refIndex).toBeLessThan(contentIndex); + } + }); + + it("should handle complete operation with mixed inline and $ref parameters", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + paths: { + "/test": { + get: { + // All keys intentionally out of order + tags: ["Test"], + description: "Test endpoint", + summary: "Get test", + operationId: "getTest", + security: [{ apiKey: [] }], + parameters: [ + { + name: "custom", + in: "query", + schema: { type: "string" }, + }, + { + $ref: "#/components/parameters/Offset", + }, + { + name: "another", + in: "query", + schema: { type: "string" }, + }, + { + $ref: "#/components/parameters/Limit", + }, + { + $ref: "#/components/parameters/Count", + }, + ], + responses: { + "200": { + description: "Success", + }, + }, + "x-custom": "value", + }, + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // Verify operation key order + const operationIdIndex = resultString.indexOf("operationId"); + const summaryIndex = resultString.indexOf("summary"); + const tagsIndex = resultString.indexOf("tags"); + const descriptionIndex = resultString.indexOf("description"); + const securityIndex = resultString.indexOf("security"); + const parametersIndex = resultString.indexOf("parameters"); + const responsesIndex = resultString.indexOf("responses"); + const xCustomIndex = resultString.indexOf("x-custom"); + + expect(operationIdIndex).toBeLessThan(summaryIndex); + expect(summaryIndex).toBeLessThan(tagsIndex); + expect(tagsIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(parametersIndex); + expect(parametersIndex).toBeLessThan(responsesIndex); + expect(responsesIndex).toBeLessThan(xCustomIndex); + + // Verify $ref parameters come first in parameters array + const parametersSection = resultString.substring(resultString.indexOf("parameters:")); + const offsetRefIndex = parametersSection.indexOf("Offset"); + const limitRefIndex = parametersSection.indexOf("Limit"); + const countRefIndex = parametersSection.indexOf("Count"); + const customIndex = parametersSection.indexOf("name: custom"); + const anotherIndex = parametersSection.indexOf("name: another"); + + // All $ref items should come before inline parameters + expect(offsetRefIndex).toBeLessThan(customIndex); + expect(limitRefIndex).toBeLessThan(customIndex); + expect(countRefIndex).toBeLessThan(customIndex); + expect(offsetRefIndex).toBeLessThan(anotherIndex); + expect(limitRefIndex).toBeLessThan(anotherIndex); + expect(countRefIndex).toBeLessThan(anotherIndex); + }); + + it("should handle root-level operation file (file with HTTP method at root)", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + // This simulates a file that is referenced via $ref and contains just an operation + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + get: { + // Keys intentionally out of order + description: "Get a list of Access Model Metadata Attributes", + operationId: "listAccessModelMetadataAttribute", + summary: "List access model metadata attributes", + tags: ["Access Model Metadata"], + security: [{ userAuth: ["idn:access-model-metadata:read"] }], + parameters: [ + { + name: "filters", + description: "Filter results", + in: "query", + required: false, + schema: { type: "string" }, + }, + { + $ref: "../../v3/parameters/offset.yaml", + }, + { + $ref: "../../v3/parameters/limit.yaml", + }, + { + name: "sorters", + description: "Sort results", + in: "query", + required: false, + schema: { type: "string", format: "comma-separated" }, + }, + ], + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "../schemas/gov-attributes/AttributeDTO.yaml", + }, + }, + }, + }, + }, + "400": { + $ref: "../../v3/responses/400.yaml", + }, + "401": { + $ref: "../../v3/responses/401.yaml", + }, + }, + "x-sailpoint-userLevels": ["ORG_ADMIN"], + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // Find the operation keys within the get: block + const getBlockStart = resultString.indexOf("get:"); + const getBlock = resultString.substring(getBlockStart); + + // Check operation keys appear in correct order + const operationIdIndex = getBlock.indexOf("operationId"); + const summaryIndex = getBlock.indexOf("summary"); + const tagsIndex = getBlock.indexOf("tags"); + const descriptionIndex = getBlock.indexOf("description"); + const securityIndex = getBlock.indexOf("security"); + const parametersIndex = getBlock.indexOf("parameters"); + const responsesIndex = getBlock.indexOf("responses"); + const xSailpointIndex = getBlock.indexOf("x-sailpoint-userLevels"); + + // Verify operation key order: operationId -> summary -> tags -> description -> security -> parameters -> responses + expect(operationIdIndex).toBeLessThan(summaryIndex); + expect(summaryIndex).toBeLessThan(tagsIndex); + expect(tagsIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(parametersIndex); + expect(parametersIndex).toBeLessThan(responsesIndex); + + // Custom extensions should come after all standard keys + expect(responsesIndex).toBeLessThan(xSailpointIndex); + + // Verify $ref parameters come first in parameters array + const parametersSection = getBlock.substring(getBlock.indexOf("parameters:")); + const offsetRefIndex = parametersSection.indexOf("offset.yaml"); + const limitRefIndex = parametersSection.indexOf("limit.yaml"); + const filtersIndex = parametersSection.indexOf("name: filters"); + const sortersIndex = parametersSection.indexOf("name: sorters"); + + // $ref items should come before inline parameters + expect(offsetRefIndex).toBeLessThan(filtersIndex); + expect(limitRefIndex).toBeLessThan(filtersIndex); + expect(offsetRefIndex).toBeLessThan(sortersIndex); + expect(limitRefIndex).toBeLessThan(sortersIndex); + }); + + it("should handle direct operation object at root (no HTTP method wrapper)", () => { + const printer = printers?.["openapi-ast"]; + expect(printer).toBeDefined(); + + // This simulates a file that contains just the operation object directly + const testData = { + isOpenAPI: true, + format: "yaml", + content: { + // Direct operation object (no HTTP method wrapper) + operationId: "testOperation", + description: "Test operation", + summary: "Test summary", + tags: ["Test"], + security: [{ apiKey: [] }], + parameters: [ + { + $ref: "#/components/parameters/Offset", + }, + { + name: "custom", + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + "200": { + description: "Success", + }, + }, + }, + originalText: "", + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error("Result is undefined"); + } + + const resultString = result.toString(); + + // Check operation keys appear in correct order + const operationIdIndex = resultString.indexOf("operationId"); + const summaryIndex = resultString.indexOf("summary"); + const tagsIndex = resultString.indexOf("tags"); + const descriptionIndex = resultString.indexOf("description"); + const securityIndex = resultString.indexOf("security"); + const parametersIndex = resultString.indexOf("parameters"); + const responsesIndex = resultString.indexOf("responses"); + + // Verify operation key order + expect(operationIdIndex).toBeLessThan(summaryIndex); + expect(summaryIndex).toBeLessThan(tagsIndex); + expect(tagsIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(parametersIndex); + expect(parametersIndex).toBeLessThan(responsesIndex); + + // Verify $ref parameters come first + const parametersSection = resultString.substring(resultString.indexOf("parameters:")); + const offsetRefIndex = parametersSection.indexOf("Offset"); + const customIndex = parametersSection.indexOf("name: custom"); + + expect(offsetRefIndex).toBeLessThan(customIndex); + }); + }); +});