import { PgDialect } from "drizzle-orm/pg-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; const authorizeTicket = vi.fn(); const notifyCommission = vi.fn(); const deleteObject = vi.fn(); const deleteReturning = vi.fn(); const deleteWhere = vi.fn(); const updateWhere = vi.fn(); const tx = { delete: vi.fn(() => ({ where: deleteWhere })), update: vi.fn(() => ({ set: () => ({ where: updateWhere }) })), }; vi.mock("@/db", () => ({ getDb: () => ({ transaction: async (run: (value: typeof tx) => Promise) => run(tx) }) })); vi.mock("@/lib/commission/tickets", () => ({ authorizeTicket })); vi.mock("@/lib/commission/server", () => ({ notifyCommission })); vi.mock("@/lib/media/storage", () => ({ getMediaStorage: async () => ({ delete: deleteObject }) })); vi.mock("@/lib/security/rate-limit", () => ({ limitRequest: async () => undefined })); const { DELETE } = await import("./route"); const ticketId = "11111111-1111-4111-8111-111111111111"; const messageId = "22222222-2222-4222-8222-222222222222"; const ownerId = "owner-1"; function deletionRequest(origin = "https://guide.sudloh.com") { return new Request(`https://guide.sudloh.com/api/commission/tickets/${ticketId}/messages/${messageId}`, { method: "DELETE", headers: { Origin: origin } }); } function context() { return { params: Promise.resolve({ id: ticketId, messageId }) } as RouteContext< "/api/commission/tickets/[id]/messages/[messageId]">; } describe("commission message deletion", () => { beforeEach(() => { process.env.BETTER_AUTH_URL = "https://guide.sudloh.com"; vi.clearAllMocks(); authorizeTicket.mockResolvedValue({ ticket: { userId: ownerId, status: "open" }, user: { id: ownerId } }); deleteWhere.mockImplementation(() => ({ returning: deleteReturning })); deleteReturning.mockResolvedValue([{ imageObjectKey: "commission/messages/image-1", imageObjectKeys: ["commission/messages/image-2", "commission/messages/image-3"] }]); updateWhere.mockResolvedValue(undefined); deleteObject.mockResolvedValue(undefined); notifyCommission.mockResolvedValue(undefined); }); it("rejects cross-origin requests before ticket authorization", async () => { const response = await DELETE(deletionRequest("https://example.com"), context()); expect(response.status).toBe(403); expect(authorizeTicket).not.toHaveBeenCalled(); }); it("deletes only a message owned by the current user in this ticket", async () => { const response = await DELETE(deletionRequest(), context()); expect(response.status).toBe(204); const condition = deleteWhere.mock.calls[0][0]; const query = new PgDialect().sqlToQuery(condition); expect(query.sql).toContain('"ticket_id"'); expect(query.sql).toContain('"author_id"'); expect(query.params).toContain(ticketId); expect(query.params).toContain(messageId); expect(query.params).toContain(ownerId); expect(deleteObject).toHaveBeenCalledWith("commission/messages/image-1"); expect(deleteObject).toHaveBeenCalledWith("commission/messages/image-2"); expect(deleteObject).toHaveBeenCalledWith("commission/messages/image-3"); expect(notifyCommission).toHaveBeenCalledWith(ticketId, ownerId); }); it("rejects deletion when the ticket is closed", async () => { authorizeTicket.mockResolvedValue({ ticket: { userId: ownerId, status: "closed" }, user: { id: ownerId } }); const response = await DELETE(deletionRequest(), context()); expect(response.status).toBe(409); expect(tx.delete).not.toHaveBeenCalled(); expect(deleteObject).not.toHaveBeenCalled(); expect(notifyCommission).not.toHaveBeenCalled(); }); it("leaves storage and notifications untouched when no owned message exists", async () => { deleteReturning.mockResolvedValue([]); const response = await DELETE(deletionRequest(), context()); expect(response.status).toBe(404); expect(deleteObject).not.toHaveBeenCalled(); expect(notifyCommission).not.toHaveBeenCalled(); }); });