Skip to content

Commit 67032dd

Browse files
authored
chore: add test for updateProfilePhotoGoogle (calcom#22169)
* fix: process base64 avatar image * better name * more * fix import * add test * util * clean up * address
1 parent 2fe71c3 commit 67032dd

8 files changed

Lines changed: 139 additions & 41 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { oauth2_v2 } from "@googleapis/oauth2";
2+
import type { OAuth2Client } from "googleapis-common";
3+
import { describe, it, expect, vi, beforeEach } from "vitest";
4+
5+
import logger from "@calcom/lib/logger";
6+
import { uploadAvatar } from "@calcom/lib/server/avatar";
7+
import { UserRepository } from "@calcom/lib/server/repository/user";
8+
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
9+
10+
import { updateProfilePhotoGoogle } from "./updateProfilePhotoGoogle";
11+
12+
vi.mock("@googleapis/oauth2", () => ({
13+
oauth2_v2: {
14+
Oauth2: vi.fn(),
15+
},
16+
}));
17+
18+
vi.mock("@calcom/lib/logger", () => ({
19+
default: {
20+
error: vi.fn(),
21+
},
22+
}));
23+
24+
vi.mock("@calcom/lib/server/avatar", () => ({
25+
uploadAvatar: vi.fn(),
26+
}));
27+
28+
vi.mock("@calcom/lib/server/resizeBase64Image", () => ({
29+
resizeBase64Image: vi.fn(),
30+
}));
31+
32+
vi.mock("@calcom/lib/server/repository/user", () => ({
33+
UserRepository: {
34+
updateAvatar: vi.fn(),
35+
},
36+
}));
37+
38+
describe("updateProfilePhotoGoogle", () => {
39+
const mockOAuth2Client = {} as OAuth2Client;
40+
const userId = 123;
41+
42+
let mockOauth2Instance: any;
43+
44+
beforeEach(() => {
45+
vi.clearAllMocks();
46+
47+
mockOauth2Instance = {
48+
userinfo: {
49+
get: vi.fn(),
50+
},
51+
};
52+
53+
(oauth2_v2.Oauth2 as any).mockImplementation(() => mockOauth2Instance);
54+
(uploadAvatar as any).mockResolvedValue("/api/avatar/processed-123.png");
55+
(resizeBase64Image as any).mockResolvedValue("data:image/png;base64,resized-data");
56+
(UserRepository.updateAvatar as any).mockResolvedValue(undefined);
57+
});
58+
59+
it("should update avatar with valid URL", async () => {
60+
const validUrl = "https://example.com/avatar.jpg";
61+
mockOauth2Instance.userinfo.get.mockResolvedValue({
62+
data: { picture: validUrl },
63+
});
64+
65+
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
66+
67+
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
68+
id: userId,
69+
avatarUrl: validUrl,
70+
});
71+
expect(uploadAvatar).not.toHaveBeenCalled();
72+
});
73+
74+
it("should process base64 data", async () => {
75+
const base64Data =
76+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
77+
mockOauth2Instance.userinfo.get.mockResolvedValue({
78+
data: { picture: base64Data },
79+
});
80+
81+
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
82+
83+
expect(resizeBase64Image).toHaveBeenCalledWith(base64Data);
84+
expect(uploadAvatar).toHaveBeenCalledWith({
85+
avatar: "data:image/png;base64,resized-data",
86+
userId,
87+
});
88+
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
89+
id: userId,
90+
avatarUrl: "/api/avatar/processed-123.png",
91+
});
92+
});
93+
94+
it("should do nothing when no picture provided", async () => {
95+
mockOauth2Instance.userinfo.get.mockResolvedValue({
96+
data: {},
97+
});
98+
99+
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
100+
101+
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
102+
expect(uploadAvatar).not.toHaveBeenCalled();
103+
});
104+
105+
it("should handle errors gracefully", async () => {
106+
const error = new Error("OAuth API failed");
107+
mockOauth2Instance.userinfo.get.mockRejectedValue(error);
108+
109+
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
110+
111+
expect(logger.error).toHaveBeenCalledWith("Error updating avatarUrl from google calendar connect", error);
112+
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
113+
});
114+
});

packages/app-store/_utils/oauth/updateProfilePhotoGoogle.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { oauth2_v2 } from "@googleapis/oauth2";
22
import type { OAuth2Client } from "googleapis-common";
33

4+
import { isBase64Image } from "@calcom/lib/isBase64Image";
45
import logger from "@calcom/lib/logger";
56
import { uploadAvatar } from "@calcom/lib/server/avatar";
67
import { UserRepository } from "@calcom/lib/server/repository/user";
@@ -16,11 +17,7 @@ export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userI
1617
}
1718

