Files
plex-api-spec/tests/utils/import.ts
2025-04-05 12:34:40 +02:00

131 lines
3.9 KiB
TypeScript

import PMSSpec from "../../output/plex-media-server-spec-dereferenced.yaml"
import Ajv from "ajv"
import addFormats from "ajv-formats"
import { expect } from "vitest"
import { isArray, merge } from "lodash-es"
import { xml2json } from "xml-js"
/**
* Validate a response against the OpenAPI spec
* NOTE: It accounts for the following scenarios:
* - properties that are not defined in the openAPI spec
* - properties that are defined in the openAPI spec but are not present in the response IF they are required
* - properties with conflicting types
* @param path
* @param type
* @param statusCode
* @param response
* @param responseType
*/
export function validateResponseSpec(
path: string,
type: "get" | "post" | "delete",
statusCode: number,
response: any,
responseType: "json" | "xml" = "json"
): void {
const ajv = new Ajv({ allErrors: true, strict: false })
const schema = PMSSpec
addAdditionalProperties(schema.paths)
ajv.addSchema(schema, "API.yaml")
addFormats(ajv)
// Convert JSONPath to JSON Pointer
const pathPointer = `/paths/${path.replace(/\//g, `~1`)}/${type}/responses/${statusCode}/content/application~1${responseType}/schema`
let jsonResponse = response
if (responseType === "xml") {
const x = response.replace(/=""/g, '="a"')
jsonResponse = flattenAttributes(
JSON.parse(
xml2json(x, {
compact: true,
ignoreDeclaration: true,
nativeType: true,
// @ts-ignore
nativeTypeAttributes: true
})
)
)
}
const validate = ajv.validate(
{ $ref: "API.yaml#" + pathPointer },
jsonResponse
)
if (!validate) {
for (const error of ajv.errors) {
if (
error.hasOwnProperty("params") &&
error.params.hasOwnProperty("allowedValues") &&
isArray(error.params.allowedValues)
) {
// Format the allowedValues to be a string to make it visible in the error message
error.params.allowedValues = error.params.allowedValues.join(", ")
}
}
console.error(ajv.errors)
console.error(JSON.stringify(jsonResponse, null, 2))
}
expect(ajv.errors).toBe(null)
}
/**
* Recursively add additionalProperties: false to all objects in the schema and merge allOf schemas
* This is necessary to ensure that the schema is strict and does not allow additional properties without explicitly defined in the openApi spec
* DOC: https://ajv.js.org/json-schema.html#additionalproperties
* @param schema
*/
function addAdditionalProperties(schema) {
if (schema === null || typeof schema !== "object") {
return
}
// If the current schema has allOf, merge the schemas because AJV handles mentions of allOf as separate schemas that all need to pass at the same time. We use it in the spec to combine multiple schemas into one.
// DOC: https://ajv.js.org/json-schema.html#allof
if (schema.hasOwnProperty("allOf")) {
const merged = merge({}, ...schema.allOf)
delete schema.allOf
Object.assign(schema, merged)
}
// If the current schema defines an object, ensure additionalProperties is false
if (schema.hasOwnProperty("type") && schema.type === "object") {
if (!schema.hasOwnProperty("additionalProperties")) {
schema.additionalProperties = false
}
}
// Recursively handle properties if they exist
for (const [_, value] of Object.entries(schema)) {
if (Array.isArray(value)) {
value.forEach((x) => addAdditionalProperties(x))
}
if (typeof value === "object") {
addAdditionalProperties(value)
}
}
}
function flattenAttributes(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(flattenAttributes)
} else if (typeof obj === "object" && obj !== null) {
if ("_attributes" in obj) {
Object.assign(obj, obj._attributes)
delete obj._attributes
}
// Recursively process all properties
for (const key in obj) {
obj[key] = flattenAttributes(obj[key])
}
}
return obj
}