diff --git a/src/app/api/testimonials/[id]/route.test.ts b/src/app/api/testimonials/[id]/route.test.ts new file mode 100644 index 00000000..dd53f3ac --- /dev/null +++ b/src/app/api/testimonials/[id]/route.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { NextRequest } from "next/server"; +import { PATCH } from "./route"; + +const mocks = vi.hoisted(() => ({ + mockGetAuthContext: vi.fn(), + mockFrom: vi.fn(), + mockUpdate: vi.fn(), +})); + +vi.mock("@/lib/auth/get-user", () => ({ + getAuthContext: mocks.mockGetAuthContext, +})); + +vi.mock("@/lib/supabase/service", () => ({ + createServiceClient: () => ({ + from: mocks.mockFrom, + }), +})); + +function makeRequest(rating: number) { + return new NextRequest("http://localhost/api/testimonials/testimonial-1", { + method: "PATCH", + body: JSON.stringify({ rating }), + headers: { "Content-Type": "application/json" }, + }); +} + +describe("PATCH /api/testimonials/[id]", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.mockGetAuthContext.mockResolvedValue({ + user: { id: "author-1" }, + supabase: {}, + }); + mocks.mockFrom.mockReturnValue({ + select: () => ({ + eq: () => ({ + single: () => + Promise.resolve({ + data: { + id: "testimonial-1", + profile_id: "profile-1", + gig_id: null, + author_id: "author-1", + }, + error: null, + }), + }), + }), + update: mocks.mockUpdate, + }); + }); + + it("rejects fractional ratings before updating the testimonial", async () => { + const response = await PATCH(makeRequest(4.5), { + params: Promise.resolve({ id: "testimonial-1" }), + }); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe("Rating must be an integer from 1-5"); + expect(mocks.mockUpdate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/api/testimonials/[id]/route.ts b/src/app/api/testimonials/[id]/route.ts index 4d77e1a8..6194bfcb 100644 --- a/src/app/api/testimonials/[id]/route.ts +++ b/src/app/api/testimonials/[id]/route.ts @@ -54,8 +54,8 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { updateData.content = content.trim(); } if (rating !== undefined) { - if (typeof rating !== "number" || rating < 1 || rating > 5) { - return NextResponse.json({ error: "Rating must be 1-5" }, { status: 400 }); + if (!Number.isInteger(rating) || rating < 1 || rating > 5) { + return NextResponse.json({ error: "Rating must be an integer from 1-5" }, { status: 400 }); } updateData.rating = rating; } diff --git a/src/app/api/testimonials/route.rating.test.ts b/src/app/api/testimonials/route.rating.test.ts new file mode 100644 index 00000000..2de2bd03 --- /dev/null +++ b/src/app/api/testimonials/route.rating.test.ts @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { NextRequest } from "next/server"; +import { POST } from "./route"; + +const mocks = vi.hoisted(() => ({ + mockGetAuthContext: vi.fn(), + mockCreateServiceClient: vi.fn(), +})); + +vi.mock("@/lib/auth/get-user", () => ({ + getAuthContext: mocks.mockGetAuthContext, +})); + +vi.mock("@/lib/supabase/service", () => ({ + createServiceClient: mocks.mockCreateServiceClient, +})); + +vi.mock("@/lib/email", () => ({ + sendEmail: vi.fn(), +})); + +describe("POST /api/testimonials rating validation", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.mockGetAuthContext.mockResolvedValue({ + user: { id: "author-1" }, + supabase: {}, + }); + }); + + it("rejects fractional ratings before creating a service client", async () => { + const request = new NextRequest("http://localhost/api/testimonials", { + method: "POST", + body: JSON.stringify({ + profile_id: "profile-1", + rating: 4.5, + content: "Consistent and helpful work.", + }), + headers: { "Content-Type": "application/json" }, + }); + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe("Rating must be an integer from 1-5"); + expect(mocks.mockCreateServiceClient).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/api/testimonials/route.ts b/src/app/api/testimonials/route.ts index 9303a9dd..c275e65e 100644 --- a/src/app/api/testimonials/route.ts +++ b/src/app/api/testimonials/route.ts @@ -90,9 +90,9 @@ export async function POST(request: NextRequest) { ); } - if (typeof rating !== "number" || rating < 1 || rating > 5) { + if (!Number.isInteger(rating) || rating < 1 || rating > 5) { return NextResponse.json( - { error: "Rating must be between 1 and 5" }, + { error: "Rating must be an integer from 1-5" }, { status: 400 } ); }