Enhance OpenAPI file validation to reject generic content in component directories and update related tests for improved accuracy

This commit is contained in:
Luke Hagar
2025-11-10 17:25:28 +00:00
parent d972391e31
commit 8979826501
4 changed files with 176 additions and 10 deletions

View File

@@ -136,7 +136,7 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
}
// Check file path patterns for common OpenAPI file structures
// Only accept files in OpenAPI-related directories
// Only accept files in OpenAPI-related directories, but validate content first
if (filePath) {
const path = filePath.toLowerCase();
@@ -155,8 +155,34 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
path.includes("/webhooks/") ||
path.includes("/paths/")
) {
// Fast-path: if filepath matches, still validate content to prevent
// generic JSON from being misclassified as OpenAPI
const keys = Object.keys(content);
const hasOnlyGenericProperties = keys.every(
(key) =>
!key.startsWith("x-") && // Not a custom extension
![
"openapi",
"swagger",
"info",
"paths",
"components",
"definitions",
"parameters",
"responses",
"securityDefinitions",
"tags",
"servers",
"webhooks",
].includes(key)
);
// Only accept if content doesn't have only generic properties
if (!hasOnlyGenericProperties) {
return true;
}
// If it has only generic properties, fall through to continue validation
}
}
// Check for component-like structures (only if we have strong indicators)

View File

