diff --git a/packages/trpc/server/routers/viewer/organizations/handleBannerUpdate.test.ts b/packages/trpc/server/routers/viewer/organizations/handleBannerUpdate.test.ts new file mode 100644 index 0000000000..1aa2588f65 --- /dev/null +++ b/packages/trpc/server/routers/viewer/organizations/handleBannerUpdate.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +import { getBannerUrl } from "./update.handler"; + +vi.mock("@calcom/lib/server/avatar", () => ({ + uploadLogo: vi.fn(), +})); + +vi.mock("@calcom/lib/server/resizeBase64Image", () => ({ + resizeBase64Image: vi.fn((input) => input), +})); + +const { uploadLogo } = await import("@calcom/lib/server/avatar"); +const { resizeBase64Image } = await import("@calcom/lib/server/resizeBase64Image"); + +describe("getBannerUrl", () => { + const mockTeamId = 123; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should return undefined when banner is undefined (not provided)", async () => { + const result = await getBannerUrl(undefined, mockTeamId); + + expect(result).toBeUndefined(); + expect(uploadLogo).not.toHaveBeenCalled(); + expect(resizeBase64Image).not.toHaveBeenCalled(); + }); + + it("should return null when banner is explicitly set to null", async () => { + const result = await getBannerUrl(null, mockTeamId); + + expect(result).toBe(null); + expect(uploadLogo).not.toHaveBeenCalled(); + expect(resizeBase64Image).not.toHaveBeenCalled(); + }); + + it("should upload and return URL for valid PNG base64 banner", async () => { + const mockBannerUrl = "https://example.com/banner.png"; + const validBase64 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="; + + vi.mocked(uploadLogo).mockResolvedValue(mockBannerUrl); + + const result = await getBannerUrl(validBase64, mockTeamId); + + expect(result).toBe(mockBannerUrl); + expect(resizeBase64Image).toHaveBeenCalledWith(validBase64, { maxSize: 1500 }); + expect(uploadLogo).toHaveBeenCalledWith({ + logo: validBase64, // resizeBase64Image mock returns input unchanged + teamId: mockTeamId, + isBanner: true, + }); + }); + + it("should upload and return URL for valid JPEG base64 banner", async () => { + const mockBannerUrl = "https://example.com/banner.jpg"; + const validBase64 = + "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/wA=="; + + vi.mocked(uploadLogo).mockResolvedValue(mockBannerUrl); + + const result = await getBannerUrl(validBase64, mockTeamId); + + expect(result).toBe(mockBannerUrl); + expect(resizeBase64Image).toHaveBeenCalledWith(validBase64, { maxSize: 1500 }); + expect(uploadLogo).toHaveBeenCalledWith({ + logo: validBase64, + teamId: mockTeamId, + isBanner: true, + }); + }); + + it("should upload and return URL for valid JPG base64 banner", async () => { + const mockBannerUrl = "https://example.com/banner.jpg"; + const validBase64 = + "data:image/jpg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/wA=="; + + vi.mocked(uploadLogo).mockResolvedValue(mockBannerUrl); + + const result = await getBannerUrl(validBase64, mockTeamId); + + expect(result).toBe(mockBannerUrl); + expect(resizeBase64Image).toHaveBeenCalledWith(validBase64, { maxSize: 1500 }); + expect(uploadLogo).toHaveBeenCalledWith({ + logo: validBase64, + teamId: mockTeamId, + isBanner: true, + }); + }); + + it("should return undefined for invalid banner string", async () => { + const result = await getBannerUrl("invalid-banner-string", mockTeamId); + + expect(result).toBeUndefined(); + expect(uploadLogo).not.toHaveBeenCalled(); + expect(resizeBase64Image).not.toHaveBeenCalled(); + }); + + it("should return undefined for empty banner string", async () => { + const result = await getBannerUrl("", mockTeamId); + + expect(result).toBeUndefined(); + expect(uploadLogo).not.toHaveBeenCalled(); + expect(resizeBase64Image).not.toHaveBeenCalled(); + }); + + it("should return undefined for non-image base64 string", async () => { + const invalidBase64 = "data:text/plain;base64,SGVsbG8gV29ybGQ="; + + const result = await getBannerUrl(invalidBase64, mockTeamId); + + expect(result).toBeUndefined(); + expect(uploadLogo).not.toHaveBeenCalled(); + expect(resizeBase64Image).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index 2bee594150..087d6fc813 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -15,6 +15,38 @@ import { TRPCError } from "@trpc/server"; import type { TrpcSessionUser } from "../../../types"; import type { TUpdateInputSchema } from "./update.schema"; +export const getBannerUrl = async ( + banner: string | null | undefined, + teamId: number +): Promise => { + if (banner === undefined) { + // Banner not provided, don't update + return undefined; + } + + if (banner === null) { + // Explicitly set to null, remove banner + return null; + } + + if ( + banner.startsWith("data:image/png;base64,") || + banner.startsWith("data:image/jpeg;base64,") || + banner.startsWith("data:image/jpg;base64,") + ) { + // Valid base64 image, resize and upload + const resizedBanner = await resizeBase64Image(banner, { maxSize: 1500 }); + return await uploadLogo({ + logo: resizedBanner, + teamId, + isBanner: true, + }); + } + + // Invalid banner string, don't update + return undefined; +}; + type UpdateOptions = { ctx: { user: NonNullable; @@ -185,21 +217,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { metadata: mergeMetadata({ ...input.metadata }), }; - if ( - input.banner && - (input.banner.startsWith("data:image/png;base64,") || - input.banner.startsWith("data:image/jpeg;base64,") || - input.banner.startsWith("data:image/jpg;base64,")) - ) { - const banner = await resizeBase64Image(input.banner, { maxSize: 1500 }); - data.bannerUrl = await uploadLogo({ - logo: banner, - teamId: currentOrgId, - isBanner: true, - }); - } else { - data.bannerUrl = null; - } + data.bannerUrl = await getBannerUrl(input.banner, currentOrgId); if ( input.logoUrl &&