feat: add integration and unit tests for Google unification connectors

- Introduced comprehensive integration tests for Google Drive, Gmail, and Calendar indexers, ensuring proper credential handling for both Composio and native connectors.
- Added unit tests to validate the acceptance of Composio-sourced credentials across various connector types.
- Implemented fixtures to seed test data and facilitate testing of hybrid search functionality, ensuring accurate document type filtering.
This commit is contained in:
Anish Sarkar 2026-03-19 17:51:15 +05:30
parent 83152e8e7e
commit 36f4709225
12 changed files with 1310 additions and 0 deletions

View file

@ -0,0 +1,57 @@
"""Unit tests: build_composio_credentials returns valid Google Credentials.
Mocks the Composio SDK (external system boundary) and verifies that the
returned ``google.oauth2.credentials.Credentials`` object is correctly
configured with a token and a working refresh handler.
"""
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import pytest
from google.oauth2.credentials import Credentials
pytestmark = pytest.mark.unit
@patch("app.services.composio_service.ComposioService")
def test_returns_credentials_with_token_and_expiry(MockComposioService):
"""build_composio_credentials returns a Credentials object with the Composio access token."""
mock_service = MagicMock()
mock_service.get_access_token.return_value = "fake-access-token"
MockComposioService.return_value = mock_service
from app.utils.google_credentials import build_composio_credentials
creds = build_composio_credentials("test-account-id")
assert isinstance(creds, Credentials)
assert creds.token == "fake-access-token"
assert creds.expiry is not None
assert creds.expiry > datetime.now(timezone.utc).replace(tzinfo=None)
@patch("app.services.composio_service.ComposioService")
def test_refresh_handler_fetches_fresh_token(MockComposioService):
"""The refresh_handler on the returned Credentials fetches a new token from Composio."""
mock_service = MagicMock()
mock_service.get_access_token.side_effect = [
"initial-token",
"refreshed-token",
]
MockComposioService.return_value = mock_service
from app.utils.google_credentials import build_composio_credentials
creds = build_composio_credentials("test-account-id")
assert creds.token == "initial-token"
refresh_handler = creds._refresh_handler
assert callable(refresh_handler)
new_token, new_expiry = refresh_handler(request=None, scopes=None)
assert new_token == "refreshed-token"
assert new_expiry > datetime.now(timezone.utc).replace(tzinfo=None)
assert mock_service.get_access_token.call_count == 2

View file