1819
// Handle base64 data
19-
if (
20-
avatarUrl.startsWith("data:image/png;base64,") ||
21-
avatarUrl.startsWith("data:image/jpeg;base64,") ||
22-
avatarUrl.startsWith("data:image/jpg;base64,")
23-
) {
20+
if (isBase64Image(avatarUrl)) {
2421
const resizedAvatarUrl = await uploadAvatar({
2522
avatar: await resizeBase64Image(avatarUrl),
2623
userId,

packages/lib/isBase64Image.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Checks if a string is a supported base64 image format
3+
*/
4+
export function isBase64Image(input: string | null | undefined): boolean {
5+
if (!input) return false;
6+
7+
return (
8+
input.startsWith("data:image/png;base64,") ||
9+
input.startsWith("data:image/jpeg;base64,") ||
10+
input.startsWith("data:image/jpg;base64,")
11+
);
12+
}

packages/trpc/server/routers/viewer/me/updateProfile.handler.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billlin
99
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
1010
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
1111
import { HttpError } from "@calcom/lib/http-error";
12+
import { isBase64Image } from "@calcom/lib/isBase64Image";
1213
import logger from "@calcom/lib/logger";
1314
import { uploadAvatar } from "@calcom/lib/server/avatar";
1415
import { checkUsername } from "@calcom/lib/server/checkUsername";
@@ -152,12 +153,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
152153
}
153154

154155
// if defined AND a base 64 string, upload and update the avatar URL
155-
if (
156-
input.avatarUrl &&
157-
(input.avatarUrl.startsWith("data:image/png;base64,") ||
158-
input.avatarUrl.startsWith("data:image/jpeg;base64,") ||
159-
input.avatarUrl.startsWith("data:image/jpg;base64,"))
160-
) {
156+
if (input.avatarUrl && isBase64Image(input.avatarUrl)) {
161157
data.avatarUrl = await uploadAvatar({
162158
avatar: await resizeBase64Image(input.avatarUrl),
163159
userId: user.id,

packages/trpc/server/routers/viewer/organizations/update.handler.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Prisma } from "@prisma/client";
22

33
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
44
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
5+
import { isBase64Image } from "@calcom/lib/isBase64Image";
56
import { uploadLogo } from "@calcom/lib/server/avatar";
67
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
78
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
@@ -161,12 +162,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
161162
metadata: mergeMetadata({ ...input.metadata }),
162163
};
163164

164-
if (
165-
input.banner &&
166-
(input.banner.startsWith("data:image/png;base64,") ||
167-
input.banner.startsWith("data:image/jpeg;base64,") ||
168-
input.banner.startsWith("data:image/jpg;base64,"))
169-
) {
165+
if (input.banner && isBase64Image(input.banner)) {
170166
const banner = await resizeBase64Image(input.banner, { maxSize: 1500 });
171167
data.bannerUrl = await uploadLogo({
172168
logo: banner,
@@ -177,12 +173,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
177173
data.bannerUrl = null;
178174
}
179175

180-
if (
181-
input.logoUrl &&
182-
(input.logoUrl.startsWith("data:image/png;base64,") ||
183-
input.logoUrl.startsWith("data:image/jpeg;base64,") ||
184-
input.logoUrl.startsWith("data:image/jpg;base64,"))
185-
) {
176+
if (input.logoUrl && isBase64Image(input.logoUrl)) {
186177
data.logoUrl = await uploadLogo({
187178
logo: await resizeBase64Image(input.logoUrl),
188179
teamId: currentOrgId,

packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Prisma, PrismaPromise, User, Membership, Profile } from "@prisma/c
22

33
import { ensureOrganizationIsReviewed } from "@calcom/ee/organizations/lib/ensureOrganizationIsReviewed";
44
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
5+
import { isBase64Image } from "@calcom/lib/isBase64Image";
56
import { uploadAvatar } from "@calcom/lib/server/avatar";
67
import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername";
78
import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
@@ -119,12 +120,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
119120
timeZone: input.timeZone,
120121
};
121122

122-
if (
123-
input.avatar &&
124-
(input.avatar.startsWith("data:image/png;base64,") ||
125-
input.avatar.startsWith("data:image/jpeg;base64,") ||
126-
input.avatar.startsWith("data:image/jpg;base64,"))
127-
) {
123+
if (input.avatar && isBase64Image(input.avatar)) {
128124
const avatar = await resizeBase64Image(input.avatar);
129125
data.avatarUrl = await uploadAvatar({
130126
avatar,

packages/trpc/server/routers/viewer/teams/create.handler.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments";
22
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
3+
import { isBase64Image } from "@calcom/lib/isBase64Image";
34
import { uploadLogo } from "@calcom/lib/server/avatar";
45
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
56
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
@@ -106,12 +107,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
106107
},
107108
});
108109
// Upload logo, create doesn't allow logo removal
109-
if (
110-
input.logo &&
111-
(input.logo.startsWith("data:image/png;base64,") ||
112-
input.logo.startsWith("data:image/jpeg;base64,") ||
113-
input.logo.startsWith("data:image/jpg;base64,"))
114-
) {
110+
if (input.logo && isBase64Image(input.logo)) {
115111
const logoUrl = await uploadLogo({
116112
logo: await resizeBase64Image(input.logo),
117113
teamId: createdTeam.id,

packages/trpc/server/routers/viewer/teams/update.handler.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
44
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
55
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
66
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
7+
import { isBase64Image } from "@calcom/lib/isBase64Image";
78
import { uploadLogo } from "@calcom/lib/server/avatar";
89
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
910
import { prisma } from "@calcom/prisma";
@@ -70,12 +71,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
7071
rrTimestampBasis: input.rrTimestampBasis,
7172
};
7273

73-
if (
74-
input.logo &&
75-
(input.logo.startsWith("data:image/png;base64,") ||
76-
input.logo.startsWith("data:image/jpeg;base64,") ||
77-
input.logo.startsWith("data:image/jpg;base64,"))
78-
) {
74+
if (input.logo && isBase64Image(input.logo)) {
7975
data.logoUrl = await uploadLogo({ teamId: input.id, logo: input.logo });
8076
} else if (typeof input.logo !== "undefined" && !input.logo) {
8177
data.logoUrl = null;

0 commit comments

Comments
 (0)