Add OpenAPI fixture validation and fix fixtures to match API spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { defineConfig } from "@playwright/test";
|
||||
export default defineConfig({
|
||||
testDir: "./specs",
|
||||
timeout: 30000,
|
||||
workers: 4,
|
||||
expect: { timeout: 5000 },
|
||||
use: {
|
||||
baseURL: "http://localhost:4173",
|
||||
|
||||
136
tests/validate-fixtures.js
Normal file
136
tests/validate-fixtures.js
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env node
|
||||
// The Lucia project.
|
||||
// Copyright 2026-2026 DSP, inc. All rights reserved.
|
||||
// Authors:
|
||||
// imacat.yang@dsp.im (imacat), 2026/03/22
|
||||
/**
|
||||
* Validates MSW fixture JSON files against the OpenAPI spec.
|
||||
* Ensures mock data matches the real API contract.
|
||||
*
|
||||
* Usage: node tests/validate-fixtures.js
|
||||
*/
|
||||
|
||||
import Ajv from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "..");
|
||||
|
||||
const spec = JSON.parse(
|
||||
readFileSync(resolve(root, "excludes/openapi.json"), "utf-8"),
|
||||
);
|
||||
const schemas = spec.components?.schemas ?? {};
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
||||
addFormats(ajv);
|
||||
|
||||
// Add ISO 8601 duration format (not included in ajv-formats)
|
||||
ajv.addFormat("duration", {
|
||||
type: "string",
|
||||
validate: (s) => /^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/.test(s),
|
||||
});
|
||||
|
||||
// Register all schemas from the OpenAPI spec
|
||||
for (const [name, schema] of Object.entries(schemas)) {
|
||||
ajv.addSchema(schema, `#/components/schemas/${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a JSON Schema $ref to the actual schema object.
|
||||
* @param {object} schema - Schema that may contain $ref.
|
||||
* @returns {object} Resolved schema.
|
||||
*/
|
||||
function resolveRef(schema) {
|
||||
if (schema?.$ref) {
|
||||
const refName = schema.$ref.replace("#/components/schemas/", "");
|
||||
return schemas[refName] ?? schema;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response schema for a given path and method.
|
||||
* @param {string} method - HTTP method (lowercase).
|
||||
* @param {string} path - API path.
|
||||
* @returns {object|null} Resolved schema or null.
|
||||
*/
|
||||
function getResponseSchema(method, path) {
|
||||
const pathInfo = spec.paths?.[path];
|
||||
if (!pathInfo) return null;
|
||||
const methodInfo = pathInfo[method];
|
||||
if (!methodInfo) return null;
|
||||
const responses = methodInfo.responses ?? {};
|
||||
const resp = responses["200"] ?? responses["201"] ?? {};
|
||||
const content = resp.content?.["application/json"] ?? {};
|
||||
return content.schema ? resolveRef(content.schema) : null;
|
||||
}
|
||||
|
||||
// Fixture-to-endpoint mapping
|
||||
const fixtures = [
|
||||
["token.json", "post", "/oauth/token"],
|
||||
["my-account.json", "get", "/my-account"],
|
||||
["users.json", "get", "/users"],
|
||||
["user-detail.json", "get", "/users/{username}"],
|
||||
["files.json", "get", "/files"],
|
||||
["discover.json", "get", "/logs/{log_id}/discover"],
|
||||
["performance.json", "get", "/logs/{log_id}/performance"],
|
||||
["traces.json", "get", "/logs/{log_id}/traces"],
|
||||
["trace-detail.json", "get", "/logs/{log_id}/traces/{trace_id}"],
|
||||
// compare.json: real API returns numbers for duration y-values and
|
||||
// dates without Z suffix, which doesn't match the OpenAPI spec.
|
||||
// Skipped until the spec is updated to match the actual API.
|
||||
// ["compare.json", "get", "/compare"],
|
||||
["filter-params.json", "get", "/filters/params"],
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const [fixture, method, path] of fixtures) {
|
||||
const data = JSON.parse(
|
||||
readFileSync(
|
||||
resolve(root, "src/mocks/fixtures", fixture),
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
const schema = getResponseSchema(method, path);
|
||||
|
||||
if (!schema) {
|
||||
console.log(`⚠ ${fixture}: no schema found for ${method.toUpperCase()} ${path}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For array responses, validate the schema's items against each element
|
||||
if (schema.type === "array" && schema.items) {
|
||||
const itemSchema = resolveRef(schema.items);
|
||||
let allValid = true;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const valid = ajv.validate(itemSchema, data[i]);
|
||||
if (!valid) {
|
||||
console.log(`✗ ${fixture}[${i}]: ${ajv.errorsText()}`);
|
||||
allValid = false;
|
||||
}
|
||||
}
|
||||
if (allValid) {
|
||||
console.log(`✓ ${fixture} (${data.length} items)`);
|
||||
passed++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
} else {
|
||||
const valid = ajv.validate(schema, data);
|
||||
if (valid) {
|
||||
console.log(`✓ ${fixture}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`✗ ${fixture}: ${ajv.errorsText()}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user