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:
2026-03-22 23:58:17 +08:00
parent 56bee336db
commit 55986a1965
11 changed files with 5580 additions and 3398 deletions

View File

@@ -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
View 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);