brackt/app/routes/__tests__/api.webhooks.cloudinary.test.ts

196 lines
7.6 KiB
TypeScript
Raw Permalink Normal View History

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" }));
});
});