|
| 1 | +import { SUCCESS_STATUS } from "@calcom/platform-constants"; |
| 2 | +import { encryptServiceAccountKey } from "@calcom/platform-libraries"; |
| 3 | +import type { Team, User } from "@calcom/prisma/client"; |
| 4 | + |
| 5 | +// Mock the toggleDelegationCredentialEnabled function to bypass Google API calls |
| 6 | +const mockToggleDelegationCredentialEnabled = jest.fn(); |
| 7 | +jest.mock("@calcom/platform-libraries/app-store", () => { |
| 8 | + const actual = jest.requireActual("@calcom/platform-libraries/app-store"); |
| 9 | + return { |
| 10 | + ...actual, |
| 11 | + toggleDelegationCredentialEnabled: (...args: unknown[]) => mockToggleDelegationCredentialEnabled(...args), |
| 12 | + }; |
| 13 | +}); |
| 14 | + |
| 15 | +import { INestApplication } from "@nestjs/common"; |
| 16 | +import { NestExpressApplication } from "@nestjs/platform-express"; |
| 17 | +import { Test } from "@nestjs/testing"; |
| 18 | +import request from "supertest"; |
| 19 | +import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; |
| 20 | +import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture"; |
| 21 | +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; |
| 22 | +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; |
| 23 | +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; |
| 24 | +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; |
| 25 | +import { randomString } from "test/utils/randomString"; |
| 26 | +import { AppModule } from "@/app.module"; |
| 27 | +import { bootstrap } from "@/bootstrap"; |
| 28 | +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; |
| 29 | +import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; |
| 30 | +import { PrismaModule } from "@/modules/prisma/prisma.module"; |
| 31 | +import { TokensModule } from "@/modules/tokens/tokens.module"; |
| 32 | +import { UsersModule } from "@/modules/users/users.module"; |
| 33 | +import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input"; |
| 34 | +import { UpdateDelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/update-delegation-credential.output"; |
| 35 | + |
| 36 | +describe("Organizations Delegation Credentials Endpoints", () => { |
| 37 | + describe("User Authentication - User is Org Admin", () => { |
| 38 | + let app: INestApplication; |
| 39 | + |
| 40 | + let userRepositoryFixture: UserRepositoryFixture; |
| 41 | + let organizationsRepositoryFixture: OrganizationRepositoryFixture; |
| 42 | + let membershipRepositoryFixture: MembershipRepositoryFixture; |
| 43 | + let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture; |
| 44 | + let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; |
| 45 | + let profilesRepositoryFixture: ProfileRepositoryFixture; |
| 46 | + let prismaWriteService: PrismaWriteService; |
| 47 | + |
| 48 | + let org: Team; |
| 49 | + let user: User; |
| 50 | + let apiKey: string; |
| 51 | + let delegationCredentialId: string; |
| 52 | + let workspacePlatformId: number; |
| 53 | + let ensureDefaultCalendarsSpy: jest.SpyInstance; |
| 54 | + |
| 55 | + const userEmail = `delegation-credentials-admin-${randomString()}@api.com`; |
| 56 | + |
| 57 | + beforeAll(async () => { |
| 58 | + const moduleRef = await Test.createTestingModule({ |
| 59 | + imports: [AppModule, PrismaModule, UsersModule, TokensModule], |
| 60 | + }).compile(); |
| 61 | + |
| 62 | + userRepositoryFixture = new UserRepositoryFixture(moduleRef); |
| 63 | + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); |
| 64 | + membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); |
| 65 | + platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); |
| 66 | + apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); |
| 67 | + profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); |
| 68 | + prismaWriteService = moduleRef.get(PrismaWriteService); |
| 69 | + |
| 70 | + user = await userRepositoryFixture.create({ |
| 71 | + email: userEmail, |
| 72 | + username: userEmail, |
| 73 | + }); |
| 74 | + |
| 75 | + org = await organizationsRepositoryFixture.create({ |
| 76 | + name: `delegation-credentials-organization-${randomString()}`, |
| 77 | + isOrganization: true, |
| 78 | + isPlatform: true, |
| 79 | + }); |
| 80 | + |
| 81 | + await profilesRepositoryFixture.create({ |
| 82 | + uid: `${randomString()}-uid`, |
| 83 | + username: userEmail, |
| 84 | + user: { connect: { id: user.id } }, |
| 85 | + organization: { connect: { id: org.id } }, |
| 86 | + movedFromUser: { connect: { id: user.id } }, |
| 87 | + }); |
| 88 | + |
| 89 | + await platformBillingRepositoryFixture.create(org.id, "SCALE"); |
| 90 | + |
| 91 | + await membershipRepositoryFixture.create({ |
| 92 | + role: "ADMIN", |
| 93 | + user: { connect: { id: user.id } }, |
| 94 | + team: { connect: { id: org.id } }, |
| 95 | + accepted: true, |
| 96 | + }); |
| 97 | + |
| 98 | + const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null, org.id); |
| 99 | + apiKey = `cal_test_${keyString}`; |
| 100 | + |
| 101 | + const workspacePlatform = await prismaWriteService.prisma.workspacePlatform.create({ |
| 102 | + data: { |
| 103 | + slug: "google", |
| 104 | + name: "Google Workspace", |
| 105 | + description: "Google Workspace for testing", |
| 106 | + defaultServiceAccountKey: { |
| 107 | + type: "service_account", |
| 108 | + project_id: "test-project", |
| 109 | + private_key_id: "test-key-id", |
| 110 | + private_key: "test-private-key", |
| 111 | + client_email: "test@test-project.iam.gserviceaccount.com", |
| 112 | + client_id: "123456789", |
| 113 | + auth_uri: "https://accounts.google.com/o/oauth2/auth", |
| 114 | + token_uri: "https://oauth2.googleapis.com/token", |
| 115 | + auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs", |
| 116 | + client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test", |
| 117 | + }, |
| 118 | + enabled: true, |
| 119 | + }, |
| 120 | + }); |
| 121 | + workspacePlatformId = workspacePlatform.id; |
| 122 | + |
| 123 | + const testServiceAccountKey = { |
| 124 | + type: "service_account" as const, |
| 125 | + project_id: "test-project", |
| 126 | + private_key_id: "test-key-id", |
| 127 | + private_key: "test-private-key", |
| 128 | + client_email: "test@test-project.iam.gserviceaccount.com", |
| 129 | + client_id: "123456789", |
| 130 | + auth_uri: "https://accounts.google.com/o/oauth2/auth", |
| 131 | + token_uri: "https://oauth2.googleapis.com/token", |
| 132 | + auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs", |
| 133 | + client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test", |
| 134 | + }; |
| 135 | + |
| 136 | + const encryptedServiceAccountKey = encryptServiceAccountKey(testServiceAccountKey); |
| 137 | + |
| 138 | + const delegationCredential = await prismaWriteService.prisma.delegationCredential.create({ |
| 139 | + data: { |
| 140 | + workspacePlatformId: workspacePlatform.id, |
| 141 | + organizationId: org.id, |
| 142 | + domain: "@test-domain.com", |
| 143 | + serviceAccountKey: encryptedServiceAccountKey, |
| 144 | + enabled: false, |
| 145 | + }, |
| 146 | + }); |
| 147 | + delegationCredentialId = delegationCredential.id; |
| 148 | + |
| 149 | + // Set up spy on prototype BEFORE app.init() - this is critical for NestJS |
| 150 | + ensureDefaultCalendarsSpy = jest |
| 151 | + .spyOn(OrganizationsDelegationCredentialService.prototype, "ensureDefaultCalendars") |
| 152 | + .mockResolvedValue(undefined); |
| 153 | + |
| 154 | + app = moduleRef.createNestApplication(); |
| 155 | + bootstrap(app as NestExpressApplication); |
| 156 | + |
| 157 | + await app.init(); |
| 158 | + }); |
| 159 | + |
| 160 | + afterEach(() => { |
| 161 | + // Clear the spy call history after each test |
| 162 | + ensureDefaultCalendarsSpy.mockClear(); |
| 163 | + mockToggleDelegationCredentialEnabled.mockClear(); |
| 164 | + }); |
| 165 | + |
| 166 | + it("should be defined", () => { |
| 167 | + expect(userRepositoryFixture).toBeDefined(); |
| 168 | + expect(organizationsRepositoryFixture).toBeDefined(); |
| 169 | + expect(user).toBeDefined(); |
| 170 | + expect(org).toBeDefined(); |
| 171 | + }); |
| 172 | + |
| 173 | + it("should call ensureDefaultCalendars when enabling delegation credentials", async () => { |
| 174 | + await prismaWriteService.prisma.delegationCredential.update({ |
| 175 | + where: { id: delegationCredentialId }, |
| 176 | + data: { enabled: false }, |
| 177 | + }); |
| 178 | + |
| 179 | + // Mock toggleDelegationCredentialEnabled to return a valid response |
| 180 | + mockToggleDelegationCredentialEnabled.mockResolvedValue({ |
| 181 | + id: delegationCredentialId, |
| 182 | + enabled: true, |
| 183 | + }); |
| 184 | + |
| 185 | + const response = await request(app.getHttpServer()) |
| 186 | + .patch(`/v2/organizations/${org.id}/delegation-credentials/${delegationCredentialId}`) |
| 187 | + .set("Authorization", `Bearer ${apiKey}`) |
| 188 | + .send({ |
| 189 | + enabled: true, |
| 190 | + } satisfies UpdateDelegationCredentialInput) |
| 191 | + .expect(200); |
| 192 | + |
| 193 | + const responseBody: UpdateDelegationCredentialOutput = response.body; |
| 194 | + expect(responseBody.status).toEqual(SUCCESS_STATUS); |
| 195 | + expect(responseBody.data.enabled).toEqual(true); |
| 196 | + |
| 197 | + expect(ensureDefaultCalendarsSpy).toHaveBeenCalledWith(org.id, "@test-domain.com"); |
| 198 | + }); |
| 199 | + |
| 200 | + it("should not call ensureDefaultCalendars when disabling delegation credentials", async () => { |
| 201 | + await prismaWriteService.prisma.delegationCredential.update({ |
| 202 | + where: { id: delegationCredentialId }, |
| 203 | + data: { enabled: true }, |
| 204 | + }); |
| 205 | + |
| 206 | + // Mock toggleDelegationCredentialEnabled to return a valid response |
| 207 | + mockToggleDelegationCredentialEnabled.mockResolvedValue({ |
| 208 | + id: delegationCredentialId, |
| 209 | + enabled: false, |
| 210 | + }); |
| 211 | + |
| 212 | + const response = await request(app.getHttpServer()) |
| 213 | + .patch(`/v2/organizations/${org.id}/delegation-credentials/${delegationCredentialId}`) |
| 214 | + .set("Authorization", `Bearer ${apiKey}`) |
| 215 | + .send({ |
| 216 | + enabled: false, |
| 217 | + } satisfies UpdateDelegationCredentialInput) |
| 218 | + .expect(200); |
| 219 | + |
| 220 | + const responseBody: UpdateDelegationCredentialOutput = response.body; |
| 221 | + expect(responseBody.status).toEqual(SUCCESS_STATUS); |
| 222 | + expect(responseBody.data.enabled).toEqual(false); |
| 223 | + |
| 224 | + expect(ensureDefaultCalendarsSpy).not.toHaveBeenCalled(); |
| 225 | + }); |
| 226 | + |
| 227 | + it("should not call ensureDefaultCalendars when enabling already enabled delegation credentials", async () => { |
| 228 | + await prismaWriteService.prisma.delegationCredential.update({ |
| 229 | + where: { id: delegationCredentialId }, |
| 230 | + data: { enabled: true }, |
| 231 | + }); |
| 232 | + |
| 233 | + // Mock toggleDelegationCredentialEnabled to return a valid response |
| 234 | + mockToggleDelegationCredentialEnabled.mockResolvedValue({ |
| 235 | + id: delegationCredentialId, |
| 236 | + enabled: true, |
| 237 | + }); |
| 238 | + |
| 239 | + const response = await request(app.getHttpServer()) |
| 240 | + .patch(`/v2/organizations/${org.id}/delegation-credentials/${delegationCredentialId}`) |
| 241 | + .set("Authorization", `Bearer ${apiKey}`) |
| 242 | + .send({ |
| 243 | + enabled: true, |
| 244 | + } satisfies UpdateDelegationCredentialInput) |
| 245 | + .expect(200); |
| 246 | + |
| 247 | + const responseBody: UpdateDelegationCredentialOutput = response.body; |
| 248 | + expect(responseBody.status).toEqual(SUCCESS_STATUS); |
| 249 | + expect(responseBody.data.enabled).toEqual(true); |
| 250 | + |
| 251 | + expect(ensureDefaultCalendarsSpy).not.toHaveBeenCalled(); |
| 252 | + }); |
| 253 | + |
| 254 | + afterAll(async () => { |
| 255 | + if (org?.id) { |
| 256 | + await prismaWriteService.prisma.delegationCredential.deleteMany({ |
| 257 | + where: { organizationId: org.id }, |
| 258 | + }); |
| 259 | + await organizationsRepositoryFixture.delete(org.id); |
| 260 | + } |
| 261 | + if (workspacePlatformId) { |
| 262 | + await prismaWriteService.prisma.workspacePlatform.delete({ |
| 263 | + where: { id: workspacePlatformId }, |
| 264 | + }); |
| 265 | + } |
| 266 | + if (user?.email) { |
| 267 | + await userRepositoryFixture.deleteByEmail(user.email); |
| 268 | + } |
| 269 | + await app.close(); |
| 270 | + }); |
| 271 | + }); |
| 272 | +}); |
0 commit comments