diff --git a/src/actions/polls.test.ts b/src/actions/polls.test.ts new file mode 100644 index 00000000..d2e424ab --- /dev/null +++ b/src/actions/polls.test.ts @@ -0,0 +1,122 @@ +// @ts-nocheck +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { selectLimit, updateSet, updateWhere } = vi.hoisted(() => ({ + selectLimit: vi.fn(), + updateSet: vi.fn(() => ({ where: updateWhere })), + updateWhere: vi.fn(), +})); + +vi.mock("@/lib/db", () => ({ + db: { + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ limit: selectLimit })), + })), + })), + update: vi.fn(() => ({ set: updateSet })), + }, + WebsitePoll: { + id: "id", + title: "title", + description: "description", + status: "status", + showResults: "showResults", + multipleChoice: "multipleChoice", + startsAt: "startsAt", + endsAt: "endsAt", + }, + WebsitePollQuestion: { + id: "id", + pollId: "pollId", + question: "question", + type: "type", + sortOrder: "sortOrder", + options: "options", + }, + WebsitePollVote: { id: "id", questionId: "questionId", userId: "userId" }, +})); + +vi.mock("@/lib/permissions", () => ({ PERMS: { POLLS_EDIT: "polls.edit" } })); +vi.mock("@/lib/services/audit", () => ({ logAudit: vi.fn() })); +vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); +vi.mock("@/lib/safe-action", () => ({ + adminAction: (options, handler) => async (input) => { + const parsed = options.schema.safeParse(input); + if (!parsed.success) return { ok: false, error: "Validation failed" }; + try { + return await handler({ + data: parsed.data, + session: { user: { id: "1" } }, + }); + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : "Internal server error", + }; + } + }, + authAction: vi.fn(), +})); + +vi.mock("@/lib/safe-action-shared", () => ({ + actionOk: (data) => ({ ok: true, data: data ?? {} }), + actionError: (error) => ({ ok: false, error }), + ActionError: class ActionError extends Error { + constructor(message) { + super(message); + this.name = "ActionError"; + } + }, +})); + +import { updatePoll, updatePollQuestion } from "./polls"; + +beforeEach(() => { + vi.clearAllMocks(); + selectLimit.mockReset(); + updateSet.mockClear(); + updateWhere.mockClear(); +}); + +describe("poll update merge validation", () => { + it("rejects a partial end time before the persisted start without updating", async () => { + selectLimit.mockResolvedValueOnce([ + { + id: 1, + title: "Schedule", + description: null, + status: "draft", + showResults: 1, + multipleChoice: 0, + startsAt: new Date("2026-09-02T12:00:00.000Z"), + endsAt: null, + }, + ]); + + const result = await updatePoll({ + id: 1, + endsAt: "2026-09-02T11:59:00.000Z", + }); + + expect(result).toEqual({ ok: false, error: "Invalid poll schedule" }); + expect(updateSet).not.toHaveBeenCalled(); + }); + + it("rejects a partial options patch for a persisted text question without updating", async () => { + selectLimit.mockResolvedValueOnce([ + { + pollId: 1, + question: "Why?", + type: "text", + sortOrder: 0, + options: "", + }, + ]); + + const result = await updatePollQuestion({ id: 1, options: "Not allowed" }); + + expect(result).toEqual({ ok: false, error: "Invalid poll question" }); + expect(updateSet).not.toHaveBeenCalled(); + }); +});