mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 08:46:22 +02:00
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:
parent
83152e8e7e
commit
36f4709225
12 changed files with 1310 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue