fix: update handler to not delete banner (#24106)

* Fix update handler to not delete banner

* code cleanup

* add banner-upload.e2e

* fix: add slug fallback in banner-upload e2e test

- Use org.slug as fallback when requestedSlug is undefined
- Addresses GitHub comment about guarding against undefined slug
- Prevents test failures when requestedSlug is not set
- Fix ESLint warnings: remove unnecessary escape characters and unused variable

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* test: add banner preservation test for profile updates

- Verify that updating organization profile without banner changes preserves existing banner
- Ensures update handler doesn't overwrite banner when undefined is passed
- Addresses user request to test banner preservation during profile saves

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* remove banner e2e wip

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
sean-brydon
2025-09-29 09:31:49 +01:00
committed by GitHub
co-authored by sean@cal.com Hariom Balhara Devin AI
parent cc8a1290ee
commit f6b061f787
2 changed files with 151 additions and 15 deletions
@@ -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();
});
});
@@ -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<string | null | undefined> => {
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<TrpcSessionUser>;
@@ -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 &&