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, +});