From e7f958c9cf033d238ee21e7d5e17940dc335a036 Mon Sep 17 00:00:00 2001
From: neo773 <62795688+neo773@users.noreply.github.com>
Date: Wed, 25 Feb 2026 18:32:19 +0530
Subject: [PATCH] Fix Google API Availability and a edge case (#18150)
First run shows a real customer account, and the second run is my
account with all the permissions.
Simulated edge case if user has access to neither
---------
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---
...-apis-service-availability.service.spec.ts | 106 ++++++++++++++++++
...oogle-apis-service-availability.service.ts | 15 ++-
.../auth/services/google-apis.service.ts | 7 ++
3 files changed, 125 insertions(+), 3 deletions(-)
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.spec.ts
index 223f9270924..af629b3a2ba 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.spec.ts
@@ -211,6 +211,112 @@ describe('GoogleApisServiceAvailabilityService', () => {
});
});
+ it('should return messaging unavailable when Google returns generic precondition check failed error', async () => {
+ mockTwentyConfigService.get.mockImplementation((key) => {
+ if (key === 'AUTH_GOOGLE_CLIENT_ID') return 'client-id';
+ if (key === 'AUTH_GOOGLE_CLIENT_SECRET') return 'client-secret';
+ if (key === 'MESSAGING_PROVIDER_GMAIL_ENABLED') return true;
+ if (key === 'CALENDAR_PROVIDER_GOOGLE_ENABLED') return true;
+
+ return undefined;
+ });
+
+ const preconditionCheckFailedError = {
+ response: {
+ status: 400,
+ data: {
+ error: {
+ code: 400,
+ message: 'Precondition check failed.',
+ errors: [
+ {
+ message: 'Precondition check failed.',
+ domain: 'global',
+ reason: 'failedPrecondition',
+ },
+ ],
+ status: 'FAILED_PRECONDITION',
+ },
+ },
+ },
+ };
+
+ const mockGmailClient = {
+ users: {
+ getProfile: jest.fn().mockRejectedValue(preconditionCheckFailedError),
+ },
+ };
+
+ const mockCalendarClient = {
+ events: {
+ list: jest.fn().mockResolvedValue({ data: {} }),
+ },
+ };
+
+ (google.gmail as jest.Mock).mockReturnValue(mockGmailClient);
+ (google.calendar as jest.Mock).mockReturnValue(mockCalendarClient);
+
+ const result = await service.checkServicesAvailability('access-token');
+
+ expect(result).toEqual({
+ isMessagingAvailable: false,
+ isCalendarAvailable: true,
+ });
+ });
+
+ it('should return Calendar unavailable when Google returns generic precondition check failed error', async () => {
+ mockTwentyConfigService.get.mockImplementation((key) => {
+ if (key === 'AUTH_GOOGLE_CLIENT_ID') return 'client-id';
+ if (key === 'AUTH_GOOGLE_CLIENT_SECRET') return 'client-secret';
+ if (key === 'MESSAGING_PROVIDER_GMAIL_ENABLED') return true;
+ if (key === 'CALENDAR_PROVIDER_GOOGLE_ENABLED') return true;
+
+ return undefined;
+ });
+
+ const preconditionCheckFailedError = {
+ response: {
+ status: 400,
+ data: {
+ error: {
+ code: 400,
+ message: 'Precondition check failed.',
+ errors: [
+ {
+ message: 'Precondition check failed.',
+ domain: 'global',
+ reason: 'failedPrecondition',
+ },
+ ],
+ status: 'FAILED_PRECONDITION',
+ },
+ },
+ },
+ };
+
+ const mockGmailClient = {
+ users: {
+ getProfile: jest.fn().mockResolvedValue({ data: {} }),
+ },
+ };
+
+ const mockCalendarClient = {
+ events: {
+ list: jest.fn().mockRejectedValue(preconditionCheckFailedError),
+ },
+ };
+
+ (google.gmail as jest.Mock).mockReturnValue(mockGmailClient);
+ (google.calendar as jest.Mock).mockReturnValue(mockCalendarClient);
+
+ const result = await service.checkServicesAvailability('access-token');
+
+ expect(result).toEqual({
+ isMessagingAvailable: true,
+ isCalendarAvailable: false,
+ });
+ });
+
it('should throw error for non-service-availability related errors', async () => {
mockTwentyConfigService.get.mockImplementation((key) => {
if (key === 'AUTH_GOOGLE_CLIENT_ID') return 'client-id';
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.ts
index 78f96aea8ae..35c89916f5b 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-service-availability.service.ts
@@ -125,9 +125,18 @@ export class GoogleApisServiceAvailabilityService {
}
const isFailedPrecondition = firstError.reason === 'failedPrecondition';
- const isServiceNotEnabled =
- firstError.message?.includes('service not enabled') ?? false;
- return isFailedPrecondition && isServiceNotEnabled;
+ const isServiceNotEnabled =
+ firstError.message?.toLowerCase()?.includes('service not enabled') ??
+ false;
+
+ const isPreconditionCheckFailed =
+ firstError.message
+ ?.toLowerCase()
+ ?.includes('precondition check failed') ?? false;
+
+ return (
+ isFailedPrecondition && (isServiceNotEnabled || isPreconditionCheckFailed)
+ );
}
}
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
index dd0618a6a52..3e62a49eb93 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
@@ -118,6 +118,13 @@ export class GoogleAPIsService {
input.accessToken,
);
+ if (!isMessagingAvailable && !isCalendarAvailable) {
+ throw new AuthException(
+ 'Unable to connect: Your Google account does not have access to Gmail or Calendar. Please contact your workspace administrator.',
+ AuthExceptionCode.INSUFFICIENT_SCOPES,
+ );
+ }
+
const authContext = buildSystemAuthContext(workspaceId);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(