diff --git a/src/server/routes/threads.ts b/src/server/routes/threads.ts index 355fa03..cfa2405 100644 --- a/src/server/routes/threads.ts +++ b/src/server/routes/threads.ts @@ -5,6 +5,7 @@ import { Hono } from "hono"; import { createCandidateSchema, createThreadSchema, + reorderCandidatesSchema, resolveThreadSchema, updateCandidateSchema, updateThreadSchema, @@ -16,6 +17,7 @@ import { deleteThread, getAllThreads, getThreadWithCandidates, + reorderCandidates, resolveThread, updateCandidate, updateThread, @@ -122,6 +124,21 @@ app.delete("/:threadId/candidates/:candidateId", async (c) => { return c.json({ success: true }); }); +// Candidate reorder + +app.patch( + "/:id/candidates/reorder", + zValidator("json", reorderCandidatesSchema), + (c) => { + const db = c.get("db"); + const threadId = Number(c.req.param("id")); + const { orderedIds } = c.req.valid("json"); + const result = reorderCandidates(db, threadId, orderedIds); + if (!result.success) return c.json({ error: result.error }, 400); + return c.json({ success: true }); + }, +); + // Resolution app.post("/:id/resolve", zValidator("json", resolveThreadSchema), (c) => { diff --git a/tests/routes/threads.test.ts b/tests/routes/threads.test.ts index 69dd782..7c2feda 100644 --- a/tests/routes/threads.test.ts +++ b/tests/routes/threads.test.ts @@ -234,6 +234,119 @@ describe("Thread Routes", () => { }); }); + describe("PATCH /api/threads/:id/candidates/reorder", () => { + it("with valid orderedIds returns 200 + { success: true }", async () => { + const thread = await createThreadViaAPI(app, "Reorder Test"); + const c1 = await createCandidateViaAPI(app, thread.id, { + name: "Candidate A", + categoryId: 1, + }); + const c2 = await createCandidateViaAPI(app, thread.id, { + name: "Candidate B", + categoryId: 1, + }); + + const res = await app.request( + `/api/threads/${thread.id}/candidates/reorder`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ orderedIds: [c2.id, c1.id] }), + }, + ); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.success).toBe(true); + }); + + it("after PATCH reorder, GET thread returns candidates in the new order", async () => { + const thread = await createThreadViaAPI(app, "Order Verify"); + const c1 = await createCandidateViaAPI(app, thread.id, { + name: "First", + categoryId: 1, + }); + const c2 = await createCandidateViaAPI(app, thread.id, { + name: "Second", + categoryId: 1, + }); + const c3 = await createCandidateViaAPI(app, thread.id, { + name: "Third", + categoryId: 1, + }); + + // Reverse the order + await app.request(`/api/threads/${thread.id}/candidates/reorder`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ orderedIds: [c3.id, c2.id, c1.id] }), + }); + + const res = await app.request(`/api/threads/${thread.id}`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.candidates[0].id).toBe(c3.id); + expect(body.candidates[1].id).toBe(c2.id); + expect(body.candidates[2].id).toBe(c1.id); + }); + + it("on a resolved thread returns 400", async () => { + const thread = await createThreadViaAPI(app, "Resolved Thread"); + const candidate = await createCandidateViaAPI(app, thread.id, { + name: "Winner", + categoryId: 1, + }); + + // Resolve the thread first + await app.request(`/api/threads/${thread.id}/resolve`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ candidateId: candidate.id }), + }); + + const res = await app.request( + `/api/threads/${thread.id}/candidates/reorder`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ orderedIds: [candidate.id] }), + }, + ); + + expect(res.status).toBe(400); + }); + + it("with invalid body (empty orderedIds) returns 400", async () => { + const thread = await createThreadViaAPI(app, "Invalid Body"); + + const res = await app.request( + `/api/threads/${thread.id}/candidates/reorder`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ orderedIds: [] }), + }, + ); + + expect(res.status).toBe(400); + }); + + it("with missing orderedIds field returns 400", async () => { + const thread = await createThreadViaAPI(app, "Missing Field"); + + const res = await app.request( + `/api/threads/${thread.id}/candidates/reorder`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }, + ); + + expect(res.status).toBe(400); + }); + }); + describe("POST /api/threads/:id/resolve", () => { it("with valid candidateId returns 200 + created item", async () => { const thread = await createThreadViaAPI(app, "Tent Decision");