5711a56569
Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/4d504045-2740-4bab-b51e-960fe4dafe4b Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
205 lines
7.3 KiB
Python
205 lines
7.3 KiB
Python
"""
|
|
Unit tests for Gmail service module.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from app.services.gmail_service import GmailService, GmailInjectionError, GMAIL_SCOPES
|
|
from app.utils.gmail_labels import (
|
|
DEFAULT_IMPORT_LABEL_TEMPLATES,
|
|
SOURCE_EMAIL_LABEL_TEMPLATE,
|
|
build_gmail_credential_scopes,
|
|
extract_granted_scopes,
|
|
extract_import_label_templates,
|
|
render_import_labels,
|
|
)
|
|
|
|
|
|
class TestGmailService:
|
|
"""Test Gmail API service"""
|
|
|
|
def test_gmail_scopes(self):
|
|
"""Test that required Gmail scopes are defined"""
|
|
assert "https://www.googleapis.com/auth/gmail.insert" in GMAIL_SCOPES
|
|
assert "https://www.googleapis.com/auth/gmail.labels" in GMAIL_SCOPES
|
|
|
|
def test_init_creates_credentials(self):
|
|
"""Test that GmailService initializes with credentials"""
|
|
service = GmailService(
|
|
access_token="test-access-token",
|
|
refresh_token="test-refresh-token",
|
|
client_id="test-client-id",
|
|
client_secret="test-client-secret",
|
|
)
|
|
|
|
assert service.credentials is not None
|
|
assert service.credentials.token == "test-access-token"
|
|
assert service.credentials.refresh_token == "test-refresh-token"
|
|
assert service.credentials.client_id == "test-client-id"
|
|
assert service.credentials.client_secret == "test-client-secret"
|
|
|
|
def test_init_without_refresh_token(self):
|
|
"""Test initialization without refresh token"""
|
|
service = GmailService(access_token="test-access-token")
|
|
|
|
assert service.credentials is not None
|
|
assert service.credentials.token == "test-access-token"
|
|
assert service.credentials.refresh_token is None
|
|
|
|
def test_service_lazy_initialization(self):
|
|
"""Test that the API service is not created until accessed"""
|
|
service = GmailService(access_token="test-access-token")
|
|
assert service._service is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inject_email_success(self):
|
|
"""Test successful email injection"""
|
|
service = GmailService(access_token="test-access-token")
|
|
|
|
mock_api = MagicMock()
|
|
mock_api.users().messages().insert().execute.return_value = {
|
|
"id": "msg123",
|
|
"threadId": "thread456",
|
|
"labelIds": ["INBOX"],
|
|
}
|
|
service._service = mock_api
|
|
|
|
result = await service.inject_email(
|
|
raw_email=b"From: test@example.com\r\nSubject: Test\r\n\r\nHello",
|
|
label_ids=["INBOX"],
|
|
source_account_name="Test Account",
|
|
)
|
|
|
|
assert result["message_id"] == "msg123"
|
|
assert result["thread_id"] == "thread456"
|
|
assert "INBOX" in result["label_ids"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inject_email_default_labels(self):
|
|
"""Test that INBOX is used as default label"""
|
|
service = GmailService(access_token="test-access-token")
|
|
|
|
mock_api = MagicMock()
|
|
mock_api.users().messages().insert().execute.return_value = {
|
|
"id": "msg123",
|
|
"threadId": "thread456",
|
|
"labelIds": ["INBOX"],
|
|
}
|
|
service._service = mock_api
|
|
|
|
# No label_ids specified - should default to INBOX
|
|
result = await service.inject_email(
|
|
raw_email=b"From: test@example.com\r\nSubject: Test\r\n\r\nHello",
|
|
)
|
|
|
|
assert result["message_id"] == "msg123"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inject_email_api_error(self):
|
|
"""Test that GmailInjectionError is raised on API error"""
|
|
service = GmailService(access_token="test-access-token")
|
|
|
|
mock_api = MagicMock()
|
|
mock_api.users().messages().insert().execute.side_effect = Exception(
|
|
"API Error"
|
|
)
|
|
service._service = mock_api
|
|
|
|
with pytest.raises(GmailInjectionError, match="Failed to inject email"):
|
|
await service.inject_email(
|
|
raw_email=b"From: test@example.com\r\nSubject: Test\r\n\r\nHello",
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_access_success(self):
|
|
"""Test successful access verification"""
|
|
service = GmailService(access_token="test-access-token")
|
|
|
|
mock_api = MagicMock()
|
|
mock_api.users().getProfile().execute.return_value = {
|
|
"emailAddress": "test@gmail.com",
|
|
}
|
|
service._service = mock_api
|
|
|
|
result = await service.verify_access()
|
|
assert result is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_access_failure(self):
|
|
"""Test failed access verification"""
|
|
service = GmailService(access_token="bad-token")
|
|
|
|
mock_api = MagicMock()
|
|
mock_api.users().getProfile().execute.side_effect = Exception("Invalid token")
|
|
service._service = mock_api
|
|
|
|
result = await service.verify_access()
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_email_address_success(self):
|
|
"""Test getting email address"""
|
|
service = GmailService(access_token="test-access-token")
|
|
|
|
mock_api = MagicMock()
|
|
mock_api.users().getProfile().execute.return_value = {
|
|
"emailAddress": "user@gmail.com",
|
|
}
|
|
service._service = mock_api
|
|
|
|
email = await service.get_email_address()
|
|
assert email == "user@gmail.com"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_email_address_failure(self):
|
|
"""Test getting email address when API fails"""
|
|
service = GmailService(access_token="bad-token")
|
|
|
|
mock_api = MagicMock()
|
|
mock_api.users().getProfile().execute.side_effect = Exception("Error")
|
|
service._service = mock_api
|
|
|
|
email = await service.get_email_address()
|
|
assert email is None
|
|
|
|
def test_gmail_label_metadata_helpers(self):
|
|
"""Test Gmail metadata extraction remains backward compatible."""
|
|
scopes = build_gmail_credential_scopes(
|
|
["scope-a", "scope-b"],
|
|
[SOURCE_EMAIL_LABEL_TEMPLATE, "Imported", " imported "],
|
|
)
|
|
|
|
assert extract_granted_scopes(scopes) == ["scope-a", "scope-b"]
|
|
assert extract_import_label_templates(scopes) == [
|
|
SOURCE_EMAIL_LABEL_TEMPLATE,
|
|
"Imported",
|
|
]
|
|
assert (
|
|
extract_import_label_templates(["legacy-scope"])
|
|
== DEFAULT_IMPORT_LABEL_TEMPLATES
|
|
)
|
|
|
|
def test_render_import_labels_uses_source_email_template(self):
|
|
"""Test that source email templates render to the source mailbox address."""
|
|
rendered = render_import_labels(
|
|
[SOURCE_EMAIL_LABEL_TEMPLATE, "Imported", ""],
|
|
"source@example.com",
|
|
)
|
|
|
|
assert rendered == ["source@example.com", "Imported"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_build_import_label_ids_creates_configured_labels(self):
|
|
"""Test that configured import labels are created and added alongside INBOX."""
|
|
service = GmailService(access_token="test-access-token")
|
|
service.get_or_create_label = AsyncMock(
|
|
side_effect=["Label-source", "Label-imported"]
|
|
) # type: ignore[method-assign]
|
|
|
|
label_ids = await service.build_import_label_ids(
|
|
import_label_templates=[SOURCE_EMAIL_LABEL_TEMPLATE, "imported"],
|
|
source_email="source@example.com",
|
|
)
|
|
|
|
assert label_ids == ["INBOX", "Label-source", "Label-imported"]
|