From f8dcc6b13dd56f983e8acdb91c4574bb7dacbbc2 Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Thu, 8 Jan 2026 11:44:50 -0300 Subject: [PATCH] fix: filter Google Workspace credentials by current user (#26216) * fix: filter Google Workspace credentials by current user Add userId filter to getUsersFromGWorkspace credential query and add unit tests for all googleWorkspace handler functions. Also fix OAuth2Client import to use google-auth-library for proper type compatibility * refactor(tests): improve googleWorkspace handler test structure Move dynamic imports to beforeEach block to reduce repetition and improve test readability --------- Co-authored-by: Keith Williams --- .../googleWorkspace.handler.test.ts | 162 ++++++++++++++++++ .../googleWorkspace.handler.ts | 7 +- 2 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.test.ts diff --git a/packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.test.ts b/packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.test.ts new file mode 100644 index 0000000000..dc584e196f --- /dev/null +++ b/packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import type { TrpcSessionUser } from "../../../types"; + +// Mock prisma +vi.mock("@calcom/prisma", () => ({ + prisma: { + credential: { + findFirst: vi.fn(), + deleteMany: vi.fn(), + }, + }, +})); + +// Mock getAppKeysFromSlug +vi.mock("@calcom/app-store/_utils/getAppKeysFromSlug", () => ({ + default: vi.fn(), +})); + +// Factory function to create mock user context +const createMockContext = (overrides: Partial = {}) => ({ + ctx: { + user: { + id: 1, + email: "test@example.com", + username: "testuser", + ...overrides, + } as NonNullable, + }, +}); + +// Factory function to create mock credential matching Prisma's Credential model +const createMockCredential = (overrides: Record = {}) => ({ + id: 1, + type: "google_workspace_directory", + key: { + refresh_token: "mock_refresh_token", + access_token: "mock_access_token", + }, + userId: 1, + teamId: null, + appId: null, + subscriptionId: null, + paymentStatus: null, + billingCycleStart: null, + invalid: false, + delegationCredentialId: null, + ...overrides, +}); + +describe("googleWorkspace.handler", () => { + let prisma: Awaited["prisma"]; + let getAppKeysFromSlug: Awaited["default"]; + let handlers: typeof import("./googleWorkspace.handler"); + + beforeEach(async () => { + vi.clearAllMocks(); + ({ prisma } = await import("@calcom/prisma")); + getAppKeysFromSlug = (await import("@calcom/app-store/_utils/getAppKeysFromSlug")).default; + handlers = await import("./googleWorkspace.handler"); + }); + + describe("checkForGWorkspace", () => { + it("queries credentials filtered by current user id", async () => { + const mockContext = createMockContext({ id: 42 }); + vi.mocked(prisma.credential.findFirst).mockResolvedValue(null); + + await handlers.checkForGWorkspace(mockContext); + + expect(prisma.credential.findFirst).toHaveBeenCalledWith({ + where: { + type: "google_workspace_directory", + userId: 42, + }, + }); + }); + + it("returns credential id when found", async () => { + const mockCredential = createMockCredential({ id: 123 }); + vi.mocked(prisma.credential.findFirst).mockResolvedValue(mockCredential); + + const result = await handlers.checkForGWorkspace(createMockContext()); + + expect(result).toEqual({ id: 123 }); + }); + + it("returns undefined id when no credential found", async () => { + vi.mocked(prisma.credential.findFirst).mockResolvedValue(null); + + const result = await handlers.checkForGWorkspace(createMockContext()); + + expect(result).toEqual({ id: undefined }); + }); + }); + + describe("getUsersFromGWorkspace", () => { + it("queries credentials filtered by current user id", async () => { + const mockContext = createMockContext({ id: 99 }); + + vi.mocked(getAppKeysFromSlug).mockResolvedValue({ + client_id: "mock_client_id", + client_secret: "mock_client_secret", + }); + vi.mocked(prisma.credential.findFirst).mockResolvedValue(null); + + await expect(handlers.getUsersFromGWorkspace(mockContext)).rejects.toThrow( + "No workspace credentials found" + ); + + expect(prisma.credential.findFirst).toHaveBeenCalledWith({ + where: { + type: "google_workspace_directory", + userId: 99, + }, + }); + }); + + it("throws error when Google client_id is missing", async () => { + vi.mocked(getAppKeysFromSlug).mockResolvedValue({}); + + await expect(handlers.getUsersFromGWorkspace(createMockContext())).rejects.toThrow( + "Google client_id missing." + ); + }); + + it("throws error when no credentials found for user", async () => { + vi.mocked(getAppKeysFromSlug).mockResolvedValue({ + client_id: "mock_client_id", + client_secret: "mock_client_secret", + }); + vi.mocked(prisma.credential.findFirst).mockResolvedValue(null); + + await expect(handlers.getUsersFromGWorkspace(createMockContext())).rejects.toThrow( + "No workspace credentials found" + ); + }); + }); + + describe("removeCurrentGoogleWorkspaceConnection", () => { + it("deletes credentials filtered by current user id", async () => { + const mockContext = createMockContext({ id: 55 }); + vi.mocked(prisma.credential.deleteMany).mockResolvedValue({ count: 1 }); + + await handlers.removeCurrentGoogleWorkspaceConnection(mockContext); + + expect(prisma.credential.deleteMany).toHaveBeenCalledWith({ + where: { + type: "google_workspace_directory", + userId: 55, + }, + }); + }); + + it("returns count of deleted credentials", async () => { + vi.mocked(prisma.credential.deleteMany).mockResolvedValue({ count: 2 }); + + const result = await handlers.removeCurrentGoogleWorkspaceConnection(createMockContext()); + + expect(result).toEqual({ deleted: 2 }); + }); + }); +}); diff --git a/packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.ts b/packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.ts index 7f875199af..e4eb9fd062 100644 --- a/packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.ts +++ b/packages/trpc/server/routers/viewer/googleWorkspace/googleWorkspace.handler.ts @@ -1,5 +1,5 @@ import { admin_directory_v1 } from "@googleapis/admin"; -import { OAuth2Client } from "googleapis-common"; +import { OAuth2Client } from "google-auth-library"; import { z } from "zod"; import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; @@ -33,7 +33,7 @@ export const checkForGWorkspace = async ({ ctx }: CheckForGCalOptions) => { return { id: gWorkspacePresent?.id }; }; -export const getUsersFromGWorkspace = async ({}: CheckForGCalOptions) => { +export const getUsersFromGWorkspace = async ({ ctx }: CheckForGCalOptions) => { const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar"); if (!client_id || typeof client_id !== "string") throw new Error("Google client_id missing."); if (!client_secret || typeof client_secret !== "string") throw new Error("Google client_secret missing."); @@ -41,6 +41,7 @@ export const getUsersFromGWorkspace = async ({}: CheckForGCalOptions) => { const hasExistingCredentials = await prisma.credential.findFirst({ where: { type: "google_workspace_directory", + userId: ctx.user.id, }, }); if (!hasExistingCredentials) { @@ -56,7 +57,7 @@ export const getUsersFromGWorkspace = async ({}: CheckForGCalOptions) => { // Create a new instance of the Admin SDK directory API const directory = new admin_directory_v1.Admin({ - auth: oAuth2Client as any, + auth: oAuth2Client, }); const { data } = await directory.users.list({ maxResults: 200, // Up this if we ever need to get more than 200 users