From 486817a688f3de79afce98904fccd08062652102 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Wed, 31 Dec 2025 14:58:20 -0300 Subject: [PATCH 1/3] fix: add timeout: 0 to E2E assertions that check for absence of elements (#26353) This fixes hanging E2E tests where assertions like toHaveCount(0) and not.toBeVisible() would wait for the full timeout (10s in CI, 120s locally) before failing, instead of failing immediately. These assertions are 'state check' assertions that verify an element is already absent or hidden, rather than waiting for it to become so. Adding { timeout: 0 } makes them fail immediately if the condition is not met. Files updated: - locale.e2e.ts: 16 instances of toHaveCount(0) - booking-seats.e2e.ts: 8 instances of toHaveCount(0) - organization-redirection.e2e.ts: 3 instances of toHaveCount(0) - organization-creation-flows.e2e.ts: 5 instances of not.toBeVisible() - insights-charts.e2e.ts: 1 instance of toHaveCount(0) - bookings-list.e2e.ts: 1 instance of toHaveCount(0) - availability.e2e.ts: 1 instance of toHaveCount(0) - managed-event-types.e2e.ts: 1 instance of toHaveCount(0) - team-invitation.e2e.ts: 1 instance of toHaveCount(0) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/playwright/availability.e2e.ts | 2 +- apps/web/playwright/booking-seats.e2e.ts | 16 ++++++------ apps/web/playwright/bookings-list.e2e.ts | 2 +- apps/web/playwright/insights-charts.e2e.ts | 2 +- apps/web/playwright/locale.e2e.ts | 26 +++++++++---------- .../web/playwright/managed-event-types.e2e.ts | 2 +- .../organization-creation-flows.e2e.ts | 10 +++---- .../organization-redirection.e2e.ts | 6 ++--- .../playwright/team/team-invitation.e2e.ts | 2 +- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/web/playwright/availability.e2e.ts b/apps/web/playwright/availability.e2e.ts index cabed0e278568c..f0ae7dc84636df 100644 --- a/apps/web/playwright/availability.e2e.ts +++ b/apps/web/playwright/availability.e2e.ts @@ -124,7 +124,7 @@ test.describe("Availability", () => { await submitAndWaitForResponse(page, "/api/trpc/availability/schedule.delete?batch=1", { action: () => page.locator('[data-testid="delete-schedule"]').click(), }); - await expect(page.locator('[data-testid="schedules"] > li').nth(1)).toHaveCount(0); + await expect(page.locator('[data-testid="schedules"] > li').nth(1)).toHaveCount(0, { timeout: 0 }); }); await test.step("Cannot delete the last schedule", async () => { await page.locator('[data-testid="schedules"] > li').nth(0).getByTestId("schedule-more").click(); diff --git a/apps/web/playwright/booking-seats.e2e.ts b/apps/web/playwright/booking-seats.e2e.ts index d4cf1a3ed5b96b..80fbd72c86ea41 100644 --- a/apps/web/playwright/booking-seats.e2e.ts +++ b/apps/web/playwright/booking-seats.e2e.ts @@ -64,14 +64,14 @@ test.describe("Booking with Seats", () => { await test.step("Attendee #2 shouldn't be able to cancel booking using only booking/uid", async () => { await page.goto(`/booking/${booking.uid}`); - await expect(page.locator("[text=Cancel]")).toHaveCount(0); + await expect(page.locator("[text=Cancel]")).toHaveCount(0, { timeout: 0 }); }); await test.step("Attendee #2 shouldn't be able to cancel booking using randomString for seatReferenceUId", async () => { await page.goto(`/booking/${booking.uid}?seatReferenceUid=${randomString(10)}`); // expect cancel button to don't be in the page - await expect(page.locator("[text=Cancel]")).toHaveCount(0); + await expect(page.locator("[text=Cancel]")).toHaveCount(0, { timeout: 0 }); }); }); @@ -82,7 +82,7 @@ test.describe("Booking with Seats", () => { { name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" }, ]); await page.goto(`/booking/${booking.uid}?cancel=true`); - await expect(page.locator("[text=Cancel]")).toHaveCount(0); + await expect(page.locator("[text=Cancel]")).toHaveCount(0, { timeout: 0 }); // expect login text to be in the page, not data-testid await expect(page.locator("text=Login")).toHaveCount(1); @@ -99,7 +99,7 @@ test.describe("Booking with Seats", () => { await page.goto(`/booking/${booking.uid}?cancel=true`); // expect login button to don't be in the page - await expect(page.locator("text=Login")).toHaveCount(0); + await expect(page.locator("text=Login")).toHaveCount(0, { timeout: 0 }); // fill reason for cancellation await page.fill('[data-testid="cancel_reason"]', "Double booked!"); @@ -183,7 +183,7 @@ test.describe("Reschedule for booking with seats", () => { await page.locator('[data-testid="day"][data-disabled="false"]').nth(1).click(); // Validate that the number of seats its 10 - await expect(page.locator("text=9 / 10 Seats available")).toHaveCount(0); + await expect(page.locator("text=9 / 10 Seats available")).toHaveCount(0, { timeout: 0 }); }); test("Should cancel with seats but event should be still accessible and with one less attendee/seat", async ({ @@ -325,7 +325,7 @@ test.describe("Reschedule for booking with seats", () => { // No attendees should be displayed only the one that it's cancelling const notFoundSecondAttendee = await page.locator('p[data-testid="attendee-email-second+seats@cal.com"]'); - await expect(notFoundSecondAttendee).toHaveCount(0); + await expect(notFoundSecondAttendee).toHaveCount(0, { timeout: 0 }); const foundFirstAttendee = await page.locator('p[data-testid="attendee-email-first+seats@cal.com"]'); await expect(foundFirstAttendee).toHaveCount(1); @@ -365,7 +365,7 @@ test.describe("Reschedule for booking with seats", () => { const getBooking = await booking.self(); await page.goto(`/booking/${booking.uid}`); - await expect(page.locator('[data-testid="reschedule"]')).toHaveCount(0); + await expect(page.locator('[data-testid="reschedule"]')).toHaveCount(0, { timeout: 0 }); // expect login text to be in the page, not data-testid await expect(page.locator("text=Login")).toHaveCount(1); @@ -382,7 +382,7 @@ test.describe("Reschedule for booking with seats", () => { await page.goto(`/booking/${booking.uid}`); // expect login button to don't be in the page - await expect(page.locator("text=Login")).toHaveCount(0); + await expect(page.locator("text=Login")).toHaveCount(0, { timeout: 0 }); // reschedule-link click await page.locator('[data-testid="reschedule-link"]').click(); diff --git a/apps/web/playwright/bookings-list.e2e.ts b/apps/web/playwright/bookings-list.e2e.ts index 2a8c3b7c652406..ca7134915a0daf 100644 --- a/apps/web/playwright/bookings-list.e2e.ts +++ b/apps/web/playwright/bookings-list.e2e.ts @@ -455,7 +455,7 @@ test.describe("Bookings", () => { .click(); await bookingsGetResponse2; - await expect(page.locator('[data-testid="booking-item"]')).toHaveCount(0); + await expect(page.locator('[data-testid="booking-item"]')).toHaveCount(0, { timeout: 0 }); }); test.describe("Filter Dropdown Item Search", () => { diff --git a/apps/web/playwright/insights-charts.e2e.ts b/apps/web/playwright/insights-charts.e2e.ts index 8860355d34dcfe..eb87071600508c 100644 --- a/apps/web/playwright/insights-charts.e2e.ts +++ b/apps/web/playwright/insights-charts.e2e.ts @@ -282,7 +282,7 @@ test.describe("Insights > Charts Loading", () => { // Verify no charts are in error state const errorCharts = page.locator('[data-testid="chart-card"][data-loading-state="error"]'); - await expect(errorCharts).toHaveCount(0); + await expect(errorCharts).toHaveCount(0, { timeout: 0 }); }); }); }); diff --git a/apps/web/playwright/locale.e2e.ts b/apps/web/playwright/locale.e2e.ts index 89175eec31afc0..ec4206bf624e02 100644 --- a/apps/web/playwright/locale.e2e.ts +++ b/apps/web/playwright/locale.e2e.ts @@ -239,7 +239,7 @@ test.describe("authorized user sees correct translations (de)", async () => { { const locator = page.getByText("Event Types", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -258,7 +258,7 @@ test.describe("authorized user sees correct translations (de)", async () => { { const locator = page.getByText("No upcoming bookings", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -277,7 +277,7 @@ test.describe("authorized user sees correct translations (de)", async () => { { const locator = page.getByText("No upcoming bookings", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); }); @@ -311,7 +311,7 @@ test.describe("authorized user sees correct translations (pt-br)", async () => { { const locator = page.getByText("Event Types", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -330,7 +330,7 @@ test.describe("authorized user sees correct translations (pt-br)", async () => { { const locator = page.getByText("Bookings", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -349,7 +349,7 @@ test.describe("authorized user sees correct translations (pt-br)", async () => { { const locator = page.getByText("Bookings", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); }); @@ -383,7 +383,7 @@ test.describe("authorized user sees correct translations (ar)", async () => { { const locator = page.getByText("Event Types", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -402,7 +402,7 @@ test.describe("authorized user sees correct translations (ar)", async () => { { const locator = page.getByText("Bookings", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -421,7 +421,7 @@ test.describe("authorized user sees correct translations (ar)", async () => { { const locator = page.getByText("Bookings", { exact: true }); - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); }); @@ -463,7 +463,7 @@ test.describe("authorized user sees changed translations (de->ar)", async () => { const locator = page.getByText("Allgemein", { exact: true }); // "general" - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -482,7 +482,7 @@ test.describe("authorized user sees changed translations (de->ar)", async () => { const locator = page.getByText("Allgemein", { exact: true }); // "general" - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); }); @@ -522,7 +522,7 @@ test.describe("authorized user sees changed translations (de->pt-BR) [locale1]", { const locator = page.getByText("Allgemein", { exact: true }); // "general" - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); @@ -541,7 +541,7 @@ test.describe("authorized user sees changed translations (de->pt-BR) [locale1]", { const locator = page.getByText("Allgemein", { exact: true }); // "general" - await expect(locator).toHaveCount(0); + await expect(locator).toHaveCount(0, { timeout: 0 }); } }); }); diff --git a/apps/web/playwright/managed-event-types.e2e.ts b/apps/web/playwright/managed-event-types.e2e.ts index 2e6898dabd02a8..f4042ec9c338d8 100644 --- a/apps/web/playwright/managed-event-types.e2e.ts +++ b/apps/web/playwright/managed-event-types.e2e.ts @@ -129,7 +129,7 @@ test.describe("Managed Event Types", () => { // Proceed to unlock and check that it got unlocked titleLockIndicator.click(); - await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(0); + await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(0, { timeout: 0 }); await expect(titleLockIndicator.locator("[data-state='unchecked']")).toHaveCount(1); // Save changes diff --git a/apps/web/playwright/organization/organization-creation-flows.e2e.ts b/apps/web/playwright/organization/organization-creation-flows.e2e.ts index 95ed2f0f65c577..5870446b139e37 100644 --- a/apps/web/playwright/organization/organization-creation-flows.e2e.ts +++ b/apps/web/playwright/organization/organization-creation-flows.e2e.ts @@ -242,8 +242,8 @@ test.describe("Organization Creation Flows - Comprehensive Suite", () => { // Verify billing fields are NOT visible to regular users if (IS_TEAM_BILLING_ENABLED) { - await expect(page.locator("input[name=seats]")).not.toBeVisible(); - await expect(page.locator("input[name=pricePerSeat]")).not.toBeVisible(); + await expect(page.locator("input[name=seats]")).not.toBeVisible({ timeout: 0 }); + await expect(page.locator("input[name=pricePerSeat]")).not.toBeVisible({ timeout: 0 }); // Verify "Upgrade to Organizations" UI is shown await expect(page.getByText(/upgrade to organizations/i)).toBeVisible(); @@ -439,9 +439,9 @@ test.describe("Organization Creation Flows - Comprehensive Suite", () => { await page.goto("/settings/organizations/new"); // Should NOT see admin billing fields - await expect(page.locator("input[name=seats]")).not.toBeVisible(); - await expect(page.locator("input[name=pricePerSeat]")).not.toBeVisible(); - await expect(page.locator("#billingPeriod")).not.toBeVisible(); + await expect(page.locator("input[name=seats]")).not.toBeVisible({ timeout: 0 }); + await expect(page.locator("input[name=pricePerSeat]")).not.toBeVisible({ timeout: 0 }); + await expect(page.locator("#billingPeriod")).not.toBeVisible({ timeout: 0 }); // Should see upgrade/pricing UI await expect(page.getByText(/upgrade to organizations/i)).toBeVisible(); diff --git a/apps/web/playwright/organization/organization-redirection.e2e.ts b/apps/web/playwright/organization/organization-redirection.e2e.ts index 2d25b7c202b1a7..e2d09e6dc4aa47 100644 --- a/apps/web/playwright/organization/organization-redirection.e2e.ts +++ b/apps/web/playwright/organization/organization-redirection.e2e.ts @@ -57,7 +57,7 @@ test.describe("Unpublished Organization Redirection", () => { await expect(page.getByTestId("empty-screen")).toBeVisible(); // Ensure that the team name is not displayed. - await expect(page.getByTestId("team-name")).toHaveCount(0); + await expect(page.getByTestId("team-name")).toHaveCount(0, { timeout: 0 }); }); }); @@ -101,8 +101,8 @@ test.describe("Unpublished Organization Redirection", () => { await expect(page.getByTestId("empty-screen")).toBeVisible(); // Ensure user profile elements are not visible. - await expect(page.locator('[data-testid="name-title"]')).toHaveCount(0); - await expect(page.locator('[data-testid="event-types"]')).toHaveCount(0); + await expect(page.locator('[data-testid="name-title"]')).toHaveCount(0, { timeout: 0 }); + await expect(page.locator('[data-testid="event-types"]')).toHaveCount(0, { timeout: 0 }); }); }); diff --git a/apps/web/playwright/team/team-invitation.e2e.ts b/apps/web/playwright/team/team-invitation.e2e.ts index dd958496c19657..73d02c198efba6 100644 --- a/apps/web/playwright/team/team-invitation.e2e.ts +++ b/apps/web/playwright/team/team-invitation.e2e.ts @@ -74,7 +74,7 @@ test.describe("Team", () => { await page.goto(`/settings/teams/${team.id}/settings`); await expect( page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) - ).toHaveCount(0); + ).toHaveCount(0, { timeout: 0 }); }); await test.step("To the team by invite link", async () => { From b08997b553b2574e79fdbd36e223333a19fd8aae Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Wed, 31 Dec 2025 16:12:28 -0300 Subject: [PATCH 2/3] fix: remove debug logs and clean up verbose logging (#25896) - Remove debug console.log statements in calendar and video adapter services - Clean up verbose request/response logging in OAuth controllers - Remove leftover debug prefixes Ensure only necessary data is captured in observability systems Co-authored-by: Keith Williams --- .../v2/src/ee/calendars/services/calendars.service.ts | 1 - .../oauth-client-users.controller.ts | 6 ++---- .../oauth-clients/oauth-clients.controller.ts | 6 ++---- .../event-types/services/teams-event-types.service.ts | 1 - packages/app-store/alby/lib/PaymentService.ts | 1 - .../app-store/office365video/lib/VideoApiAdapter.ts | 5 ----- packages/app-store/webex/lib/VideoApiAdapter.ts | 11 ++--------- 7 files changed, 6 insertions(+), 25 deletions(-) diff --git a/apps/api/v2/src/ee/calendars/services/calendars.service.ts b/apps/api/v2/src/ee/calendars/services/calendars.service.ts index ec4f62dbdece5d..727dd951c476a4 100644 --- a/apps/api/v2/src/ee/calendars/services/calendars.service.ts +++ b/apps/api/v2/src/ee/calendars/services/calendars.service.ts @@ -75,7 +75,6 @@ export class CalendarsService { eventTypeId: null, prisma: this.dbWrite.prisma as unknown as PrismaClient, }); - console.log("saving cache", JSON.stringify(result)); await this.calendarsCacheService.setConnectedAndDestinationCalendarsCache(userId, result); return result; diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index b3f807bbc594ce..09fdc70d018962 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -84,9 +84,7 @@ export class OAuthClientUsersController { @Param("clientId") oAuthClientId: string, @Body() body: CreateManagedUserInput ): Promise { - this.logger.log( - `Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}` - ); + this.logger.log(`Creating user for OAuth Client ${oAuthClientId}`); const client = await this.oauthRepository.getOAuthClient(oAuthClientId); if (!client) { throw new NotFoundException(`OAuth Client with ID ${oAuthClientId} not found`); @@ -133,7 +131,7 @@ export class OAuthClientUsersController { @GetOrgId() organizationId: number ): Promise { await this.validateManagedUserOwnership(clientId, userId); - this.logger.log(`Updating user with ID ${userId}: ${JSON.stringify(body, null, 2)}`); + this.logger.log(`Updating user ${userId} for OAuth Client ${clientId}`); const user = await this.oAuthClientUsersService.updateOAuthClientUser( clientId, diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts index d84291fff95f19..cb7658b3984466 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts @@ -69,9 +69,7 @@ export class OAuthClientsController { @GetOrgId() organizationId: number, @Body() body: CreateOAuthClientInput ): Promise { - this.logger.log( - `For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}` - ); + this.logger.log(`Creating OAuth Client for organisation ${organizationId}`); const organization = await this.teamsRepository.findByIdIncludeBilling(organizationId); if (!organization?.platformBilling || !organization?.platformBilling?.subscriptionId) { @@ -140,7 +138,7 @@ export class OAuthClientsController { @Param("clientId") clientId: string, @Body() body: UpdateOAuthClientInput ): Promise { - this.logger.log(`For client ${clientId} updating OAuth Client with data: ${JSON.stringify(body)}`); + this.logger.log(`Updating OAuth Client ${clientId}`); const client = await this.oAuthClientsService.updateOAuthClient(clientId, body); return { status: SUCCESS_STATUS, data: client }; } diff --git a/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts b/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts index 735cf9dd902b70..95e775d59c81e7 100644 --- a/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts +++ b/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts @@ -135,7 +135,6 @@ export class TeamsEventTypesService { }); const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId); - this.logger.debug("nl debug - update team event type - eventType", JSON.stringify(eventType, null, 2)); if (!eventType) { throw new NotFoundException(`Event type with id ${eventTypeId} not found`); diff --git a/packages/app-store/alby/lib/PaymentService.ts b/packages/app-store/alby/lib/PaymentService.ts index a25ed825ade376..faed5713d6bb07 100644 --- a/packages/app-store/alby/lib/PaymentService.ts +++ b/packages/app-store/alby/lib/PaymentService.ts @@ -55,7 +55,6 @@ export class PaymentService implements IAbstractPaymentService { referenceId: uid, }, }); - console.log("Created invoice", invoice, uid); const paymentData = await prisma.payment.create({ data: { diff --git a/packages/app-store/office365video/lib/VideoApiAdapter.ts b/packages/app-store/office365video/lib/VideoApiAdapter.ts index 4206e0ee7b23e3..b656707ae3acc8 100644 --- a/packages/app-store/office365video/lib/VideoApiAdapter.ts +++ b/packages/app-store/office365video/lib/VideoApiAdapter.ts @@ -38,7 +38,6 @@ const getO365VideoAppKeys = async () => { }; const TeamsVideoApiAdapter = (credential: CredentialForCalendarServiceWithTenantId): VideoApiAdapter => { - console.log("TeamsVideoApiAdapter--credential: ", credential); let azureUserId: string | null; const tokenResponse = oAuthManagerHelper.getTokenObjectFromCredential(credential); @@ -251,11 +250,7 @@ const TeamsVideoApiAdapter = (credential: CredentialForCalendarServiceWithTenant return Promise.resolve([]); }, createMeeting: async (event: CalendarEvent): Promise => { - console.log("=======>createMeeting: "); - const url = `${await getUserEndpoint()}/onlineMeetings`; - console.log("urllllllllllll: ", url); - console.log("translateEvent(event): ", translateEvent(event)); const resultString = await auth .requestRaw({ url, diff --git a/packages/app-store/webex/lib/VideoApiAdapter.ts b/packages/app-store/webex/lib/VideoApiAdapter.ts index eb7921263a63bd..bef86518f4cc8d 100644 --- a/packages/app-store/webex/lib/VideoApiAdapter.ts +++ b/packages/app-store/webex/lib/VideoApiAdapter.ts @@ -112,7 +112,7 @@ const webexAuth = (credential: CredentialPayload) => { let credentialKey: WebexToken | null = null; try { credentialKey = webexTokenSchema.parse(credential.key); - } catch (error) { + } catch { return Promise.reject("Webex credential keys parsing error"); } @@ -148,8 +148,6 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => const fetchWebexApi = async (endpoint: string, options?: RequestInit) => { const auth = webexAuth(credential); const accessToken = await auth.getToken(); - console.log("result of accessToken in fetchWebexApi", accessToken); - console.log("createMeeting options in fetchWebexApi", options); const response = await fetch(`https://webexapis.com/v1/${endpoint}`, { method: "GET", ...options, @@ -181,9 +179,6 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => createMeeting: async (event: CalendarEvent): Promise => { /** @link https://developer.webex.com/docs/api/v1/meetings/create-a-meeting */ try { - console.log("Creating meeting", event); - console.log("meting body", translateEvent(event)); - console.log("request body in createMeeting", JSON.stringify(translateEvent(event))); const response = await fetchWebexApi("meetings", { method: "POST", headers: { @@ -191,7 +186,6 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => }, body: JSON.stringify(translateEvent(event)), }); - console.log("Webex create meeting response", response); if (response.error) { if (response.error === "invalid_grant") { await invalidateCredential(credential.id); @@ -220,7 +214,6 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => const response = await fetchWebexApi(`meetings/${uid}`, { method: "DELETE", }); - console.log("Webex delete meeting response", response); if (response.error) { if (response.error === "invalid_grant") { await invalidateCredential(credential.id); @@ -228,7 +221,7 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => } } return Promise.resolve(); - } catch (err) { + } catch { return Promise.reject(new Error("Failed to delete meeting")); } }, From 128d15ea8fceada05f1e3fd921d659630b794c68 Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Wed, 31 Dec 2025 16:30:26 -0300 Subject: [PATCH 3/3] fix: ensure proper async handling in delegation credentials (#25898) * fix: ensure proper async handling in ensureDefaultCalendars Replace forEach(async...) with Promise.allSettled to ensure: - Caller properly awaits completion - Errors are captured and logged - All users are processed even if some fail Adds unit tests for ensureDefaultCalendars * fix: use correct Jest assertion pattern for async test Co-Authored-By: keith@cal.com --------- Co-authored-by: Keith Williams Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ...ions-delegation-credential.service.spec.ts | 138 ++++++++++++++++++ ...nizations-delegation-credential.service.ts | 43 ++++-- 2 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.spec.ts diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.spec.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.spec.ts new file mode 100644 index 00000000000000..31841d46696b07 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.spec.ts @@ -0,0 +1,138 @@ +import { + CALENDARS_QUEUE, + DEFAULT_CALENDARS_JOB, +} from "@/ee/calendars/processors/calendars.processor"; +import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; +import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; +import { Logger } from "@nestjs/common"; +import { getQueueToken } from "@nestjs/bull"; +import { Test, TestingModule } from "@nestjs/testing"; + +describe("OrganizationsDelegationCredentialService", () => { + let service: OrganizationsDelegationCredentialService; + let mockRepository: OrganizationsDelegationCredentialRepository; + let mockQueue: { getJob: jest.Mock; add: jest.Mock }; + + const orgId = 1; + const domain = "example.com"; + + beforeEach(async () => { + mockQueue = { + getJob: jest.fn().mockResolvedValue(null), + add: jest.fn().mockResolvedValue(undefined), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OrganizationsDelegationCredentialService, + { + provide: OrganizationsDelegationCredentialRepository, + useValue: { + findDelegatedUserProfiles: jest.fn().mockResolvedValue([]), + }, + }, + { + provide: getQueueToken(CALENDARS_QUEUE), + useValue: mockQueue, + }, + ], + }).compile(); + + service = module.get( + OrganizationsDelegationCredentialService + ); + mockRepository = module.get( + OrganizationsDelegationCredentialRepository + ); + + jest.spyOn(Logger.prototype, "log").mockImplementation(); + jest.spyOn(Logger.prototype, "error").mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("ensureDefaultCalendars", () => { + it("adds calendar jobs for each delegated user profile", async () => { + (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([ + { userId: 1 }, + { userId: 2 }, + ]); + + await service.ensureDefaultCalendars(orgId, domain); + + expect(mockRepository.findDelegatedUserProfiles).toHaveBeenCalledWith(orgId, domain); + expect(mockQueue.add).toHaveBeenCalledTimes(2); + expect(mockQueue.add).toHaveBeenCalledWith( + DEFAULT_CALENDARS_JOB, + { userId: 1 }, + { jobId: `${DEFAULT_CALENDARS_JOB}_1`, removeOnComplete: true } + ); + expect(mockQueue.add).toHaveBeenCalledWith( + DEFAULT_CALENDARS_JOB, + { userId: 2 }, + { jobId: `${DEFAULT_CALENDARS_JOB}_2`, removeOnComplete: true } + ); + }); + + it("removes existing job before adding new one", async () => { + const mockExistingJob = { remove: jest.fn().mockResolvedValue(undefined) }; + (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([{ userId: 1 }]); + mockQueue.getJob.mockResolvedValue(mockExistingJob); + + await service.ensureDefaultCalendars(orgId, domain); + + expect(mockExistingJob.remove).toHaveBeenCalled(); + expect(mockQueue.add).toHaveBeenCalledTimes(1); + }); + + it("skips profiles without userId", async () => { + (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([ + { userId: 1 }, + { userId: null }, + { userId: 3 }, + ]); + + await service.ensureDefaultCalendars(orgId, domain); + + expect(mockQueue.add).toHaveBeenCalledTimes(2); + expect(mockQueue.add).toHaveBeenCalledWith(DEFAULT_CALENDARS_JOB, { userId: 1 }, expect.any(Object)); + expect(mockQueue.add).toHaveBeenCalledWith(DEFAULT_CALENDARS_JOB, { userId: 3 }, expect.any(Object)); + }); + + it("does not add jobs when profiles list is empty", async () => { + (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([]); + + await service.ensureDefaultCalendars(orgId, domain); + + expect(mockQueue.add).not.toHaveBeenCalled(); + }); + + it("processes all profiles even when some fail (Promise.allSettled)", async () => { + (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([ + { userId: 1 }, + { userId: 2 }, + { userId: 3 }, + ]); + mockQueue.add + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error("Queue error")) + .mockResolvedValueOnce(undefined); + + await service.ensureDefaultCalendars(orgId, domain); + + // All 3 jobs were attempted despite the failure + expect(mockQueue.add).toHaveBeenCalledTimes(3); + }); + + it("does not throw when repository fails", async () => { + (mockRepository.findDelegatedUserProfiles as jest.Mock).mockRejectedValue( + new Error("Database error") + ); + + await expect(service.ensureDefaultCalendars(orgId, domain)).resolves.toBeUndefined(); + expect(mockQueue.add).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts index 6541b40ef9ece3..6ddd955147e85b 100644 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts +++ b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts @@ -87,23 +87,34 @@ export class OrganizationsDelegationCredentialService { const delegatedUserProfiles = await this.organizationsDelegationCredentialRepository.findDelegatedUserProfiles(orgId, domain); - delegatedUserProfiles.forEach(async (profile) => { - if (profile.userId) { - const job = await this.calendarsQueue.getJob(`${DEFAULT_CALENDARS_JOB}_${profile.userId}`); - if (job) { - await job.remove(); - this.logger.log(`Removed default calendar job for user with id: ${profile.userId}`); + const results = await Promise.allSettled( + delegatedUserProfiles.map(async (profile) => { + if (profile.userId) { + const job = await this.calendarsQueue.getJob(`${DEFAULT_CALENDARS_JOB}_${profile.userId}`); + if (job) { + await job.remove(); + this.logger.log(`Removed default calendar job for user with id: ${profile.userId}`); + } + this.logger.log(`Adding default calendar job for user with id: ${profile.userId}`); + await this.calendarsQueue.add( + DEFAULT_CALENDARS_JOB, + { + userId: profile.userId, + } satisfies DefaultCalendarsJobDataType, + { jobId: `${DEFAULT_CALENDARS_JOB}_${profile.userId}`, removeOnComplete: true } + ); } - this.logger.log(`Adding default calendar job for user with id: ${profile.userId}`); - await this.calendarsQueue.add( - DEFAULT_CALENDARS_JOB, - { - userId: profile.userId, - } satisfies DefaultCalendarsJobDataType, - { jobId: `${DEFAULT_CALENDARS_JOB}_${profile.userId}`, removeOnComplete: true } - ); - } - }); + }) + ); + + const failures = results.filter( + (result): result is PromiseRejectedResult => result.status === "rejected" + ); + if (failures.length > 0) { + this.logger.error( + `Failed to ensure default calendars for ${failures.length} users in org ${orgId}: ${failures.map((f) => f.reason).join(", ")}` + ); + } } catch (err) { this.logger.error( err,