import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("~/lib/cloudinary.server", () => ({ verifyCloudinaryWebhookSignature: vi.fn(), deleteCloudinaryImageByUrl: vi.fn().mockResolvedValue(undefined), })); vi.mock("~/models/user", () => ({ findUserById: vi.fn(), updateUser: vi.fn(), })); vi.mock("~/models/team", () => ({ findTeamById: vi.fn(), updateTeam: vi.fn(), })); vi.mock("~/lib/logger", () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() }, })); import { action } from "../api.webhooks.cloudinary"; import { verifyCloudinaryWebhookSignature, deleteCloudinaryImageByUrl, } from "~/lib/cloudinary.server"; import { findUserById, updateUser } from "~/models/user"; import { findTeamById, updateTeam } from "~/models/team"; function makeRequest(body: object) { return new Request("http://test/api/webhooks/cloudinary", { method: "POST", body: JSON.stringify(body), headers: { "Content-Type": "application/json", "X-Cld-Signature": "sig", "X-Cld-Timestamp": "1234567890", }, }); } function approvedUserPayload(overrides: object = {}) { return { secure_url: "https://res.cloudinary.com/demo/image/upload/v1/avatars/user-1.jpg", moderation_status: "approved", context: { avatar_target: "user", entity_id: "user-abc" }, ...overrides, }; } function approvedTeamPayload(overrides: object = {}) { return { secure_url: "https://res.cloudinary.com/demo/image/upload/v1/avatars/team-1.jpg", moderation_status: "approved", context: { avatar_target: "team", entity_id: "team-xyz" }, ...overrides, }; } beforeEach(() => { vi.clearAllMocks(); vi.mocked(verifyCloudinaryWebhookSignature).mockReturnValue(true); vi.mocked(updateUser).mockResolvedValue({} as never); vi.mocked(updateTeam).mockResolvedValue({} as never); vi.mocked(findUserById).mockResolvedValue(null as never); vi.mocked(findTeamById).mockResolvedValue(null as never); }); describe("Cloudinary webhook — signature", () => { it("returns 401 when signature is invalid", async () => { vi.mocked(verifyCloudinaryWebhookSignature).mockReturnValue(false); const res = await action({ request: makeRequest(approvedUserPayload()), params: {}, context: {} } as never); expect(res.status).toBe(401); }); }); describe("Cloudinary webhook — moderation", () => { it("returns ignored when moderation_status is rejected", async () => { const res = await action({ request: makeRequest({ ...approvedUserPayload(), moderation_status: "rejected" }), params: {}, context: {}, } as never); const body = await res.json(); expect(body).toEqual({ status: "ignored" }); }); it("returns ignored when moderation_status is pending", async () => { const res = await action({ request: makeRequest({ ...approvedUserPayload(), moderation_status: "pending" }), params: {}, context: {}, } as never); const body = await res.json(); expect(body).toEqual({ status: "ignored" }); }); it("reads approval from nested moderation array", async () => { vi.mocked(findUserById).mockResolvedValue({ customAvatarUrl: null } as never); const payload = { secure_url: "https://res.cloudinary.com/demo/image/upload/v1/avatars/user-1.jpg", moderation: [{ status: "approved" }], context: { avatar_target: "user", entity_id: "user-abc" }, }; const res = await action({ request: makeRequest(payload), params: {}, context: {} } as never); const body = await res.json(); expect(body).toEqual({ status: "updated" }); }); }); describe("Cloudinary webhook — missing fields", () => { it("returns 400 when secure_url is absent", async () => { const res = await action({ request: makeRequest({ moderation_status: "approved", context: { avatar_target: "user", entity_id: "u1" } }), params: {}, context: {}, } as never); expect(res.status).toBe(400); }); it("returns 400 when context is absent", async () => { const res = await action({ request: makeRequest({ moderation_status: "approved", secure_url: "https://res.cloudinary.com/demo/image/upload/v1/avatars/x.jpg" }), params: {}, context: {}, } as never); expect(res.status).toBe(400); }); it("returns 400 for unknown avatar_target", async () => { const res = await action({ request: makeRequest({ ...approvedUserPayload(), context: { avatar_target: "league", entity_id: "l1" } }), params: {}, context: {}, } as never); expect(res.status).toBe(400); }); }); describe("Cloudinary webhook — user update", () => { it("sets customAvatarUrl and avatarType on the user", async () => { vi.mocked(findUserById).mockResolvedValue({ customAvatarUrl: null } as never); await action({ request: makeRequest(approvedUserPayload()), params: {}, context: {} } as never); expect(updateUser).toHaveBeenCalledWith("user-abc", { customAvatarUrl: "https://res.cloudinary.com/demo/image/upload/v1/avatars/user-1.jpg", avatarType: "uploaded", }); }); it("deletes the previous avatar when one exists", async () => { const oldUrl = "https://res.cloudinary.com/demo/image/upload/v1/avatars/user-old.jpg"; vi.mocked(findUserById).mockResolvedValue({ customAvatarUrl: oldUrl } as never); await action({ request: makeRequest(approvedUserPayload()), params: {}, context: {} } as never); expect(deleteCloudinaryImageByUrl).toHaveBeenCalledWith(oldUrl); }); it("does not call delete when there is no previous avatar", async () => { vi.mocked(findUserById).mockResolvedValue({ customAvatarUrl: null } as never); await action({ request: makeRequest(approvedUserPayload()), params: {}, context: {} } as never); expect(deleteCloudinaryImageByUrl).not.toHaveBeenCalled(); }); it("does not delete when previous URL matches new URL", async () => { const url = "https://res.cloudinary.com/demo/image/upload/v1/avatars/user-1.jpg"; vi.mocked(findUserById).mockResolvedValue({ customAvatarUrl: url } as never); await action({ request: makeRequest(approvedUserPayload({ secure_url: url })), params: {}, context: {} } as never); expect(deleteCloudinaryImageByUrl).not.toHaveBeenCalled(); }); }); describe("Cloudinary webhook — team update", () => { it("sets logoUrl and avatarType on the team", async () => { vi.mocked(findTeamById).mockResolvedValue({ logoUrl: null } as never); await action({ request: makeRequest(approvedTeamPayload()), params: {}, context: {} } as never); expect(updateTeam).toHaveBeenCalledWith("team-xyz", { logoUrl: "https://res.cloudinary.com/demo/image/upload/v1/avatars/team-1.jpg", avatarType: "uploaded", }); }); it("deletes the previous team logo when one exists", async () => { const oldUrl = "https://res.cloudinary.com/demo/image/upload/v1/avatars/team-old.jpg"; vi.mocked(findTeamById).mockResolvedValue({ logoUrl: oldUrl } as never); await action({ request: makeRequest(approvedTeamPayload()), params: {}, context: {} } as never); expect(deleteCloudinaryImageByUrl).toHaveBeenCalledWith(oldUrl); }); it("also handles context in pipe-separated string format", async () => { vi.mocked(findUserById).mockResolvedValue({ customAvatarUrl: null } as never); const payload = { secure_url: "https://res.cloudinary.com/demo/image/upload/v1/avatars/user-1.jpg", moderation_status: "approved", context: "avatar_target=user|entity_id=user-abc", }; await action({ request: makeRequest(payload), params: {}, context: {} } as never); expect(updateUser).toHaveBeenCalledWith("user-abc", expect.objectContaining({ avatarType: "uploaded" })); }); });