@@ -1265,6 +1265,40 @@ type: string`;
expect(result?.isOpenAPI).toBeFalse();
});
it('should reject generic content in component directories (security check)', () => {
const parser = parsers?.['openapi-parser'];
expect(parser).toBeDefined();
// Generic JSON/YAML that should NOT be accepted even in component directories
const genericContent = `firstName: John
lastName: Doe
email: john@example.com
age: 30`;
// Test various component directory paths
const componentPaths = [
'components/schemas/User.yaml',
'components/parameters/UserId.yaml',
'components/responses/UserResponse.yaml',
'components/requestBodies/UserCreate.yaml',
'components/headers/RateLimit.yaml',
'components/examples/UserExample.yaml',
'components/securitySchemes/BearerAuth.yaml',
'components/links/UserLink.yaml',
'components/callbacks/NewMessageCallback.yaml',
'webhooks/messageCreated.yaml',
'paths/users.yaml',
];
componentPaths.forEach((path) => {
// @ts-expect-error We are testing edge cases
const result = parser?.parse(genericContent, { filepath: path });
expect(result).toBeDefined();
// Should be rejected even though path matches component directory pattern
expect(result?.isOpenAPI).toBeFalse();
});
});
it('should handle response code sorting with mixed numeric and non-numeric codes', () => {
const printer = printers?.['openapi-ast'];
expect(printer).toBeDefined();

View File

@@ -159,17 +159,40 @@ city: New York`;
expect(parsedData?.isOpenAPI).toBeFalse();
});
it('should accept files in OpenAPI directories even with simple content', () => {
it('should reject generic content in OpenAPI component directories', () => {
const parser = parsers?.['openapi-parser'];
expect(parser).toBeDefined();
const simpleYaml = `name: John
// Generic content that doesn't match OpenAPI structure
const genericYaml = `name: John
age: 30
city: New York`;
// @ts-expect-error We are mocking things here
const result = parser?.parse(simpleYaml, { filepath: 'components/schemas/User.yaml' });
const result = parser?.parse(genericYaml, { filepath: 'components/schemas/User.yaml' });
expect(result).toBeDefined();
// Should be rejected even though it's in a component directory
expect(result?.isOpenAPI).toBeFalse();
// Format may be undefined when isOpenAPI is false
// The important thing is that it's rejected
});
it('should accept valid OpenAPI content in component directories', () => {
const parser = parsers?.['openapi-parser'];
expect(parser).toBeDefined();
// Valid schema content in component directory
const schemaYaml = `type: object
properties:
name:
type: string
age:
type: integer`;
// @ts-expect-error We are mocking things here
const result = parser?.parse(schemaYaml, { filepath: 'components/schemas/User.yaml' });
expect(result).toBeDefined();
// Should be accepted because content is valid OpenAPI schema
expect(result?.isOpenAPI).toBeTrue();
expect(result?.format).toBe('yaml');
});

View File

@@ -1,13 +1,90 @@
import { describe, expect, it } from "bun:test";
import { printers } from "../src/index.js";
import prettier from "prettier";
describe("Markdown Formatting in Descriptions", () => {
const printer = printers?.["openapi-ast"];
// Get printDocToString from Prettier's internals (call once per test)
function getPrintDocToString() {
// Try multiple paths to access Prettier's printDocToString
const prettierAny = prettier as any;
// Path 1: __internal.doc.printDocToString
if (prettierAny.__internal?.doc?.printDocToString) {
return prettierAny.__internal.doc.printDocToString;
}
// Path 2: __internal.docPrinter.formatDoc
if (prettierAny.__internal?.docPrinter?.formatDoc) {
return prettierAny.__internal.docPrinter.formatDoc;
}
// Path 3: Try require with different paths
try {
// Try prettier/standalone or other internal paths
const docUtils = require("prettier/standalone");
if (docUtils?.printDocToString) {
return docUtils.printDocToString;
}
} catch {
// Not available
}
try {
// Try direct access to internal modules
const prettierDoc = require("prettier/doc");
if (prettierDoc?.printer?.printDocToString) {
return prettierDoc.printer.printDocToString;
}
} catch {
// Not available
}
// If all else fails, check if result is already a string
// (some Prettier versions return strings directly)
return null;
}
// Helper function to convert Doc to formatted string using Prettier's printDocToString
function docToString(doc: any, printDocToString: any, options: any = {}): string {
// If already a string, return it
if (typeof doc === "string") {
return doc;
}
// If printDocToString is not available, try to stringify the doc
if (!printDocToString) {
// Fallback: if it's an object, try JSON.stringify or inspect
if (doc && typeof doc === "object") {
// Try to see if it has a toString that works
const str = String(doc);
if (str !== "[object Object]") {
return str;
}
// Otherwise, we need printDocToString
throw new Error("printDocToString is required to convert Doc to string");
}
return String(doc);
}
// Use printDocToString to convert Doc to formatted string
const result = printDocToString(doc, {
printWidth: options.printWidth || 80,
tabWidth: options.tabWidth || 2,
useTabs: false,
});
// printDocToString returns { formatted: string }
return result?.formatted || String(doc);
}
describe("Basic markdown formatting", () => {
it("should format description fields with markdown", () => {
expect(printer).toBeDefined();
const printDocToString = getPrintDocToString();
const testData = {
isOpenAPI: true,
format: "yaml",
@@ -44,18 +121,20 @@ describe("Markdown Formatting in Descriptions", () => {
throw new Error("Result is undefined");
}
const resultString = result.toString();
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
// Check that multiple spaces are normalized in the original content
// Note: YAML may format this differently, but the content should be processed
// The description field should exist and be formatted
expect(resultString).toContain("description:");
// Check that multiple blank lines are normalized
// Check that multiple blank lines are normalized (no quad-blank-lines)
expect(resultString).not.toMatch(/\n{4,}/);
});
it("should preserve code blocks in descriptions", () => {
const printDocToString = getPrintDocToString();
const testData = {
isOpenAPI: true,
format: "yaml",
@@ -79,7 +158,7 @@ describe("Markdown Formatting in Descriptions", () => {
throw new Error("Result is undefined");
}
const resultString = result.toString();
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
// Code blocks (4+ spaces) should be preserved
expect(resultString).toContain(" const x = 1;");
@@ -87,6 +166,8 @@ describe("Markdown Formatting in Descriptions", () => {
});
it("should format markdown in nested objects", () => {
const printDocToString = getPrintDocToString();
const testData = {
isOpenAPI: true,
format: "yaml",
@@ -127,7 +208,7 @@ describe("Markdown Formatting in Descriptions", () => {
throw new Error("Result is undefined");
}
const resultString = result.toString();
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
// Both parameter and response descriptions should be formatted
expect(resultString).toContain("description:");
@@ -136,6 +217,8 @@ describe("Markdown Formatting in Descriptions", () => {
describe("Summary field formatting", () => {
it("should format summary fields", () => {
const printDocToString = getPrintDocToString();
const testData = {
isOpenAPI: true,
format: "yaml",
@@ -167,7 +250,7 @@ describe("Markdown Formatting in Descriptions", () => {
throw new Error("Result is undefined");
}
const resultString = result.toString();
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
// Summary fields should be processed
expect(resultString).toContain("summary:");