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); }); }); });