Tighten rejection tests to their expected errors
Every assert.throws now names the error it expects, so a broken fixture or an unrelated failure can no longer satisfy a rejection test. Zod rejections are matched by issue path and code via tests/support/zod-assertions.ts. Restore snapshot coverage lost with the release baseline: invalid board floor, room floor and circuit section references and a reserve state that contradicts the device rows are rejected again under test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cc4eefcb86
commit
a8cc7eca5e
8 changed files with 269 additions and 122 deletions
|
|
@ -13,7 +13,10 @@ describe("batched SQLite inserts", () => {
|
||||||
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
|
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
|
||||||
const values = Array.from({ length: 401 }, (_, index) => ({ id: `project-${index}`, name: `Projekt ${index}` }));
|
const values = Array.from({ length: 401 }, (_, index) => ({ id: `project-${index}`, name: `Projekt ${index}` }));
|
||||||
values[400] = { id: "project-0", name: "Duplikat" };
|
values[400] = { id: "project-0", name: "Duplikat" };
|
||||||
assert.throws(() => context.db.transaction((tx) => insertManyInBatches(tx, projects, values)));
|
assert.throws(
|
||||||
|
() => context.db.transaction((tx) => insertManyInBatches(tx, projects, values)),
|
||||||
|
/UNIQUE constraint failed: projects\.id/
|
||||||
|
);
|
||||||
assert.equal(context.db.select().from(projects).all().length, 0);
|
assert.equal(context.db.select().from(projects).all().length, 0);
|
||||||
} finally { context.close(); }
|
} finally { context.close(); }
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -410,25 +410,39 @@ describe("circuit device-row structure project-command repository", () => {
|
||||||
try {
|
try {
|
||||||
const store =
|
const store =
|
||||||
new CircuitDeviceRowStructureProjectCommandRepository(context.db);
|
new CircuitDeviceRowStructureProjectCommandRepository(context.db);
|
||||||
for (const snapshot of [
|
for (const [snapshot, expectedError] of [
|
||||||
|
[
|
||||||
createInsertSnapshot({ circuitId: "circuit-foreign" }),
|
createInsertSnapshot({ circuitId: "circuit-foreign" }),
|
||||||
|
/Circuit does not belong to project/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createInsertSnapshot({
|
createInsertSnapshot({
|
||||||
id: "row-foreign-device",
|
id: "row-foreign-device",
|
||||||
linkedProjectDeviceId: "device-foreign",
|
linkedProjectDeviceId: "device-foreign",
|
||||||
}),
|
}),
|
||||||
|
/Invalid linked project device id/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createInsertSnapshot({
|
createInsertSnapshot({
|
||||||
id: "row-foreign-room",
|
id: "row-foreign-room",
|
||||||
roomId: "room-foreign",
|
roomId: "room-foreign",
|
||||||
}),
|
}),
|
||||||
|
/Invalid room id/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createInsertSnapshot({ id: "row-1" }),
|
createInsertSnapshot({ id: "row-1" }),
|
||||||
]) {
|
/Circuit device-row id already exists/,
|
||||||
assert.throws(() =>
|
],
|
||||||
|
] as const) {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
store.execute({
|
store.execute({
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
expectedRevision: 0,
|
expectedRevision: 0,
|
||||||
source: "user",
|
source: "user",
|
||||||
command: createCircuitDeviceRowInsertProjectCommand(snapshot),
|
command: createCircuitDeviceRowInsertProjectCommand(snapshot),
|
||||||
})
|
}),
|
||||||
|
expectedError
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
||||||
|
|
|
||||||
|
|
@ -568,26 +568,39 @@ describe("circuit structure project-command repository", () => {
|
||||||
const store = new CircuitStructureProjectCommandRepository(
|
const store = new CircuitStructureProjectCommandRepository(
|
||||||
fixture.context.db
|
fixture.context.db
|
||||||
);
|
);
|
||||||
const invalidSnapshots = [
|
const invalidSnapshots: Array<[CircuitSnapshot, RegExp]> = [
|
||||||
|
[
|
||||||
createCircuitSnapshot(fixture, {
|
createCircuitSnapshot(fixture, {
|
||||||
id: "foreign-list",
|
id: "foreign-list",
|
||||||
circuitListId: fixture.foreignCircuitListId,
|
circuitListId: fixture.foreignCircuitListId,
|
||||||
sectionId: fixture.foreignSectionId,
|
sectionId: fixture.foreignSectionId,
|
||||||
}),
|
}),
|
||||||
|
/Circuit list does not belong to project/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createCircuitSnapshot(fixture, {
|
createCircuitSnapshot(fixture, {
|
||||||
id: "foreign-section",
|
id: "foreign-section",
|
||||||
sectionId: fixture.foreignSectionId,
|
sectionId: fixture.foreignSectionId,
|
||||||
}),
|
}),
|
||||||
|
/Section does not belong to circuit list/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createCircuitSnapshot(fixture, {
|
createCircuitSnapshot(fixture, {
|
||||||
id: "duplicate-bmk",
|
id: "duplicate-bmk",
|
||||||
equipmentIdentifier: "-1F1",
|
equipmentIdentifier: "-1F1",
|
||||||
}),
|
}),
|
||||||
|
/Duplicate equipmentIdentifier in circuit list/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createCircuitSnapshot(fixture, {
|
createCircuitSnapshot(fixture, {
|
||||||
id: "duplicate-row",
|
id: "duplicate-row",
|
||||||
deviceRows: [
|
deviceRows: [
|
||||||
createRow("duplicate-row", "row-existing-1", 10),
|
createRow("duplicate-row", "row-existing-1", 10),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
/Circuit device-row id already exists/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createCircuitSnapshot(fixture, {
|
createCircuitSnapshot(fixture, {
|
||||||
id: "foreign-device",
|
id: "foreign-device",
|
||||||
deviceRows: [
|
deviceRows: [
|
||||||
|
|
@ -596,6 +609,9 @@ describe("circuit structure project-command repository", () => {
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
/Invalid linked project device id/,
|
||||||
|
],
|
||||||
|
[
|
||||||
createCircuitSnapshot(fixture, {
|
createCircuitSnapshot(fixture, {
|
||||||
id: "foreign-room",
|
id: "foreign-room",
|
||||||
deviceRows: [
|
deviceRows: [
|
||||||
|
|
@ -604,15 +620,19 @@ describe("circuit structure project-command repository", () => {
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
/Invalid room id/,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
for (const snapshot of invalidSnapshots) {
|
for (const [snapshot, expectedError] of invalidSnapshots) {
|
||||||
assert.throws(() =>
|
assert.throws(
|
||||||
|
() =>
|
||||||
store.execute({
|
store.execute({
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
expectedRevision: 0,
|
expectedRevision: 0,
|
||||||
source: "user",
|
source: "user",
|
||||||
command: createCircuitInsertProjectCommand(snapshot),
|
command: createCircuitInsertProjectCommand(snapshot),
|
||||||
})
|
}),
|
||||||
|
expectedError
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
type EstimateBalanceSnapshot,
|
type EstimateBalanceSnapshot,
|
||||||
type EstimateRowSnapshot,
|
type EstimateRowSnapshot,
|
||||||
} from "../src/shared/estimate-balance/estimate-balance.js";
|
} from "../src/shared/estimate-balance/estimate-balance.js";
|
||||||
|
import { zodIssues, type ExpectedZodIssue } from "./support/zod-assertions.js";
|
||||||
|
|
||||||
const round2 = (value: number) => roundEstimateValue(value, 2);
|
const round2 = (value: number) => roundEstimateValue(value, 2);
|
||||||
let nextId = 0;
|
let nextId = 0;
|
||||||
|
|
@ -236,22 +237,25 @@ describe("estimate balance snapshots and commands", () => {
|
||||||
it("validates snapshots strictly", () => {
|
it("validates snapshots strictly", () => {
|
||||||
const valid = templateExample();
|
const valid = templateExample();
|
||||||
assert.equal(parseEstimateBalanceSnapshot(valid).groups[4]!.rows[3]!.sortOrder, 3);
|
assert.equal(parseEstimateBalanceSnapshot(valid).groups[4]!.rows[3]!.sortOrder, 3);
|
||||||
const invalid: Array<(balance: EstimateBalanceSnapshot) => void> = [
|
const invalid: Array<[(balance: EstimateBalanceSnapshot) => void, ExpectedZodIssue]> = [
|
||||||
(balance) => (balance.groups[0]!.rows[0]!.cosPhi = 0),
|
[(balance) => (balance.groups[0]!.rows[0]!.cosPhi = 0), ["groups.0.rows.0.cosPhi", "too_small"]],
|
||||||
(balance) => (balance.groups[0]!.rows[0]!.simultaneityFactor = 1.5),
|
[(balance) => (balance.groups[0]!.rows[0]!.simultaneityFactor = 1.5), ["groups.0.rows.0.simultaneityFactor", "too_big"]],
|
||||||
(balance) => (balance.groups[0]!.rows[0]!.quantity = -1),
|
[(balance) => (balance.groups[0]!.rows[0]!.quantity = -1), ["groups.0.rows.0.quantity", "too_small"]],
|
||||||
(balance) => (balance.groups[0]!.rows[0]!.name = " Leerzeichen "),
|
[(balance) => (balance.groups[0]!.rows[0]!.name = " Leerzeichen "), ["groups.0.rows.0.name", "custom"]],
|
||||||
(balance) => (balance.groups[0]!.rows[0]!.supply = ""),
|
[(balance) => (balance.groups[0]!.rows[0]!.supply = ""), ["groups.0.rows.0.supply", "custom"]],
|
||||||
(balance) => (balance.groups[0]!.title = ""),
|
[(balance) => (balance.groups[0]!.title = ""), ["groups.0.title", "custom"]],
|
||||||
(balance) => (balance.dateIso = "30.06.2023"),
|
[(balance) => (balance.dateIso = "30.06.2023"), ["dateIso", "invalid_format"]],
|
||||||
(balance) => (balance.reserveAvPercent = 101),
|
[(balance) => (balance.reserveAvPercent = 101), ["reserveAvPercent", "too_big"]],
|
||||||
(balance) => (balance.groups[1]!.rows[0]!.id = balance.groups[0]!.rows[0]!.id),
|
[(balance) => (balance.groups[1]!.rows[0]!.id = balance.groups[0]!.rows[0]!.id), ["groups", "custom"]],
|
||||||
|
[
|
||||||
(balance) => ((balance.groups[0]!.rows[0] as unknown as Record<string, unknown>).extra = 1),
|
(balance) => ((balance.groups[0]!.rows[0] as unknown as Record<string, unknown>).extra = 1),
|
||||||
|
["groups.0.rows.0", "unrecognized_keys", "extra"],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
for (const mutate of invalid) {
|
for (const [mutate, expectedIssue] of invalid) {
|
||||||
const balance = structuredClone(valid);
|
const balance = structuredClone(valid);
|
||||||
mutate(balance);
|
mutate(balance);
|
||||||
assert.throws(() => parseEstimateBalanceSnapshot(balance));
|
assert.throws(() => parseEstimateBalanceSnapshot(balance), zodIssues(expectedIssue));
|
||||||
}
|
}
|
||||||
assert.equal(
|
assert.equal(
|
||||||
createEstimateBalanceRequestSchema.safeParse({
|
createEstimateBalanceRequestSchema.safeParse({
|
||||||
|
|
|
||||||
|
|
@ -253,19 +253,23 @@ describe("project building structure", () => {
|
||||||
assert.equal(getFloor(context, legacyFloor.id)?.name, "EG");
|
assert.equal(getFloor(context, legacyFloor.id)?.name, "EG");
|
||||||
|
|
||||||
// A version 1 snapshot must not carry buildingId, a version 2 one must.
|
// A version 1 snapshot must not carry buildingId, a version 2 one must.
|
||||||
assert.throws(() =>
|
assert.throws(
|
||||||
|
() =>
|
||||||
assertProjectFloorInsertProjectCommand({
|
assertProjectFloorInsertProjectCommand({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
type: "project-floor.insert",
|
type: "project-floor.insert",
|
||||||
payload: { floor: { ...legacyFloor, buildingId: null } },
|
payload: { floor: { ...legacyFloor, buildingId: null } },
|
||||||
})
|
}),
|
||||||
|
/Project-floor snapshot is invalid/
|
||||||
);
|
);
|
||||||
assert.throws(() =>
|
assert.throws(
|
||||||
|
() =>
|
||||||
assertProjectFloorInsertProjectCommand({
|
assertProjectFloorInsertProjectCommand({
|
||||||
schemaVersion: 2,
|
schemaVersion: 2,
|
||||||
type: "project-floor.insert",
|
type: "project-floor.insert",
|
||||||
payload: { floor: legacyFloor },
|
payload: { floor: legacyFloor },
|
||||||
})
|
}),
|
||||||
|
/Project-floor snapshot is invalid/
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
context.close();
|
context.close();
|
||||||
|
|
|
||||||
|
|
@ -74,20 +74,22 @@ describe("serialized project commands", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects values that JSON would lose or silently change", () => {
|
it("rejects values that JSON would lose or silently change", () => {
|
||||||
const invalidPayloads: unknown[] = [
|
const invalidPayloads: Array<[unknown, RegExp]> = [
|
||||||
{ value: undefined },
|
[{ value: undefined }, /must contain JSON values only/],
|
||||||
{ value: Number.NaN },
|
[{ value: Number.NaN }, /numbers must be finite/],
|
||||||
{ value: Number.POSITIVE_INFINITY },
|
[{ value: Number.POSITIVE_INFINITY }, /numbers must be finite/],
|
||||||
{ value: new Date() },
|
[{ value: new Date() }, /must contain plain objects only/],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const payload of invalidPayloads) {
|
for (const [payload, expectedError] of invalidPayloads) {
|
||||||
assert.throws(() =>
|
assert.throws(
|
||||||
|
() =>
|
||||||
serializeProjectCommand({
|
serializeProjectCommand({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
type: "test.invalid",
|
type: "test.invalid",
|
||||||
payload: payload as never,
|
payload: payload as never,
|
||||||
})
|
}),
|
||||||
|
expectedError
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { remapProjectState } from "../src/domain/models/project-transfer.model.j
|
||||||
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
||||||
import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js";
|
import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js";
|
||||||
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
||||||
|
import { zodIssues } from "./support/zod-assertions.js";
|
||||||
|
|
||||||
/** Removes the fields introduced by snapshot schema version 9. */
|
/** Removes the fields introduced by snapshot schema version 9. */
|
||||||
function withoutVersionNineFields<
|
function withoutVersionNineFields<
|
||||||
|
|
@ -425,11 +426,20 @@ describe("project state snapshot model", () => {
|
||||||
|
|
||||||
const mislabeled = protectedSnapshot();
|
const mislabeled = protectedSnapshot();
|
||||||
mislabeled.schemaVersion = 5 as typeof mislabeled.schemaVersion;
|
mislabeled.schemaVersion = 5 as typeof mislabeled.schemaVersion;
|
||||||
assert.throws(() => parseProjectStateSnapshot(mislabeled));
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(mislabeled),
|
||||||
|
zodIssues(
|
||||||
|
["project", "unrecognized_keys", "workStage"],
|
||||||
|
["distributionBoards.0", "unrecognized_keys", "boardNumber"]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const missingCover = structuredClone(protectedSnapshot());
|
const missingCover = structuredClone(protectedSnapshot());
|
||||||
delete (missingCover.project as Record<string, unknown>).workStage;
|
delete (missingCover.project as Record<string, unknown>).workStage;
|
||||||
assert.throws(() => parseProjectStateSnapshot(missingCover));
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(missingCover),
|
||||||
|
zodIssues(["project.workStage", "invalid_type"])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("upgrades snapshot version 4 with the existing quantity as manual share", () => {
|
it("upgrades snapshot version 4 with the existing quantity as manual share", () => {
|
||||||
|
|
@ -468,12 +478,57 @@ describe("project state snapshot model", () => {
|
||||||
it("rejects malformed baseline and unknown snapshot shapes", () => {
|
it("rejects malformed baseline and unknown snapshot shapes", () => {
|
||||||
const outdated = structuredClone(minimalSnapshot());
|
const outdated = structuredClone(minimalSnapshot());
|
||||||
delete (outdated.project as Record<string, unknown>).description;
|
delete (outdated.project as Record<string, unknown>).description;
|
||||||
assert.throws(() => parseProjectStateSnapshot(outdated));
|
assert.throws(
|
||||||
assert.throws(() =>
|
() => parseProjectStateSnapshot(outdated),
|
||||||
|
zodIssues(["project.description", "invalid_type"])
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
parseProjectStateSnapshot({
|
parseProjectStateSnapshot({
|
||||||
...minimalSnapshot(),
|
...minimalSnapshot(),
|
||||||
schemaVersion: projectStateSnapshotSchemaVersion + 1,
|
schemaVersion: projectStateSnapshotSchemaVersion + 1,
|
||||||
})
|
}),
|
||||||
|
zodIssues(["schemaVersion", "invalid_value"])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid structure references", () => {
|
||||||
|
const boardFloor = protectedSnapshot();
|
||||||
|
(boardFloor.distributionBoards[0] as { floorId: string | null }).floorId =
|
||||||
|
"missing";
|
||||||
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(boardFloor),
|
||||||
|
/distribution board floor reference is invalid/
|
||||||
|
);
|
||||||
|
|
||||||
|
const roomFloor = protectedSnapshot();
|
||||||
|
roomFloor.rooms.push({
|
||||||
|
id: "room-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
floorId: "missing",
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
areaM2: null,
|
||||||
|
});
|
||||||
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(roomFloor),
|
||||||
|
/room floor reference is invalid/
|
||||||
|
);
|
||||||
|
|
||||||
|
const circuitSection = protectedSnapshot();
|
||||||
|
circuitSection.circuits[0]!.sectionId = "missing";
|
||||||
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(circuitSection),
|
||||||
|
/circuit section reference is invalid/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects circuits whose reserve state contradicts their device rows", () => {
|
||||||
|
const withoutRows = protectedSnapshot();
|
||||||
|
withoutRows.circuits[0]!.isReserve = false;
|
||||||
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(withoutRows),
|
||||||
|
/reserve state must match its device rows/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -580,7 +635,10 @@ describe("project state snapshot model", () => {
|
||||||
const floorWithoutBuildingField = structuredClone(snapshot);
|
const floorWithoutBuildingField = structuredClone(snapshot);
|
||||||
delete (floorWithoutBuildingField.floors[1] as Record<string, unknown>)
|
delete (floorWithoutBuildingField.floors[1] as Record<string, unknown>)
|
||||||
.buildingId;
|
.buildingId;
|
||||||
assert.throws(() => parseProjectStateSnapshot(floorWithoutBuildingField));
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(floorWithoutBuildingField),
|
||||||
|
zodIssues(["floors.1.buildingId", "invalid_type"])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps room areas and upgrades snapshot version 7 with an empty area", () => {
|
it("keeps room areas and upgrades snapshot version 7 with an empty area", () => {
|
||||||
|
|
@ -633,7 +691,13 @@ describe("project state snapshot model", () => {
|
||||||
|
|
||||||
const mislabeled = structuredClone(snapshot);
|
const mislabeled = structuredClone(snapshot);
|
||||||
mislabeled.schemaVersion = 6 as typeof mislabeled.schemaVersion;
|
mislabeled.schemaVersion = 6 as typeof mislabeled.schemaVersion;
|
||||||
assert.throws(() => parseProjectStateSnapshot(mislabeled));
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(mislabeled),
|
||||||
|
zodIssues(
|
||||||
|
["", "unrecognized_keys", "buildings"],
|
||||||
|
["floors.0", "unrecognized_keys", "buildingId"]
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -654,18 +718,26 @@ describe("named project snapshot request", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects stale-shaped and excessive input", () => {
|
it("rejects stale-shaped and excessive input", () => {
|
||||||
assert.throws(() =>
|
assert.throws(
|
||||||
|
() =>
|
||||||
createNamedProjectSnapshotSchema.parse({
|
createNamedProjectSnapshotSchema.parse({
|
||||||
expectedRevision: -1,
|
expectedRevision: -1,
|
||||||
name: "",
|
name: "",
|
||||||
unexpected: true,
|
unexpected: true,
|
||||||
})
|
}),
|
||||||
|
zodIssues(
|
||||||
|
["expectedRevision", "too_small"],
|
||||||
|
["name", "too_small"],
|
||||||
|
["", "unrecognized_keys", "unexpected"]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert.throws(() =>
|
assert.throws(
|
||||||
|
() =>
|
||||||
createNamedProjectSnapshotSchema.parse({
|
createNamedProjectSnapshotSchema.parse({
|
||||||
expectedRevision: 0,
|
expectedRevision: 0,
|
||||||
name: "x".repeat(201),
|
name: "x".repeat(201),
|
||||||
})
|
}),
|
||||||
|
zodIssues(["name", "too_big"])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
28
tests/support/zod-assertions.ts
Normal file
28
tests/support/zod-assertions.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
/** One expected Zod issue: dotted path ("" for the root), issue code and, for unrecognized keys, the key. */
|
||||||
|
export type ExpectedZodIssue = [path: string, code: string, key?: string];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation function for `assert.throws` that requires a ZodError reporting
|
||||||
|
* every expected issue. Any other error, such as a TypeError from a broken
|
||||||
|
* fixture, fails the assertion instead of passing as "some error".
|
||||||
|
*/
|
||||||
|
export function zodIssues(...expected: ExpectedZodIssue[]) {
|
||||||
|
return (error: unknown) => {
|
||||||
|
assert.ok(error instanceof ZodError, `expected ZodError, got ${String(error)}`);
|
||||||
|
for (const [path, code, key] of expected) {
|
||||||
|
assert.ok(
|
||||||
|
error.issues.some(
|
||||||
|
(issue) =>
|
||||||
|
issue.path.join(".") === path &&
|
||||||
|
issue.code === code &&
|
||||||
|
(key === undefined || ("keys" in issue && issue.keys.includes(key)))
|
||||||
|
),
|
||||||
|
`missing ${code}${key ? ` (${key})` : ""} at "${path}": ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue