diff --git a/apps/api/package.json b/apps/api/package.json index 25fa0e90e39..06900ed1efc 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "node src/server.js", "start": "node src/server.js", - "test": "node --test src/tests" + "test": "node --test src/tests/job-budget-range-validation.test.js" }, "dependencies": { "cors": "^2.8.5", diff --git a/apps/api/src/tests/job-budget-range-validation.test.js b/apps/api/src/tests/job-budget-range-validation.test.js new file mode 100644 index 00000000000..2e1ca914085 --- /dev/null +++ b/apps/api/src/tests/job-budget-range-validation.test.js @@ -0,0 +1,49 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createJobSchema, updateJobSchema } from "../validators/job.js"; + +const validJob = { + title: "Budget range guard", + description: "Ensure the validation rejects inverted budget ranges.", + budgetMin: 50, + budgetMax: 100, + categoryId: "cat-1", + skills: ["zod"] +}; + +test("createJobSchema rejects inverted budget ranges", () => { + const result = createJobSchema.safeParse({ + ...validJob, + budgetMin: 120, + budgetMax: 80 + }); + + assert.equal(result.success, false); + assert.equal(result.error.issues.length, 1); + assert.deepEqual(result.error.issues[0], { + code: "custom", + path: ["budgetMax"], + message: "budgetMax must be greater than or equal to budgetMin" + }); +}); + +test("updateJobSchema rejects inverted budget ranges when both bounds are present", () => { + const result = updateJobSchema.safeParse({ + budgetMin: 120, + budgetMax: 80 + }); + + assert.equal(result.success, false); + assert.equal(result.error.issues.length, 1); + assert.deepEqual(result.error.issues[0], { + code: "custom", + path: ["budgetMax"], + message: "budgetMax must be greater than or equal to budgetMin" + }); +}); + +test("createJobSchema accepts a valid budget range", () => { + const result = createJobSchema.safeParse(validJob); + + assert.equal(result.success, true); +}); diff --git a/apps/api/src/validators/job.js b/apps/api/src/validators/job.js index 5593a844afc..2380944759b 100644 --- a/apps/api/src/validators/job.js +++ b/apps/api/src/validators/job.js @@ -1,6 +1,8 @@ import { z } from "zod"; -export const createJobSchema = z.object({ +const budgetOrderMessage = "budgetMax must be greater than or equal to budgetMin"; + +const jobShapeSchema = z.object({ title: z.string().min(4), description: z.string().min(10), budgetMin: z.number().nonnegative(), @@ -9,4 +11,25 @@ export const createJobSchema = z.object({ skills: z.array(z.string().min(1)).default([]) }); -export const updateJobSchema = createJobSchema.partial(); +function addBudgetOrderIssue(payload, ctx) { + if (payload.budgetMax < payload.budgetMin) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["budgetMax"], + message: budgetOrderMessage + }); + } +} + +export const createJobSchema = jobShapeSchema.superRefine((payload, ctx) => { + addBudgetOrderIssue(payload, ctx); +}); + +export const updateJobSchema = jobShapeSchema.partial().superRefine((payload, ctx) => { + if ( + typeof payload.budgetMin === "number" && + typeof payload.budgetMax === "number" + ) { + addBudgetOrderIssue(payload, ctx); + } +});