mirror of
https://github.com/LukeHagar/openapi-types.git
synced 2025-12-06 04:20:29 +00:00
514 lines
12 KiB
TypeScript
Executable File
514 lines
12 KiB
TypeScript
Executable File
import { describe, expect, it } from "bun:test";
|
|
import Ajv, { type JSONSchemaType } from "ajv";
|
|
import type { Specification } from "../2.0";
|
|
import { schemas } from "../schemas/2.0";
|
|
|
|
// Import all specification files from tests/2.0
|
|
import { apiWithExamples } from "./2.0/api-with-examples";
|
|
import { petstore } from "./2.0/petstore";
|
|
import { petstoreExpanded } from "./2.0/petstore-expanded";
|
|
import { petstoreMinimal } from "./2.0/petstore-minimal";
|
|
import { petstoreSimple } from "./2.0/petstore-simple";
|
|
import { petstoreWithExternalDocs } from "./2.0/petstore-with-external-docs";
|
|
import { uber } from "./2.0/uber";
|
|
|
|
const ajv = new Ajv({
|
|
allErrors: true,
|
|
verbose: true,
|
|
strict: false,
|
|
});
|
|
|
|
const schema: JSONSchemaType<Specification> = JSON.parse(
|
|
JSON.stringify(schemas.specification),
|
|
);
|
|
|
|
// validate is a type guard for Specification - type is inferred from schema type
|
|
const validate = ajv.compile(schema);
|
|
|
|
// All specification files to test
|
|
const specsToTest = [
|
|
{ name: "API with Examples", spec: apiWithExamples },
|
|
{ name: "Petstore", spec: petstore },
|
|
{ name: "Petstore with External Docs", spec: petstoreWithExternalDocs },
|
|
{ name: "Petstore Simple", spec: petstoreSimple },
|
|
{ name: "Petstore Minimal", spec: petstoreMinimal },
|
|
{ name: "Petstore Expanded", spec: petstoreExpanded },
|
|
{ name: "Uber API", spec: uber },
|
|
];
|
|
|
|
describe("Swagger 2.0 Schema Validation", () => {
|
|
for (const { name, spec } of specsToTest) {
|
|
describe(name, () => {
|
|
it("should be a valid Swagger 2.0 specification", () => {
|
|
const isValid = validate(spec);
|
|
|
|
if (!isValid) {
|
|
console.error(`Validation errors for ${name}:`, validate.errors);
|
|
}
|
|
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should have required swagger version", () => {
|
|
expect(spec.swagger).toBe("2.0");
|
|
});
|
|
|
|
it("should have required info object", () => {
|
|
expect(spec.info).toBeDefined();
|
|
expect(spec.info.title).toBeDefined();
|
|
expect(spec.info.version).toBeDefined();
|
|
});
|
|
|
|
it("should have valid paths object", () => {
|
|
if (spec.paths) {
|
|
expect(typeof spec.paths).toBe("object");
|
|
expect(spec.paths).not.toBeNull();
|
|
}
|
|
});
|
|
|
|
it("should have valid definitions object", () => {
|
|
if (spec.definitions) {
|
|
expect(typeof spec.definitions).toBe("object");
|
|
expect(spec.definitions).not.toBeNull();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
describe("Schema Validation Details", () => {
|
|
it("should validate all specifications against the JSON schema", () => {
|
|
const results = specsToTest.map(({ name, spec }) => {
|
|
const isValid = validate(spec);
|
|
return { name, isValid, errors: validate.errors };
|
|
});
|
|
|
|
const failedSpecs = results.filter((result) => !result.isValid);
|
|
|
|
if (failedSpecs.length > 0) {
|
|
console.error("Failed specifications:");
|
|
failedSpecs.forEach(({ name, errors }) => {
|
|
console.error(`${name}:`, errors);
|
|
});
|
|
}
|
|
|
|
expect(failedSpecs.length).toBe(0);
|
|
});
|
|
|
|
it("should have consistent swagger version across all specs", () => {
|
|
const versions = specsToTest.map(({ spec }) => spec.swagger);
|
|
const uniqueVersions = [...new Set(versions)];
|
|
|
|
expect(uniqueVersions).toHaveLength(1);
|
|
expect(uniqueVersions[0]).toBe("2.0");
|
|
});
|
|
|
|
it("should have valid host format when present", () => {
|
|
specsToTest.forEach(({ name, spec }) => {
|
|
if (spec.host) {
|
|
// Host should not contain protocol
|
|
expect(spec.host).not.toMatch(/^https?:\/\//);
|
|
// Host should not contain path
|
|
expect(spec.host).not.toContain("/");
|
|
}
|
|
});
|
|
});
|
|
|
|
it("should have valid basePath format when present", () => {
|
|
specsToTest.forEach(({ name, spec }) => {
|
|
if (spec.basePath) {
|
|
// BasePath should start with /
|
|
expect(spec.basePath).toMatch(/^\//);
|
|
}
|
|
});
|
|
});
|
|
|
|
it("should have valid schemes when present", () => {
|
|
specsToTest.forEach(({ name, spec }) => {
|
|
if (spec.schemes) {
|
|
expect(Array.isArray(spec.schemes)).toBe(true);
|
|
spec.schemes.forEach((scheme: string) => {
|
|
expect(["http", "https", "ws", "wss"]).toContain(scheme);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Error Validation Tests", () => {
|
|
it("should reject invalid swagger version", () => {
|
|
const invalidSpec = {
|
|
swagger: "1.0", // Invalid version
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
expect(validate.errors?.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should reject missing required fields", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
// Missing required 'info' field
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
expect(
|
|
validate.errors?.some((error) => error.keyword === "required"),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("should reject invalid info object structure", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
// Missing required title and version
|
|
description: "Test API",
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid host format", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
host: "https://example.com", // Should not include protocol
|
|
basePath: "/api",
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid basePath format", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
basePath: "api", // Should start with /
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid schemes", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
schemes: ["invalid-scheme", "ftp"], // Invalid schemes
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid paths structure", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
paths: {
|
|
"/test": {
|
|
get: {
|
|
responses: {
|
|
"200": {
|
|
description: "Success",
|
|
// Missing schema or type - this should be invalid for Swagger 2.0
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
// Note: This might pass if the schema allows responses without schema/type
|
|
// We'll check if it fails, and if not, we'll adjust the test
|
|
if (!isValid) {
|
|
expect(validate.errors).toBeDefined();
|
|
expect(validate.errors?.length).toBeGreaterThan(0);
|
|
} else {
|
|
// If it passes, let's create a more obviously invalid case
|
|
const moreInvalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
paths: {
|
|
"/test": {
|
|
get: {
|
|
responses: {
|
|
"200": {
|
|
// Missing required description
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const isMoreInvalid = validate(moreInvalidSpec);
|
|
expect(isMoreInvalid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("should reject invalid parameter definitions", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
paths: {
|
|
"/test": {
|
|
get: {
|
|
parameters: [
|
|
{
|
|
name: "test-param",
|
|
in: "query",
|
|
// Missing required 'type' field
|
|
description: "Test parameter",
|
|
},
|
|
],
|
|
responses: {
|
|
"200": {
|
|
description: "Success",
|
|
schema: {
|
|
type: "string",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid response definitions", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
paths: {
|
|
"/test": {
|
|
get: {
|
|
responses: {
|
|
"200": {
|
|
// Missing required 'description' field
|
|
schema: {
|
|
type: "string",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid security definitions", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
securityDefinitions: {
|
|
"invalid-auth": {
|
|
type: "invalid-type", // Invalid security type
|
|
name: "Authorization",
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid OAuth2 security definitions", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
securityDefinitions: {
|
|
oauth2: {
|
|
type: "oauth2",
|
|
flow: "invalid-flow", // Invalid OAuth2 flow
|
|
authorizationUrl: "https://example.com/oauth/authorize",
|
|
tokenUrl: "https://example.com/oauth/token",
|
|
scopes: {
|
|
read: "Read access",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid definitions schema", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
definitions: {
|
|
InvalidModel: {
|
|
type: "invalid-type", // Invalid JSON Schema type
|
|
properties: {
|
|
name: {
|
|
type: "string",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid external documentation", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
externalDocs: {
|
|
// Missing required 'url' field
|
|
description: "External documentation",
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid tags", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
},
|
|
tags: [
|
|
{
|
|
// Missing required 'name' field
|
|
description: "Test tag",
|
|
},
|
|
],
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid contact information", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
contact: {
|
|
email: "invalid-email-format", // Invalid email format
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should reject invalid license information", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
version: "1.0.0",
|
|
license: {
|
|
name: "MIT",
|
|
// Missing required 'url' field for some license types
|
|
},
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
});
|
|
|
|
it("should provide detailed error messages for validation failures", () => {
|
|
const invalidSpec = {
|
|
swagger: "2.0",
|
|
info: {
|
|
title: "Test API",
|
|
// Missing version
|
|
},
|
|
};
|
|
|
|
const isValid = validate(invalidSpec);
|
|
expect(isValid).toBe(false);
|
|
expect(validate.errors).toBeDefined();
|
|
|
|
// Check that error messages are descriptive
|
|
const errors = validate.errors || [];
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
|
|
// Verify error structure
|
|
errors.forEach((error) => {
|
|
expect(error).toHaveProperty("keyword");
|
|
expect(error).toHaveProperty("message");
|
|
// AJV uses 'instancePath' instead of 'dataPath' in newer versions
|
|
expect(error).toHaveProperty("instancePath");
|
|
});
|
|
});
|
|
});
|
|
});
|