281 lines
8.8 KiB
TypeScript
281 lines
8.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
generateFixtures,
|
|
constraintCoverageMatrix,
|
|
modelJson,
|
|
parseFixtureModel,
|
|
serializeDataset,
|
|
topologicalTableOrder,
|
|
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);
|
|
});
|
|
|
|
it("imports and generates composite relational constraints topologically", () => {
|
|
const sql = `CREATE TABLE tenants (
|
|
region VARCHAR(8) NOT NULL,
|
|
tenant_id INTEGER NOT NULL,
|
|
PRIMARY KEY (region, tenant_id)
|
|
);
|
|
CREATE TABLE events (
|
|
region VARCHAR(8) NOT NULL,
|
|
tenant_id INTEGER NOT NULL,
|
|
sequence INTEGER NOT NULL,
|
|
UNIQUE (region, tenant_id, sequence),
|
|
FOREIGN KEY (region, tenant_id) REFERENCES tenants(region, tenant_id)
|
|
);`;
|
|
const model = parseFixtureModel(sql, "sql").model;
|
|
expect(model.tables[1]?.constraints).toEqual([
|
|
{ kind: "unique", fields: ["region", "tenant_id", "sequence"] },
|
|
{
|
|
kind: "foreignKey",
|
|
fields: ["region", "tenant_id"],
|
|
references: { table: "tenants", fields: ["region", "tenant_id"] },
|
|
},
|
|
]);
|
|
expect(topologicalTableOrder(model)).toEqual(["tenants", "events"]);
|
|
const dataset = generateFixtures(model, {
|
|
seed: "composite",
|
|
rows: 12,
|
|
boundaryPercent: 10,
|
|
invalidPercent: 0,
|
|
providerProfile: "auto",
|
|
});
|
|
const targets = new Set(
|
|
dataset.tables.tenants?.map((row) => `${row.region}:${row.tenant_id}`),
|
|
);
|
|
expect(
|
|
dataset.tables.events?.every((row) =>
|
|
targets.has(`${row.region}:${row.tenant_id}`),
|
|
),
|
|
).toBe(true);
|
|
expect(dataset.generationOrder).toEqual(["tenants", "events"]);
|
|
expect(
|
|
constraintCoverageMatrix(model, dataset).some(
|
|
(entry) =>
|
|
entry.table === "events" &&
|
|
entry.constraint.startsWith("foreignKey(region,tenant_id)"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("accepts scalar table-level primary and foreign-key constraints", () => {
|
|
const sql = `CREATE TABLE accounts (
|
|
account_id INTEGER NOT NULL,
|
|
PRIMARY KEY (account_id)
|
|
);
|
|
CREATE TABLE entries (
|
|
account_id INTEGER NOT NULL,
|
|
FOREIGN KEY (account_id) REFERENCES accounts(account_id)
|
|
);`;
|
|
const model = parseFixtureModel(sql, "sql").model;
|
|
expect(model.tables[0]?.constraints).toEqual([
|
|
{ kind: "unique", fields: ["account_id"] },
|
|
]);
|
|
expect(model.tables[1]?.constraints).toEqual([
|
|
{
|
|
kind: "foreignKey",
|
|
fields: ["account_id"],
|
|
references: { table: "accounts", fields: ["account_id"] },
|
|
},
|
|
]);
|
|
const dataset = generateFixtures(model, {
|
|
seed: "scalar-table-constraints",
|
|
rows: 8,
|
|
boundaryPercent: 0,
|
|
invalidPercent: 0,
|
|
});
|
|
const accounts = new Set(
|
|
dataset.tables.accounts?.map((row) => row.account_id),
|
|
);
|
|
expect(
|
|
dataset.tables.entries?.every((row) => accounts.has(row.account_id)),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("supports deterministic built-in and custom provider profiles", () => {
|
|
const model = parseFixtureModel("display_name,host,sku\n", "csv").model;
|
|
const options = {
|
|
seed: "providers",
|
|
rows: 4,
|
|
boundaryPercent: 0,
|
|
invalidPercent: 0,
|
|
providerProfile: "auto",
|
|
};
|
|
const generated = generateFixtures(model, options);
|
|
expect(generated).toEqual(generateFixtures(model, options));
|
|
expect(generated.tables.records?.[0]).toMatchObject({
|
|
display_name: expect.stringMatching(/ /u),
|
|
host: expect.stringMatching(/\.example\.test$/u),
|
|
sku: "SKU-000001",
|
|
});
|
|
});
|
|
});
|