@ -0,0 +1,226 @@
"""Unit tests: Gmail, Calendar, and Drive connectors accept Composio-sourced credentials.
These tests exercise the REAL connector code with Composio-style credentials
(token + expiry + refresh_handler, but NO refresh_token / client_id / client_secret).
Only the Google API boundary (``googleapiclient.discovery.build``) is mocked.
This verifies Phase 2b: the relaxed validation in ``_get_credentials()`` correctly
allows Composio credentials through without raising ValueError or persisting to DB.
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from google.oauth2.credentials import Credentials
pytestmark = pytest.mark.unit
def _utcnow_naive() -> datetime:
"""Return current UTC time as a naive datetime (matches google-auth convention)."""
return datetime.now(timezone.utc).replace(tzinfo=None)
def _composio_credentials(*, expired: bool = False) -> Credentials:
"""Create a Credentials object that mimics build_composio_credentials output."""
if expired:
expiry = _utcnow_naive() - timedelta(hours=1)
else:
expiry = _utcnow_naive() + timedelta(hours=1)
def refresh_handler(request, scopes):
return "refreshed-token", _utcnow_naive() + timedelta(hours=1)
return Credentials(
token="composio-access-token",
expiry=expiry,
refresh_handler=refresh_handler,
)
# ---------------------------------------------------------------------------
# Gmail
# ---------------------------------------------------------------------------
@patch("app.connectors.google_gmail_connector.build")
async def test_gmail_accepts_valid_composio_credentials(mock_build):
"""GoogleGmailConnector.get_user_profile succeeds with Composio credentials
that have no client_id, client_secret, or refresh_token."""
from app.connectors.google_gmail_connector import GoogleGmailConnector
creds = _composio_credentials(expired=False)
mock_service = MagicMock()
mock_service.users.return_value.getProfile.return_value.execute.return_value = {
"emailAddress": "test@example.com",
"messagesTotal": 42,
"threadsTotal": 10,
"historyId": "12345",
}
mock_build.return_value = mock_service
connector = GoogleGmailConnector(
creds, session=MagicMock(), user_id="test-user",
)
profile, error = await connector.get_user_profile()
assert error is None
assert profile["email_address"] == "test@example.com"
mock_build.assert_called_once_with("gmail", "v1", credentials=creds)
@patch("app.connectors.google_gmail_connector.Request")
@patch("app.connectors.google_gmail_connector.build")
async def test_gmail_refreshes_expired_composio_credentials(mock_build, mock_request_cls):
"""GoogleGmailConnector handles expired Composio credentials via refresh_handler
without attempting DB persistence."""
from app.connectors.google_gmail_connector import GoogleGmailConnector
creds = _composio_credentials(expired=True)
assert creds.expired
mock_service = MagicMock()
mock_service.users.return_value.getProfile.return_value.execute.return_value = {
"emailAddress": "test@example.com",
"messagesTotal": 42,
"threadsTotal": 10,
"historyId": "12345",
}
mock_build.return_value = mock_service
mock_session = AsyncMock()
connector = GoogleGmailConnector(
creds, session=mock_session, user_id="test-user",
)
profile, error = await connector.get_user_profile()
assert error is None
assert profile["email_address"] == "test@example.com"
assert creds.token == "refreshed-token"
assert not creds.expired
mock_session.execute.assert_not_called()
mock_session.commit.assert_not_called()
# ---------------------------------------------------------------------------
# Calendar
# ---------------------------------------------------------------------------
@patch("app.connectors.google_calendar_connector.build")
async def test_calendar_accepts_valid_composio_credentials(mock_build):
"""GoogleCalendarConnector.get_calendars succeeds with Composio credentials
that have no client_id, client_secret, or refresh_token."""
from app.connectors.google_calendar_connector import GoogleCalendarConnector
creds = _composio_credentials(expired=False)
mock_service = MagicMock()
mock_service.calendarList.return_value.list.return_value.execute.return_value = {
"items": [{"id": "primary", "summary": "My Calendar", "primary": True}],
}
mock_build.return_value = mock_service
connector = GoogleCalendarConnector(
creds, session=MagicMock(), user_id="test-user",
)
calendars, error = await connector.get_calendars()
assert error is None
assert len(calendars) == 1
assert calendars[0]["summary"] == "My Calendar"
mock_build.assert_called_once_with("calendar", "v3", credentials=creds)
@patch("app.connectors.google_calendar_connector.Request")
@patch("app.connectors.google_calendar_connector.build")
async def test_calendar_refreshes_expired_composio_credentials(mock_build, mock_request_cls):
"""GoogleCalendarConnector handles expired Composio credentials via refresh_handler
without attempting DB persistence."""
from app.connectors.google_calendar_connector import GoogleCalendarConnector
creds = _composio_credentials(expired=True)
assert creds.expired
mock_service = MagicMock()
mock_service.calendarList.return_value.list.return_value.execute.return_value = {
"items": [{"id": "primary", "summary": "My Calendar", "primary": True}],
}
mock_build.return_value = mock_service
mock_session = AsyncMock()
connector = GoogleCalendarConnector(
creds, session=mock_session, user_id="test-user",
)
calendars, error = await connector.get_calendars()
assert error is None
assert len(calendars) == 1
assert creds.token == "refreshed-token"
assert not creds.expired
mock_session.execute.assert_not_called()
mock_session.commit.assert_not_called()
# ---------------------------------------------------------------------------
# Drive
# ---------------------------------------------------------------------------
@patch("app.connectors.google_drive.client.build")
async def test_drive_client_uses_prebuilt_composio_credentials(mock_build):
"""GoogleDriveClient with pre-built Composio credentials uses them directly,
bypassing DB credential loading via get_valid_credentials."""
from app.connectors.google_drive.client import GoogleDriveClient
creds = _composio_credentials(expired=False)
mock_service = MagicMock()
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [],
"nextPageToken": None,
}
mock_build.return_value = mock_service
client = GoogleDriveClient(
session=MagicMock(), connector_id=999, credentials=creds,
)
files, next_token, error = await client.list_files()
assert error is None
assert files == []
mock_build.assert_called_once_with("drive", "v3", credentials=creds)
@patch("app.connectors.google_drive.client.get_valid_credentials")
@patch("app.connectors.google_drive.client.build")
async def test_drive_client_prebuilt_creds_skip_db_loading(mock_build, mock_get_valid):
"""GoogleDriveClient does NOT call get_valid_credentials when pre-built
credentials are provided."""
from app.connectors.google_drive.client import GoogleDriveClient
creds = _composio_credentials(expired=False)
mock_service = MagicMock()
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [],
"nextPageToken": None,
}
mock_build.return_value = mock_service
client = GoogleDriveClient(
session=MagicMock(), connector_id=999, credentials=creds,
)
await client.list_files()
mock_get_valid.assert_not_called()

View file

@ -0,0 +1,53 @@
"""Unit tests: connector type acceptance sets include both native and Composio types.
The indexer ``ACCEPTED_*_CONNECTOR_TYPES`` sets and the shared
``COMPOSIO_GOOGLE_CONNECTOR_TYPES`` set are the constants that control
whether a connector is accepted by an indexer and which credential path
is used. These tests verify those sets are correctly defined so that
both native and Composio connectors are handled by the unified pipeline.
"""
import pytest
from app.db import SearchSourceConnectorType
pytestmark = pytest.mark.unit
def test_drive_indexer_accepts_both_native_and_composio():
"""ACCEPTED_DRIVE_CONNECTOR_TYPES should include both native and Composio Drive types."""
from app.tasks.connector_indexers.google_drive_indexer import (
ACCEPTED_DRIVE_CONNECTOR_TYPES,
)
assert SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES
def test_gmail_indexer_accepts_both_native_and_composio():
"""ACCEPTED_GMAIL_CONNECTOR_TYPES should include both native and Composio Gmail types."""
from app.tasks.connector_indexers.google_gmail_indexer import (
ACCEPTED_GMAIL_CONNECTOR_TYPES,
)
assert SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES
assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES
def test_calendar_indexer_accepts_both_native_and_composio():
"""ACCEPTED_CALENDAR_CONNECTOR_TYPES should include both native and Composio Calendar types."""
from app.tasks.connector_indexers.google_calendar_indexer import (
ACCEPTED_CALENDAR_CONNECTOR_TYPES,
)
assert SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES
def test_composio_connector_types_set_covers_all_google_services():
"""COMPOSIO_GOOGLE_CONNECTOR_TYPES should contain all three Composio Google types."""
from app.utils.google_credentials import COMPOSIO_GOOGLE_CONNECTOR_TYPES
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES