import { readdir, readFile, stat } from "node:fs/promises"; import { join, resolve, relative, dirname, basename } from "node:path"; import chalk from "chalk"; import { type CommandResult, success, silentFailure } from "../../lib/command-result"; interface ValidationContext { failures: number; } function pass(msg: string) { console.log(chalk.green(`\u2713 ${msg}`)); } function fail(ctx: ValidationContext, msg: string) { console.log(chalk.red(`\u2717 ${msg}`)); ctx.failures++; } async function subdirs(dir: string): Promise { const entries = await readdir(dir); const dirs: string[] = []; for (const entry of entries) { const full = join(dir, entry); const s = await stat(full); if (s.isDirectory()) dirs.push(entry); } return dirs.sort(); } function checkStructure(ctx: ValidationContext, entries: string[], label: string): boolean { if (entries.includes("README.md")) { pass(`${label}: has README.md`); } else { fail(ctx, `${label}: missing README.md`); } if (entries.includes("mock.json")) { pass(`${label}: has mock.json`); return true; } fail(ctx, `${label}: missing mock.json`); return false; } async function checkSchema( ctx: ValidationContext, data: MockoonData, label: string, ): Promise { const commons = await import("@mockoon/commons"); // EnvironmentSchemaNoFix is a Joi schema typed as `any` in @mockoon/commons const schema = commons.EnvironmentSchemaNoFix as { validate: ( value: unknown, options?: { abortEarly?: boolean }, ) => { error?: { details: { message: string }[] } }; }; const result = schema.validate(data, { abortEarly: false }); if (!result.error) { pass(`${label}: valid Mockoon schema`); return true; } for (const detail of result.error.details) { fail(ctx, `${label}: schema — ${detail.message}`); } return false; } interface MockoonRoute { uuid: string; responses?: MockoonResponse[]; } interface MockoonResponse { uuid: string; label?: string; headers?: { key: string; value: string }[]; bodyType?: string; databucketID?: string; } interface MockoonData { uuid?: string; name?: string; port?: number; routes?: MockoonRoute[]; data?: { id: string }[]; } function checkResponseQuality(ctx: ValidationContext, data: MockoonData, label: string) { const routes = data.routes ?? []; for (const route of routes) { for (const resp of route.responses ?? []) { const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`; const headers = resp.headers ?? []; const hasContentType = headers.some((h) => h.key.toLowerCase() === "content-type"); if (hasContentType) { pass(`${respLabel}: has Content-Type`); } else { fail(ctx, `${respLabel}: missing Content-Type header`); } if (resp.label && resp.label.trim().length > 0) { pass(`${respLabel}: has label "${resp.label}"`); } else { fail(ctx, `${respLabel}: missing or empty label`); } } } } function checkDatabucketRefs(ctx: ValidationContext, data: MockoonData, label: string) { const bucketIds = new Set((data.data ?? []).map((d) => d.id)); for (const route of data.routes ?? []) { for (const resp of route.responses ?? []) { if (resp.bodyType === "DATABUCKET") { const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`; if (resp.databucketID && bucketIds.has(resp.databucketID)) { pass(`${respLabel}: databucket "${resp.databucketID}" exists`); } else { fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`); } } } } } async function discoverAllScenarios(mocksDir: string): Promise { const scenarios: string[] = []; const providers = await subdirs(mocksDir); for (const provider of providers) { const providerDir = join(mocksDir, provider); for (const scenario of await subdirs(providerDir)) { scenarios.push(`${provider}/${scenario}`); } } return scenarios; } function resolveScenarioDir(arg: string): string { const abs = resolve(arg); if (basename(abs) === "mock.json") return dirname(abs); return abs; } async function validateScenario(ctx: ValidationContext, scenarioDir: string, mocksDir: string) { const label = relative(mocksDir, scenarioDir); console.log(chalk.bold(`\n\u2500\u2500 ${label} \u2500\u2500`)); let entries: string[]; try { entries = await readdir(scenarioDir); } catch { fail(ctx, `${label}: directory not found`); return; } const hasMock = checkStructure(ctx, entries, label); if (!hasMock) return; const mockPath = join(scenarioDir, "mock.json"); let data: MockoonData; try { data = JSON.parse(await readFile(mockPath, "utf-8")) as MockoonData; } catch (err: unknown) { const errMsg = err instanceof Error ? err.message : String(err); fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`); return; } // The quality checks below can throw on schema-invalid data const schemaOk = await checkSchema(ctx, data, label); if (!schemaOk) return; checkResponseQuality(ctx, data, label); checkDatabucketRefs(ctx, data, label); } export async function runMockValidate(mocksRoot: string, paths: string[]): Promise { const ctx: ValidationContext = { failures: 0 }; const mocksDir = resolve(mocksRoot); const targets = paths.length > 0 ? paths.map(resolveScenarioDir) : (await discoverAllScenarios(mocksDir)).map((s) => join(mocksDir, s)); if (targets.length === 0) { fail(ctx, "No scenarios found under mocks/"); return silentFailure(); } console.log(chalk.bold("\nValidating mock configs...\n")); for (const target of targets) { await validateScenario(ctx, target, mocksDir); } console.log(chalk.bold("\n\u2500\u2500 summary \u2500\u2500")); if (ctx.failures === 0) { console.log(chalk.green("\u2713 All checks passed")); } else { console.log(chalk.red(`\u2717 ${ctx.failures} check(s) failed`)); } return ctx.failures === 0 ? success() : silentFailure(); }