From d32dd29ad0e96ba743428ab92609b3511420cf89 Mon Sep 17 00:00:00 2001 From: AlvaroWhiteRD <3089marzo@gmail.com> Date: Mon, 1 Jun 2026 01:22:34 -0400 Subject: [PATCH] fix: reject inverted budget ranges in createJobSchema - Extract base schema to allow refinement on both create and update schemas - Add refine check ensuring budgetMax >= budgetMin when both fields present - updateJobSchema (partial) also validates range when both budget fields provided - Add 8 regression tests covering valid ranges, inverted ranges, equal values, partial updates, and missing fields Closes #2853 --- apps/api/src/tests/job.test.js | 56 ++++++++++++++++++++++++++++++++++ apps/api/src/validators/job.js | 25 +++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/tests/job.test.js diff --git a/apps/api/src/tests/job.test.js b/apps/api/src/tests/job.test.js new file mode 100644 index 0000000000..d718562be4 --- /dev/null +++ b/apps/api/src/tests/job.test.js @@ -0,0 +1,56 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createJobSchema, updateJobSchema } from "../validators/job.js"; + +const validJob = { + title: "Build a website", + description: "We need a responsive website built", + budgetMin: 100, + budgetMax: 500, + categoryId: "cat_1", + skills: ["javascript", "react"], +}; + +test("createJobSchema accepts valid budget range", () => { + const result = createJobSchema.safeParse(validJob); + assert.equal(result.success, true); +}); + +test("createJobSchema accepts equal budgetMin and budgetMax", () => { + const result = createJobSchema.safeParse({ ...validJob, budgetMin: 300, budgetMax: 300 }); + assert.equal(result.success, true); +}); + +test("createJobSchema rejects inverted budget range", () => { + const result = createJobSchema.safeParse({ ...validJob, budgetMin: 500, budgetMax: 100 }); + assert.equal(result.success, false); + assert.ok(result.error.issues.some((i) => i.message.includes("budgetMax"))); +}); + +test("createJobSchema rejects zero budgetMax with positive budgetMin", () => { + const result = createJobSchema.safeParse({ ...validJob, budgetMin: 100, budgetMax: 0 }); + assert.equal(result.success, false); +}); + +test("updateJobSchema accepts partial update with valid range", () => { + const result = updateJobSchema.safeParse({ budgetMin: 100, budgetMax: 500 }); + assert.equal(result.success, true); +}); + +test("updateJobSchema rejects inverted range when both fields present", () => { + const result = updateJobSchema.safeParse({ budgetMin: 500, budgetMax: 100 }); + assert.equal(result.success, false); +}); + +test("updateJobSchema accepts single budget field without the other", () => { + const result = updateJobSchema.safeParse({ budgetMin: 100 }); + assert.equal(result.success, true); + + const result2 = updateJobSchema.safeParse({ budgetMax: 500 }); + assert.equal(result2.success, true); +}); + +test("createJobSchema rejects missing required fields", () => { + const result = createJobSchema.safeParse({ title: "Hi" }); + assert.equal(result.success, false); +}); diff --git a/apps/api/src/validators/job.js b/apps/api/src/validators/job.js index 5593a844af..66bc556054 100644 --- a/apps/api/src/validators/job.js +++ b/apps/api/src/validators/job.js @@ -1,12 +1,31 @@ import { z } from "zod"; -export const createJobSchema = z.object({ +const jobBaseSchema = z.object({ title: z.string().min(4), description: z.string().min(10), budgetMin: z.number().nonnegative(), budgetMax: z.number().nonnegative(), categoryId: z.string().min(1), - skills: z.array(z.string().min(1)).default([]) + skills: z.array(z.string().min(1)).default([]), }); -export const updateJobSchema = createJobSchema.partial(); +const budgetRangeCheck = { + refine: (data) => { + if (data.budgetMin !== undefined && data.budgetMax !== undefined) { + return data.budgetMax >= data.budgetMin; + } + return true; + }, + message: "budgetMax must be greater than or equal to budgetMin", + path: ["budgetMax"], +}; + +export const createJobSchema = jobBaseSchema.refine(budgetRangeCheck.refine, { + message: budgetRangeCheck.message, + path: budgetRangeCheck.path, +}); + +export const updateJobSchema = jobBaseSchema.partial().refine(budgetRangeCheck.refine, { + message: budgetRangeCheck.message, + path: budgetRangeCheck.path, +});