Files
fixture-tools/tests/fixture/model.test.ts
T
2026-09-01 13:03:32 +02:00

179 lines
5.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
generateFixtures,
modelJson,
parseFixtureModel,
serializeDataset,
type GeneratedDataset,
} from "../../src/fixture/model";
const SQL = `CREATE TABLE users (
id INTEGER PRIMARY KEY,
email VARCHAR(80) NOT NULL UNIQUE,
score DECIMAL(5,2)
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id)
);`;
describe("fixture model", () => {
it("imports focused SQL constraints and preserves foreign keys", () => {
const parsed = parseFixtureModel(SQL, "sql");
expect(parsed.model.tables).toHaveLength(2);
expect(parsed.model.tables[0]?.fields[0]).toMatchObject({
name: "id",
type: "integer",
required: true,
unique: true,
distribution: "sequential",
});
expect(parsed.model.tables[0]?.fields[1]).toMatchObject({
name: "email",
type: "email",
unique: true,
maxLength: 80,
});
expect(parsed.model.tables[1]?.fields[1]).toMatchObject({
type: "foreignKey",
reference: "users.id",
});
});
it("generates replayable records with referential integrity", () => {
const model = parseFixtureModel(SQL, "sql").model;
const options = {
seed: "repeat-me",
rows: 20,
boundaryPercent: 15,
invalidPercent: 0,
};
const first = generateFixtures(model, options);
const second = generateFixtures(model, options);
expect(first).toEqual(second);
expect(
generateFixtures(model, { ...options, seed: "different" }),
).not.toEqual(first);
const ids = new Set(first.tables.users?.map((row) => row.id));
expect(first.tables.orders?.every((row) => ids.has(row.user_id))).toBe(
true,
);
expect(new Set(first.tables.users?.map((row) => row.email)).size).toBe(20);
});
it("imports JSON Schema bounds and round-trips the editable model", () => {
const parsed = parseFixtureModel(
JSON.stringify({
title: "People",
type: "object",
required: ["age"],
properties: {
age: { type: "integer", minimum: 18, maximum: 90 },
state: { enum: ["draft", "active"] },
nickname: { type: "string", minLength: 2, maxLength: 8 },
},
}),
"json-schema",
);
expect(parsed.model.tables[0]).toMatchObject({ name: "People" });
expect(parsed.model.tables[0]?.fields[0]).toMatchObject({
minimum: 18,
maximum: 90,
required: true,
});
expect(
parseFixtureModel(modelJson(parsed.model), "fixture-model").model,
).toEqual(parsed.model);
});
it("records every intentional mutation and emits common formats", () => {
const model = parseFixtureModel("id,email\n", "csv").model;
const dataset = generateFixtures(model, {
seed: "invalid",
rows: 3,
boundaryPercent: 0,
invalidPercent: 100,
});
expect(dataset.violations).toHaveLength(6);
expect(serializeDataset(dataset, "json")).toMatch(/^\[/u);
expect(serializeDataset(dataset, "ndjson").trim().split("\n")).toHaveLength(
3,
);
expect(serializeDataset(dataset, "xml")).toContain("<fixtures");
expect(serializeDataset(dataset, "sql")).toContain('INSERT INTO "records"');
});
it("makes CSV spreadsheet-safe and quotes SQL literals", () => {
const dataset: GeneratedDataset = {
seed: "safe",
rowsPerTable: 1,
tables: {
records: [{ value: '=HYPERLINK("https://invalid")', quote: "O'Brien" }],
},
violations: [],
};
expect(serializeDataset(dataset, "csv")).toContain("'=HYPERLINK");
expect(serializeDataset(dataset, "sql")).toContain("O''Brien");
});
it("imports bounded XSD and rejects active document declarations", () => {
const xsd = `<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><xs:element name="people"><xs:complexType><xs:sequence><xs:element name="age" type="xs:integer"/><xs:element name="email" type="xs:string" minOccurs="0"/></xs:sequence></xs:complexType></xs:element></xs:schema>`;
const parsed = parseFixtureModel(xsd, "xsd");
expect(parsed.model.tables[0]?.fields).toMatchObject([
{ name: "age", type: "integer", required: true },
{ name: "email", required: false },
]);
expect(() =>
parseFixtureModel('<!DOCTYPE x [<!ENTITY y "z">]><x/>', "xsd"),
).toThrow(/DOCTYPE/u);
});
it("rejects misleading recipes and caps wide generations", () => {
expect(() =>
parseFixtureModel(
JSON.stringify({
tables: [
{
name: "records",
fields: [
{
name: "id",
type: "integer",
required: true,
unique: true,
distribution: "constant",
constant: "wrong type",
},
],
},
],
}),
"fixture-model",
),
).toThrow(/constant/u);
const wide = {
tables: [
{
name: "wide",
fields: Array.from({ length: 100 }, (_unused, index) => ({
name: `field_${index}`,
type: "string" as const,
required: true,
unique: false,
distribution: "sequential" as const,
})),
},
],
};
expect(() =>
generateFixtures(wide, {
seed: "bounded",
rows: 1_001,
boundaryPercent: 0,
invalidPercent: 0,
}),
).toThrow(/100,000 field values/u);
});
});