chore: add test for updateProfilePhotoGoogle (#22169)

* fix: process base64 avatar image

* better name

* more

* fix import

* add test

* util

* clean up

* address
This commit is contained in:
Benny Joo authored and GitHub committed 2025-07-01 11:06:57 +01:00
1 parent 2fe71c3695
commit 67032ddaf1
8 files changed
+139 -41

No files matched your search

@@ -0,0 +1,114 @@
import { oauth2_v2 } from "@googleapis/oauth2";
import type { OAuth2Client } from "googleapis-common";
import { describe, it, expect, vi, beforeEach } from "vitest";
import logger from "@calcom/lib/logger";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
import { updateProfilePhotoGoogle } from "./updateProfilePhotoGoogle";
vi.mock("@googleapis/oauth2", () => ({
oauth2_v2: {
Oauth2: vi.fn(),
},
}));
vi.mock("@calcom/lib/logger", () => ({
default: {
error: vi.fn(),
},
}));
vi.mock("@calcom/lib/server/avatar", () => ({
uploadAvatar: vi.fn(),
}));
vi.mock("@calcom/lib/server/resizeBase64Image", () => ({
resizeBase64Image: vi.fn(),
}));
vi.mock("@calcom/lib/server/repository/user", () => ({
UserRepository: {
updateAvatar: vi.fn(),
},
}));
describe("updateProfilePhotoGoogle", () => {
const mockOAuth2Client = {} as OAuth2Client;
const userId = 123;
let mockOauth2Instance: any;
beforeEach(() => {
vi.clearAllMocks();
mockOauth2Instance = {
userinfo: {
get: vi.fn(),
},
};
(oauth2_v2.Oauth2 as any).mockImplementation(() => mockOauth2Instance);
(uploadAvatar as any).mockResolvedValue("/api/avatar/processed-123.png");
(resizeBase64Image as any).mockResolvedValue("data:image/png;base64,resized-data");
(UserRepository.updateAvatar as any).mockResolvedValue(undefined);
});
it("should update avatar with valid URL", async () => {
const validUrl = "https://example.com/avatar.jpg";
mockOauth2Instance.userinfo.get.mockResolvedValue({
data: { picture: validUrl },
});
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
id: userId,
avatarUrl: validUrl,
});
expect(uploadAvatar).not.toHaveBeenCalled();
});
it("should process base64 data", async () => {
const base64Data =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
mockOauth2Instance.userinfo.get.mockResolvedValue({
data: { picture: base64Data },
});
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(resizeBase64Image).toHaveBeenCalledWith(base64Data);
expect(uploadAvatar).toHaveBeenCalledWith({
avatar: "data:image/png;base64,resized-data",
userId,
});
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
id: userId,
avatarUrl: "/api/avatar/processed-123.png",
});
});
it("should do nothing when no picture provided", async () => {
mockOauth2Instance.userinfo.get.mockResolvedValue({
data: {},
});
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
expect(uploadAvatar).not.toHaveBeenCalled();
});
it("should handle errors gracefully", async () => {
const error = new Error("OAuth API failed");
mockOauth2Instance.userinfo.get.mockRejectedValue(error);
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
expect(logger.error).toHaveBeenCalledWith("Error updating avatarUrl from google calendar connect", error);
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
});
});
@@ -1,6 +1,7 @@
import { oauth2_v2 } from "@googleapis/oauth2";
import type { OAuth2Client } from "googleapis-common";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import logger from "@calcom/lib/logger";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { UserRepository } from "@calcom/lib/server/repository/user";
@@ -16,11 +17,7 @@ export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userI
}
// Handle base64 data
if (
avatarUrl.startsWith("data:image/png;base64,") ||
avatarUrl.startsWith("data:image/jpeg;base64,") ||
avatarUrl.startsWith("data:image/jpg;base64,")
) {
if (isBase64Image(avatarUrl)) {
const resizedAvatarUrl = await uploadAvatar({
avatar: await resizeBase64Image(avatarUrl),
userId,
+12
View File
@@ -0,0 +1,12 @@
/**
* Checks if a string is a supported base64 image format
*/
export function isBase64Image(input: string | null | undefined): boolean {
if (!input) return false;
return (
input.startsWith("data:image/png;base64,") ||
input.startsWith("data:image/jpeg;base64,") ||
input.startsWith("data:image/jpg;base64,")
);
}
@@ -9,6 +9,7 @@ import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billlin
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { HttpError } from "@calcom/lib/http-error";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import logger from "@calcom/lib/logger";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { checkUsername } from "@calcom/lib/server/checkUsername";
@@ -152,12 +153,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
}
// if defined AND a base 64 string, upload and update the avatar URL
if (
input.avatarUrl &&
(input.avatarUrl.startsWith("data:image/png;base64,") ||
input.avatarUrl.startsWith("data:image/jpeg;base64,") ||
input.avatarUrl.startsWith("data:image/jpg;base64,"))
) {
if (input.avatarUrl && isBase64Image(input.avatarUrl)) {
data.avatarUrl = await uploadAvatar({
avatar: await resizeBase64Image(input.avatarUrl),
userId: user.id,
@@ -2,6 +2,7 @@ import type { Prisma } from "@prisma/client";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadLogo } from "@calcom/lib/server/avatar";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
@@ -161,12 +162,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,"))
) {
if (input.banner && isBase64Image(input.banner)) {
const banner = await resizeBase64Image(input.banner, { maxSize: 1500 });
data.bannerUrl = await uploadLogo({
logo: banner,
@@ -177,12 +173,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
data.bannerUrl = null;
}
if (
input.logoUrl &&
(input.logoUrl.startsWith("data:image/png;base64,") ||
input.logoUrl.startsWith("data:image/jpeg;base64,") ||
input.logoUrl.startsWith("data:image/jpg;base64,"))
) {
if (input.logoUrl && isBase64Image(input.logoUrl)) {
data.logoUrl = await uploadLogo({
logo: await resizeBase64Image(input.logoUrl),
teamId: currentOrgId,
@@ -2,6 +2,7 @@ import type { Prisma, PrismaPromise, User, Membership, Profile } from "@prisma/c
import { ensureOrganizationIsReviewed } from "@calcom/ee/organizations/lib/ensureOrganizationIsReviewed";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername";
import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
@@ -119,12 +120,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
timeZone: input.timeZone,
};
if (
input.avatar &&
(input.avatar.startsWith("data:image/png;base64,") ||
input.avatar.startsWith("data:image/jpeg;base64,") ||
input.avatar.startsWith("data:image/jpg;base64,"))
) {
if (input.avatar && isBase64Image(input.avatar)) {
const avatar = await resizeBase64Image(input.avatar);
data.avatarUrl = await uploadAvatar({
avatar,
@@ -1,5 +1,6 @@
import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadLogo } from "@calcom/lib/server/avatar";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
@@ -106,12 +107,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
},
});
// Upload logo, create doesn't allow logo removal
if (
input.logo &&
(input.logo.startsWith("data:image/png;base64,") ||
input.logo.startsWith("data:image/jpeg;base64,") ||
input.logo.startsWith("data:image/jpg;base64,"))
) {
if (input.logo && isBase64Image(input.logo)) {
const logoUrl = await uploadLogo({
logo: await resizeBase64Image(input.logo),
teamId: createdTeam.id,
@@ -4,6 +4,7 @@ import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
import { isBase64Image } from "@calcom/lib/isBase64Image";
import { uploadLogo } from "@calcom/lib/server/avatar";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import { prisma } from "@calcom/prisma";
@@ -70,12 +71,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
rrTimestampBasis: input.rrTimestampBasis,
};
if (
input.logo &&
(input.logo.startsWith("data:image/png;base64,") ||
input.logo.startsWith("data:image/jpeg;base64,") ||
input.logo.startsWith("data:image/jpg;base64,"))
) {
if (input.logo && isBase64Image(input.logo)) {
data.logoUrl = await uploadLogo({ teamId: input.id, logo: input.logo });
} else if (typeof input.logo !== "undefined" && !input.logo) {
data.logoUrl = null;