Files
diegosouzapw--omniroute/tests/unit/api/combo-002-first-field.test.ts
2026-07-13 13:39:12 +08:00

110 lines
4.2 KiB
TypeScript

/**
* TDD regression guard for #5083 — Bug 3:
* COMBO_002 validation errors only surface the generic
* "One or more combo fields are invalid" message; the field-level reason
* is buried inside error.details.issues.details[*].
*
* Fix: extract the first issue from error.details and add
* error.details.firstField / error.details.firstMessage to the response body.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { buildComboErrorBody } from "@/lib/api/comboErrorResponse";
/**
* Simulate what the PUT /api/combos/[id] route passes as `details`
* for a COMBO_002 response after the fix.
* The fix extracts the first Zod issue from validation.error.details
* and adds firstField / firstMessage to the payload.
*/
describe("COMBO_002 response — firstField / firstMessage surfacing (#5083 Bug 3)", () => {
it("details payload exposes firstField when provided", () => {
// Simulate the fixed route calling comboErrorResponse with firstField/firstMessage
const details = {
issues: { message: "Invalid request", details: [{ field: "name", message: "Required" }] },
firstField: "name",
firstMessage: "Required",
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.code, "COMBO_002");
assert.equal(body.error.details.firstField, "name");
assert.equal(body.error.details.firstMessage, "Required");
});
it("details payload exposes firstField for nested path (e.g. models.0.id)", () => {
const details = {
issues: {
message: "Invalid request",
details: [{ field: "models.0.id", message: "String must contain at least 1 character(s)" }],
},
firstField: "models.0.id",
firstMessage: "String must contain at least 1 character(s)",
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.details.firstField, "models.0.id");
assert.equal(body.error.details.firstMessage, "String must contain at least 1 character(s)");
});
it("details payload has firstField=null when no issues are present", () => {
// Edge case: empty issues list
const details = {
issues: { message: "Invalid request", details: [] },
firstField: null,
firstMessage: null,
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.details.firstField, null);
assert.equal(body.error.details.firstMessage, null);
});
it("the generic message key is still present (backward compat)", () => {
const details = {
issues: { message: "Invalid request", details: [{ field: "strategy", message: "Invalid enum value" }] },
firstField: "strategy",
firstMessage: "Invalid enum value",
};
const body = buildComboErrorBody("COMBO_002", details);
// The top-level error.message is from the error code catalog, not the issue message
assert.equal(body.error.message, "One or more combo fields are invalid");
});
});
/**
* This helper simulates the logic that the fixed route.ts uses to extract
* firstField/firstMessage from validateBody's error payload.
* It exercises the extraction logic independently of the heavy route harness.
*/
describe("COMBO_002 firstField extraction logic", () => {
function extractFirstField(validationError: {
message: string;
details: Array<{ field: string; message: string }>;
}): { firstField: string | null; firstMessage: string | null } {
const first = validationError.details?.[0] ?? null;
return {
firstField: first?.field ?? null,
firstMessage: first?.message ?? null,
};
}
it("extracts the first field from a non-empty details array", () => {
const error = {
message: "Invalid request",
details: [
{ field: "name", message: "Required" },
{ field: "strategy", message: "Invalid enum value" },
],
};
const { firstField, firstMessage } = extractFirstField(error);
assert.equal(firstField, "name");
assert.equal(firstMessage, "Required");
});
it("returns null/null for an empty details array", () => {
const error = { message: "Invalid request", details: [] };
const { firstField, firstMessage } = extractFirstField(error);
assert.equal(firstField, null);
assert.equal(firstMessage, null);
});
});