Files
calendar/packages/app-store/googlecalendar/lib/__tests__/CalendarService.auth.test.ts
T

313 lines
11 KiB
TypeScript

import prismock from "../../../../../tests/libs/__mocks__/prisma";
import "../__mocks__/features.repository";
import "../__mocks__/getGoogleAppKeys";
import {
setCredentialsMock,
calendarListMock,
getLastCreatedJWT,
getLastCreatedOAuth2Client,
setLastCreatedJWT,
setLastCreatedOAuth2Client,
calendarMock,
adminMock,
MOCK_JWT_TOKEN,
MOCK_OAUTH2_TOKEN,
} from "../__mocks__/googleapis";
import { expect, test, beforeEach, vi, describe } from "vitest";
import "vitest-fetch-mock";
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
import CalendarService from "../CalendarService";
import {
createMockJWTInstance,
createInMemoryDelegationCredentialForCalendarService,
defaultDelegatedCredential,
createCredentialForCalendarService,
} from "./utils";
function expectJWTInstanceToBeCreated() {
expect(getLastCreatedJWT()).toBeDefined();
expect(setCredentialsMock).not.toHaveBeenCalled();
}
function expectOAuth2InstanceToBeCreated() {
expect(setCredentialsMock).toHaveBeenCalled();
expect(getLastCreatedJWT()).toBeNull();
}
function mockSuccessfulCalendarListFetch() {
calendarListMock.mockImplementation(() => {
return {
data: { items: [] },
};
});
}
beforeEach(() => {
vi.clearAllMocks();
setCredentialsMock.mockClear();
calendarMock.calendar_v3.Calendar.mockClear();
adminMock.admin_directory_v1.Admin.mockClear();
setLastCreatedJWT(null);
setLastCreatedOAuth2Client(null);
createMockJWTInstance({});
});
async function expectNoCredentialsInDb() {
const credentials = await prismock.credential.findMany({});
expect(credentials).toHaveLength(0);
}
async function expectCredentialsInDb(credentials: CredentialForCalendarServiceWithEmail[]) {
const credentialsInDb = await prismock.credential.findMany({});
expect(credentialsInDb.length).toBe(credentials.length);
expect(credentialsInDb).toEqual(expect.arrayContaining(credentials));
}
describe("GoogleCalendarService credential handling", () => {
describe("Delegation Credential", () => {
test("uses JWT auth with impersonation when listCalendars is called and creates a new credential in DB with the key as JWT token if it doesn't exist", async () => {
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: "user@example.com" },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
mockSuccessfulCalendarListFetch();
expectNoCredentialsInDb();
const calendarService = new CalendarService(credentialWithDelegation);
await calendarService.listCalendars();
expectJWTInstanceToBeCreated();
await expectCredentialsInDb([
expect.objectContaining({
delegationCredentialId: credentialWithDelegation.delegationCredentialId,
key: MOCK_JWT_TOKEN,
userId: credentialWithDelegation.userId,
}),
]);
});
test("uses JWT auth with impersonation when listCalendars is called and updates the credential in DB with the new JWT token if it exists", async () => {
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: "user@example.com" },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
mockSuccessfulCalendarListFetch();
const existingCredential = await prismock.credential.create({
data: {
type: "google_calendar",
delegationCredentialId: credentialWithDelegation.delegationCredentialId,
userId: credentialWithDelegation.userId,
key: {
access_token: "CURRENT_ACCESS_TOKEN",
},
},
});
const calendarService = new CalendarService(credentialWithDelegation);
await calendarService.listCalendars();
expectJWTInstanceToBeCreated();
await expectCredentialsInDb([
expect.objectContaining({
id: existingCredential.id,
delegationCredentialId: credentialWithDelegation.delegationCredentialId,
key: MOCK_JWT_TOKEN,
userId: credentialWithDelegation.userId,
}),
]);
});
test("JWT token is reused when not expired when listCalendars is called again on a new instance of CalendarService", async () => {
const jwtTokenThatHasNotExpired = {
...MOCK_JWT_TOKEN,
expiry_date: Date.now() + 1000 * 60 * 60 * 24,
};
createMockJWTInstance({
tokenExpiryDate: jwtTokenThatHasNotExpired.expiry_date,
});
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: "user@example.com" },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
console.log("TESTS: credentialWithDelegation", credentialWithDelegation);
mockSuccessfulCalendarListFetch();
await expectNoCredentialsInDb();
console.log("TESTS: First instance of CalendarService");
const calendarService1 = new CalendarService({
...credentialWithDelegation,
});
await calendarService1.listCalendars();
await expectCredentialsInDb([
expect.objectContaining({
delegationCredentialId: credentialWithDelegation.delegationCredentialId,
key: jwtTokenThatHasNotExpired,
userId: credentialWithDelegation.userId,
}),
]);
const existingCredential = await prismock.credential.findFirst({
where: {
delegationCredentialId: credentialWithDelegation.delegationCredentialId,
userId: credentialWithDelegation.userId,
},
});
expect(existingCredential).toBeDefined();
console.log("TESTS: Second instance of CalendarService");
createMockJWTInstance({
authorizeError: {
response: {
data: {
error: "I_WOULD_ERROR_IF_YOU_USE_ME",
},
},
},
});
const calendarService2 = new CalendarService({
...credentialWithDelegation,
});
await calendarService2.listCalendars();
await expectCredentialsInDb([
expect.objectContaining({
// Same credential should be reused
id: existingCredential?.id,
delegationCredentialId: credentialWithDelegation.delegationCredentialId,
key: jwtTokenThatHasNotExpired,
}),
]);
});
});
describe("Non-Delegation Credential", () => {
test("uses OAuth2 auth when listCalendars is called", async () => {
const regularCredential = await createCredentialForCalendarService();
mockSuccessfulCalendarListFetch();
const calendarService = new CalendarService(regularCredential);
await calendarService.listCalendars();
expectOAuth2InstanceToBeCreated();
expect(calendarMock.calendar_v3.Calendar).toHaveBeenCalledWith({
auth: getLastCreatedOAuth2Client(),
});
await expectCredentialsInDb([
expect.objectContaining({
id: regularCredential.id,
key: MOCK_OAUTH2_TOKEN,
}),
]);
});
});
describe("Delegation Credential Error handling", () => {
test("handles clientId not added to Google Workspace Admin Console error", async () => {
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: "user@example.com" },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
createMockJWTInstance({
authorizeError: {
response: {
data: {
error: "unauthorized_client",
},
},
},
});
const calendarService = new CalendarService(credentialWithDelegation);
await expect(calendarService.listCalendars()).rejects.toThrow(
"Make sure that the Client ID for the delegation credential is added to the Google Workspace Admin Console"
);
});
test("handles DelegationCredential authorization errors appropriately", async () => {
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: "user@example.com" },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
createMockJWTInstance({
authorizeError: {
response: {
data: {
error: "unauthorized_client",
},
},
},
});
const calendarService = new CalendarService(credentialWithDelegation);
await expect(calendarService.listCalendars()).rejects.toThrow(
"Make sure that the Client ID for the delegation credential is added to the Google Workspace Admin Console"
);
});
test("handles invalid_grant error (user not in workspace) appropriately", async () => {
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: "user@example.com" },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
createMockJWTInstance({
authorizeError: {
response: {
data: {
error: "invalid_grant",
},
},
},
});
const calendarService = new CalendarService(credentialWithDelegation);
await expect(calendarService.listCalendars()).rejects.toThrow(
`User ${credentialWithDelegation.user?.email} might not exist in Google Workspace`
);
});
test("handles DelegationCredential authorization errors appropriately", async () => {
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: "user@example.com" },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
createMockJWTInstance({
authorizeError: new Error("Some unexpected error"),
});
const calendarService = new CalendarService(credentialWithDelegation);
await expect(calendarService.listCalendars()).rejects.toThrow(
"Error authorizing delegation credential"
);
});
test("On missing user email for DelegationCredential, it should fallback to OAuth2 auth", async () => {
const credentialWithDelegation = await createInMemoryDelegationCredentialForCalendarService({
user: { email: null },
delegatedTo: defaultDelegatedCredential,
delegationCredentialId: "delegation-credential-id-1",
});
const calendarService = new CalendarService(credentialWithDelegation);
mockSuccessfulCalendarListFetch();
await calendarService.listCalendars();
expectOAuth2InstanceToBeCreated();
});
});
});