chore: connector tests

This commit is contained in:
Alex committed 2026-03-29 10:32:48 +01:00
1 parent 6492852f7d
commit e06debad5f
8 files changed
+2866

No files matched your search

View File
Whitespace-only changes.
View File
Whitespace-only changes.
+110
View File
@@ -0,0 +1,110 @@
"""Tests for connector base classes."""
import pytest
from application.parser.connectors.base import BaseConnectorAuth, BaseConnectorLoader
from application.parser.schema.base import Document
class ConcreteAuth(BaseConnectorAuth):
"""Minimal concrete implementation for testing the ABC."""
def get_authorization_url(self, state=None):
return f"https://example.com/auth?state={state}"
def exchange_code_for_tokens(self, authorization_code):
return {"access_token": "tok", "code": authorization_code}
def refresh_access_token(self, refresh_token):
return {"access_token": "new_tok", "refresh_token": refresh_token}
def is_token_expired(self, token_info):
return token_info.get("expired", False)
class ConcreteLoader(BaseConnectorLoader):
"""Minimal concrete implementation for testing the ABC."""
def __init__(self, session_token):
self.session_token = session_token
def load_data(self, inputs):
return [Document(text="test", doc_id="1", extra_info={})]
def download_to_directory(self, local_dir, source_config=None):
return {"files_downloaded": 0, "directory_path": local_dir}
class TestBaseConnectorAuth:
@pytest.mark.unit
def test_sanitize_token_info_extracts_standard_fields(self):
auth = ConcreteAuth()
token_info = {
"access_token": "at",
"refresh_token": "rt",
"token_uri": "https://token.uri",
"expiry": 12345,
"extra_field": "should_not_appear",
}
result = auth.sanitize_token_info(token_info)
assert result == {
"access_token": "at",
"refresh_token": "rt",
"token_uri": "https://token.uri",
"expiry": 12345,
}
@pytest.mark.unit
def test_sanitize_token_info_with_extra_kwargs(self):
auth = ConcreteAuth()
token_info = {
"access_token": "at",
"refresh_token": "rt",
"token_uri": "https://token.uri",
"expiry": 100,
}
result = auth.sanitize_token_info(token_info, custom_field="custom_val")
assert result["custom_field"] == "custom_val"
assert result["access_token"] == "at"
@pytest.mark.unit
def test_sanitize_token_info_missing_fields_returns_none(self):
auth = ConcreteAuth()
result = auth.sanitize_token_info({})
assert result["access_token"] is None
assert result["refresh_token"] is None
assert result["token_uri"] is None
assert result["expiry"] is None
@pytest.mark.unit
def test_abstract_methods_invocable_on_concrete(self):
auth = ConcreteAuth()
assert "example.com" in auth.get_authorization_url("s1")
assert auth.exchange_code_for_tokens("code1")["access_token"] == "tok"
assert auth.refresh_access_token("rt")["access_token"] == "new_tok"
assert auth.is_token_expired({"expired": True}) is True
assert auth.is_token_expired({"expired": False}) is False
class TestBaseConnectorLoader:
@pytest.mark.unit
def test_concrete_loader_init(self):
loader = ConcreteLoader("session123")
assert loader.session_token == "session123"
@pytest.mark.unit
def test_concrete_loader_load_data(self):
loader = ConcreteLoader("s")
docs = loader.load_data({})
assert len(docs) == 1
assert docs[0].text == "test"
@pytest.mark.unit
def test_concrete_loader_download_to_directory(self):
loader = ConcreteLoader("s")
result = loader.download_to_directory("/tmp/test")
assert result["directory_path"] == "/tmp/test"
assert result["files_downloaded"] == 0
@@ -0,0 +1,102 @@
"""Tests for ConnectorCreator factory class."""
from unittest.mock import patch, MagicMock
import pytest
class TestConnectorCreator:
@pytest.fixture(autouse=True)
def _patch_settings(self):
"""Patch settings so connector imports don't fail on missing credentials."""
mock_settings = MagicMock()
mock_settings.GOOGLE_CLIENT_ID = "gid"
mock_settings.GOOGLE_CLIENT_SECRET = "gsecret"
mock_settings.CONNECTOR_REDIRECT_BASE_URI = "https://redirect"
mock_settings.MICROSOFT_CLIENT_ID = "mid"
mock_settings.MICROSOFT_CLIENT_SECRET = "msecret"
mock_settings.MICROSOFT_TENANT_ID = "tid"
mock_settings.MONGO_DB_NAME = "test_db"
with patch("application.core.settings.settings", mock_settings), \
patch("application.parser.connectors.share_point.auth.settings", mock_settings), \
patch("application.parser.connectors.google_drive.auth.settings", mock_settings), \
patch("application.parser.connectors.share_point.auth.ConfidentialClientApplication"):
from application.parser.connectors.connector_creator import ConnectorCreator
self.ConnectorCreator = ConnectorCreator
yield
@pytest.mark.unit
def test_get_supported_connectors(self):
supported = self.ConnectorCreator.get_supported_connectors()
assert "google_drive" in supported
assert "share_point" in supported
@pytest.mark.unit
def test_is_supported_valid(self):
assert self.ConnectorCreator.is_supported("google_drive") is True
assert self.ConnectorCreator.is_supported("share_point") is True
@pytest.mark.unit
def test_is_supported_case_insensitive(self):
assert self.ConnectorCreator.is_supported("Google_Drive") is True
assert self.ConnectorCreator.is_supported("SHARE_POINT") is True
@pytest.mark.unit
def test_is_supported_invalid(self):
assert self.ConnectorCreator.is_supported("dropbox") is False
assert self.ConnectorCreator.is_supported("") is False
@pytest.mark.unit
def test_create_auth_google_drive(self):
auth = self.ConnectorCreator.create_auth("google_drive")
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
assert isinstance(auth, GoogleDriveAuth)
@pytest.mark.unit
def test_create_auth_share_point(self):
auth = self.ConnectorCreator.create_auth("share_point")
from application.parser.connectors.share_point.auth import SharePointAuth
assert isinstance(auth, SharePointAuth)
@pytest.mark.unit
def test_create_auth_invalid_raises(self):
with pytest.raises(ValueError, match="No auth class found"):
self.ConnectorCreator.create_auth("invalid")
@pytest.mark.unit
def test_create_connector_invalid_raises(self):
with pytest.raises(ValueError, match="No connector class found"):
self.ConnectorCreator.create_connector("invalid", "session_tok")
@pytest.mark.unit
def test_create_connector_google_drive(self):
with patch("application.parser.connectors.google_drive.loader.GoogleDriveAuth") as MockAuth:
mock_auth_instance = MagicMock()
mock_auth_instance.get_token_info_from_session.return_value = {
"access_token": "at", "refresh_token": "rt"
}
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.expired = False
mock_auth_instance.create_credentials_from_token_info.return_value = mock_creds
mock_auth_instance.build_drive_service.return_value = MagicMock()
MockAuth.return_value = mock_auth_instance
loader = self.ConnectorCreator.create_connector("google_drive", "session_tok")
from application.parser.connectors.google_drive.loader import GoogleDriveLoader
assert isinstance(loader, GoogleDriveLoader)
@pytest.mark.unit
def test_create_connector_share_point(self):
with patch("application.parser.connectors.share_point.loader.SharePointAuth") as MockAuth:
mock_auth_instance = MagicMock()
mock_auth_instance.get_token_info_from_session.return_value = {
"access_token": "at", "refresh_token": "rt"
}
MockAuth.return_value = mock_auth_instance
loader = self.ConnectorCreator.create_connector("share_point", "session_tok")
from application.parser.connectors.share_point.loader import SharePointLoader
assert isinstance(loader, SharePointLoader)
@@ -0,0 +1,441 @@
"""Tests for GoogleDriveAuth."""
import datetime
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def mock_settings():
s = MagicMock()
s.GOOGLE_CLIENT_ID = "test-client-id"
s.GOOGLE_CLIENT_SECRET = "test-client-secret"
s.CONNECTOR_REDIRECT_BASE_URI = "https://redirect.example.com/callback"
s.MONGO_DB_NAME = "test_db"
return s
@pytest.fixture
def auth(mock_settings):
with patch("application.parser.connectors.google_drive.auth.settings", mock_settings):
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
return GoogleDriveAuth()
class TestGoogleDriveAuthInit:
@pytest.mark.unit
def test_init_sets_credentials(self, auth, mock_settings):
assert auth.client_id == "test-client-id"
assert auth.client_secret == "test-client-secret"
assert auth.redirect_uri == "https://redirect.example.com/callback"
@pytest.mark.unit
def test_init_missing_client_id_raises(self, mock_settings):
mock_settings.GOOGLE_CLIENT_ID = None
with patch("application.parser.connectors.google_drive.auth.settings", mock_settings):
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
with pytest.raises(ValueError, match="Google OAuth credentials not configured"):
GoogleDriveAuth()
@pytest.mark.unit
def test_init_missing_client_secret_raises(self, mock_settings):
mock_settings.GOOGLE_CLIENT_SECRET = None
with patch("application.parser.connectors.google_drive.auth.settings", mock_settings):
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
with pytest.raises(ValueError, match="Google OAuth credentials not configured"):
GoogleDriveAuth()
class TestGetAuthorizationUrl:
@pytest.mark.unit
def test_returns_authorization_url(self, auth):
mock_flow = MagicMock()
mock_flow.authorization_url.return_value = ("https://accounts.google.com/auth?state=s1", "s1")
with patch("application.parser.connectors.google_drive.auth.Flow") as MockFlow:
MockFlow.from_client_config.return_value = mock_flow
url = auth.get_authorization_url(state="s1")
assert url == "https://accounts.google.com/auth?state=s1"
mock_flow.authorization_url.assert_called_once_with(
access_type='offline',
prompt='consent',
include_granted_scopes='false',
state="s1"
)
@pytest.mark.unit
def test_raises_on_flow_error(self, auth):
with patch("application.parser.connectors.google_drive.auth.Flow") as MockFlow:
MockFlow.from_client_config.side_effect = Exception("flow error")
with pytest.raises(Exception, match="flow error"):
auth.get_authorization_url()
class TestExchangeCodeForTokens:
@pytest.mark.unit
def test_successful_exchange(self, auth):
mock_creds = MagicMock()
mock_creds.token = "access_tok"
mock_creds.refresh_token = "refresh_tok"
mock_creds.token_uri = "https://oauth2.googleapis.com/token"
mock_creds.client_id = "test-client-id"
mock_creds.client_secret = "test-client-secret"
mock_creds.scopes = ["https://www.googleapis.com/auth/drive.file"]
mock_creds.expiry = datetime.datetime(2025, 1, 1, 12, 0, 0)
mock_flow = MagicMock()
mock_flow.credentials = mock_creds
with patch("application.parser.connectors.google_drive.auth.Flow") as MockFlow:
MockFlow.from_client_config.return_value = mock_flow
result = auth.exchange_code_for_tokens("auth_code_123")
assert result["access_token"] == "access_tok"
assert result["refresh_token"] == "refresh_tok"
assert result["token_uri"] == "https://oauth2.googleapis.com/token"
assert result["client_id"] == "test-client-id"
assert result["client_secret"] == "test-client-secret"
assert result["expiry"] == "2025-01-01T12:00:00"
@pytest.mark.unit
def test_empty_code_raises(self, auth):
with pytest.raises(ValueError, match="Authorization code is required"):
auth.exchange_code_for_tokens("")
@pytest.mark.unit
def test_no_access_token_raises(self, auth):
mock_creds = MagicMock()
mock_creds.token = None
mock_creds.refresh_token = "rt"
mock_flow = MagicMock()
mock_flow.credentials = mock_creds
with patch("application.parser.connectors.google_drive.auth.Flow") as MockFlow:
MockFlow.from_client_config.return_value = mock_flow
with pytest.raises(ValueError, match="did not return an access token"):
auth.exchange_code_for_tokens("code")
@pytest.mark.unit
def test_no_refresh_token_raises(self, auth):
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = None
mock_flow = MagicMock()
mock_flow.credentials = mock_creds
with patch("application.parser.connectors.google_drive.auth.Flow") as MockFlow:
MockFlow.from_client_config.return_value = mock_flow
with pytest.raises(ValueError, match="No refresh token received"):
auth.exchange_code_for_tokens("code")
@pytest.mark.unit
def test_fills_in_missing_token_uri(self, auth):
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = "rt"
mock_creds.token_uri = None
mock_creds.client_id = None
mock_creds.client_secret = None
mock_creds.scopes = []
mock_creds.expiry = None
mock_flow = MagicMock()
mock_flow.credentials = mock_creds
with patch("application.parser.connectors.google_drive.auth.Flow") as MockFlow:
MockFlow.from_client_config.return_value = mock_flow
result = auth.exchange_code_for_tokens("code")
assert result["token_uri"] == "https://oauth2.googleapis.com/token"
assert result["client_id"] == "test-client-id"
assert result["client_secret"] == "test-client-secret"
class TestRefreshAccessToken:
@pytest.mark.unit
def test_successful_refresh(self, auth):
mock_request_cls = MagicMock()
with patch("application.parser.connectors.google_drive.auth.Credentials") as MockCreds, \
patch("google.auth.transport.requests.Request", mock_request_cls):
mock_cred_instance = MagicMock()
mock_cred_instance.token = "new_access"
mock_cred_instance.token_uri = "https://oauth2.googleapis.com/token"
mock_cred_instance.client_id = "cid"
mock_cred_instance.client_secret = "cs"
mock_cred_instance.scopes = []
mock_cred_instance.expiry = datetime.datetime(2025, 6, 1, 0, 0, 0)
MockCreds.return_value = mock_cred_instance
result = auth.refresh_access_token("old_refresh")
assert result["access_token"] == "new_access"
assert result["refresh_token"] == "old_refresh"
mock_cred_instance.refresh.assert_called_once()
@pytest.mark.unit
def test_empty_refresh_token_raises(self, auth):
with pytest.raises(ValueError, match="Refresh token is required"):
auth.refresh_access_token("")
@pytest.mark.unit
def test_refresh_failure_raises(self, auth):
with patch("application.parser.connectors.google_drive.auth.Credentials") as MockCreds, \
patch("google.auth.transport.requests.Request"):
mock_cred_instance = MagicMock()
mock_cred_instance.refresh.side_effect = Exception("refresh failed")
MockCreds.return_value = mock_cred_instance
with pytest.raises(Exception, match="refresh failed"):
auth.refresh_access_token("rt")
class TestCreateCredentialsFromTokenInfo:
@pytest.mark.unit
def test_creates_credentials(self, auth, mock_settings):
with patch("application.parser.connectors.google_drive.auth.Credentials") as MockCreds, \
patch("application.parser.connectors.google_drive.auth.settings", mock_settings):
mock_cred = MagicMock()
mock_cred.token = "at"
MockCreds.return_value = mock_cred
creds = auth.create_credentials_from_token_info({
"access_token": "at",
"refresh_token": "rt",
"scopes": ["scope1"],
})
assert creds.token == "at"
@pytest.mark.unit
def test_missing_access_token_raises(self, auth, mock_settings):
with patch("application.parser.connectors.google_drive.auth.settings", mock_settings):
with pytest.raises(ValueError, match="No access token found"):
auth.create_credentials_from_token_info({})
@pytest.mark.unit
def test_credentials_without_valid_token_raises(self, auth, mock_settings):
with patch("application.parser.connectors.google_drive.auth.Credentials") as MockCreds, \
patch("application.parser.connectors.google_drive.auth.settings", mock_settings):
mock_cred = MagicMock()
mock_cred.token = None
MockCreds.return_value = mock_cred
with pytest.raises(ValueError, match="Credentials created without valid access token"):
auth.create_credentials_from_token_info({"access_token": "at"})
class TestBuildDriveService:
@pytest.mark.unit
def test_builds_service(self, auth):
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = "rt"
mock_creds.expired = False
with patch("application.parser.connectors.google_drive.auth.build") as mock_build:
mock_build.return_value = MagicMock()
service = auth.build_drive_service(mock_creds)
mock_build.assert_called_once_with('drive', 'v3', credentials=mock_creds)
assert service is not None
@pytest.mark.unit
def test_no_credentials_raises(self, auth):
with pytest.raises(ValueError, match="No credentials provided"):
auth.build_drive_service(None)
@pytest.mark.unit
def test_no_token_no_refresh_raises(self, auth):
mock_creds = MagicMock()
mock_creds.token = None
mock_creds.refresh_token = None
with pytest.raises(ValueError, match="No access token or refresh token"):
auth.build_drive_service(mock_creds)
@pytest.mark.unit
def test_expired_token_refreshes(self, auth):
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = "rt"
mock_creds.expired = True
with patch("application.parser.connectors.google_drive.auth.build") as mock_build, \
patch("google.auth.transport.requests.Request"):
mock_build.return_value = MagicMock()
auth.build_drive_service(mock_creds)
mock_creds.refresh.assert_called_once()
@pytest.mark.unit
def test_expired_no_refresh_token_raises(self, auth):
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = None
mock_creds.expired = True
with pytest.raises(ValueError, match="No access token or refresh token"):
auth.build_drive_service(mock_creds)
@pytest.mark.unit
def test_refresh_failure_raises(self, auth):
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = "rt"
mock_creds.expired = True
with patch("google.auth.transport.requests.Request"):
mock_creds.refresh.side_effect = Exception("Cannot refresh")
with pytest.raises(ValueError, match="Failed to refresh credentials"):
auth.build_drive_service(mock_creds)
@pytest.mark.unit
def test_http_error_raises(self, auth):
from googleapiclient.errors import HttpError
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = "rt"
mock_creds.expired = False
mock_resp = MagicMock()
mock_resp.status = 500
with patch("application.parser.connectors.google_drive.auth.build") as mock_build:
mock_build.side_effect = HttpError(mock_resp, b"error")
with pytest.raises(ValueError, match="HTTP 500"):
auth.build_drive_service(mock_creds)
class TestIsTokenExpired:
@pytest.mark.unit
def test_expired_token(self, auth):
past = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=1)).isoformat()
assert auth.is_token_expired({"expiry": past}) is True
@pytest.mark.unit
def test_valid_token(self, auth):
future = (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)).isoformat()
assert auth.is_token_expired({"expiry": future}) is False
@pytest.mark.unit
def test_token_within_buffer(self, auth):
almost_expired = (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=30)).isoformat()
assert auth.is_token_expired({"expiry": almost_expired}) is True
@pytest.mark.unit
def test_no_expiry_with_access_token(self, auth):
assert auth.is_token_expired({"access_token": "at"}) is False
@pytest.mark.unit
def test_no_expiry_no_access_token(self, auth):
assert auth.is_token_expired({}) is True
@pytest.mark.unit
def test_invalid_expiry_format_returns_true(self, auth):
assert auth.is_token_expired({"expiry": "not-a-date"}) is True
@pytest.mark.unit
def test_none_expiry_with_access_token(self, auth):
assert auth.is_token_expired({"expiry": None, "access_token": "at"}) is False
class TestGetTokenInfoFromSession:
def _mock_mongo(self, mock_settings, find_one_return):
mock_collection = MagicMock()
mock_collection.find_one.return_value = find_one_return
mock_db = MagicMock()
mock_db.__getitem__ = MagicMock(return_value=mock_collection)
return {mock_settings.MONGO_DB_NAME: mock_db}
@pytest.mark.unit
def test_valid_session(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {
"session_token": "st",
"token_info": {"access_token": "at", "refresh_token": "rt"},
})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
result = auth.get_token_info_from_session("st")
assert result["access_token"] == "at"
assert result["token_uri"] == "https://oauth2.googleapis.com/token"
@pytest.mark.unit
def test_session_not_found_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, None)
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve Google Drive token"):
auth.get_token_info_from_session("bad_token")
@pytest.mark.unit
def test_session_missing_token_info_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {"session_token": "st"})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve Google Drive token"):
auth.get_token_info_from_session("st")
@pytest.mark.unit
def test_missing_required_fields_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {
"session_token": "st",
"token_info": {"access_token": "at"},
})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve Google Drive token"):
auth.get_token_info_from_session("st")
@pytest.mark.unit
def test_empty_token_info_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {
"session_token": "st",
"token_info": None,
})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve Google Drive token"):
auth.get_token_info_from_session("st")
class TestValidateCredentials:
@pytest.mark.unit
def test_valid_credentials(self, auth):
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.refresh_token = "rt"
mock_creds.expired = False
mock_service = MagicMock()
mock_service.about.return_value.get.return_value.execute.return_value = {"user": {}}
with patch.object(auth, 'build_drive_service', return_value=mock_service):
assert auth.validate_credentials(mock_creds) is True
@pytest.mark.unit
def test_http_error_returns_false(self, auth):
from googleapiclient.errors import HttpError
mock_creds = MagicMock()
mock_resp = MagicMock()
mock_resp.status = 401
mock_service = MagicMock()
mock_service.about.return_value.get.return_value.execute.side_effect = HttpError(mock_resp, b"unauth")
with patch.object(auth, 'build_drive_service', return_value=mock_service):
assert auth.validate_credentials(mock_creds) is False
@pytest.mark.unit
def test_general_error_returns_false(self, auth):
mock_creds = MagicMock()
with patch.object(auth, 'build_drive_service', side_effect=Exception("fail")):
assert auth.validate_credentials(mock_creds) is False
@@ -0,0 +1,851 @@
"""Tests for GoogleDriveLoader."""
from unittest.mock import MagicMock, patch
import pytest
from application.parser.schema.base import Document
def _make_loader(service=None):
"""Create a GoogleDriveLoader with mocked dependencies."""
with patch("application.parser.connectors.google_drive.loader.GoogleDriveAuth") as MockAuth:
mock_auth = MagicMock()
mock_auth.get_token_info_from_session.return_value = {
"access_token": "at",
"refresh_token": "rt",
}
mock_creds = MagicMock()
mock_creds.token = "at"
mock_creds.expired = False
mock_creds.refresh_token = "rt"
mock_auth.create_credentials_from_token_info.return_value = mock_creds
mock_auth.build_drive_service.return_value = service or MagicMock()
MockAuth.return_value = mock_auth
from application.parser.connectors.google_drive.loader import GoogleDriveLoader
loader = GoogleDriveLoader("session_tok")
return loader
@pytest.fixture
def mock_service():
return MagicMock()
@pytest.fixture
def loader(mock_service):
return _make_loader(mock_service)
class TestGoogleDriveLoaderInit:
@pytest.mark.unit
def test_init_sets_attributes(self, loader):
assert loader.session_token == "session_tok"
assert loader.credentials is not None
assert loader.service is not None
assert loader.next_page_token is None
@pytest.mark.unit
def test_init_service_failure_sets_none(self):
with patch("application.parser.connectors.google_drive.loader.GoogleDriveAuth") as MockAuth:
mock_auth = MagicMock()
mock_auth.get_token_info_from_session.return_value = {
"access_token": "at", "refresh_token": "rt"
}
mock_creds = MagicMock()
mock_creds.token = "at"
mock_auth.create_credentials_from_token_info.return_value = mock_creds
mock_auth.build_drive_service.side_effect = Exception("service fail")
MockAuth.return_value = mock_auth
from application.parser.connectors.google_drive.loader import GoogleDriveLoader
loader = GoogleDriveLoader("st")
assert loader.service is None
class TestProcessFile:
@pytest.mark.unit
def test_supported_mime_type_with_content(self, loader):
loader._download_file_content = MagicMock(return_value="file content")
metadata = {
"id": "f1",
"name": "test.pdf",
"mimeType": "application/pdf",
"size": 1024,
"createdTime": "2025-01-01T00:00:00Z",
"modifiedTime": "2025-01-02T00:00:00Z",
"parents": ["root"],
}
doc = loader._process_file(metadata, load_content=True)
assert doc is not None
assert doc.text == "file content"
assert doc.doc_id == "f1"
assert doc.extra_info["file_name"] == "test.pdf"
assert doc.extra_info["source"] == "google_drive"
@pytest.mark.unit
def test_supported_mime_type_no_content(self, loader):
metadata = {
"id": "f1",
"name": "test.pdf",
"mimeType": "application/pdf",
}
doc = loader._process_file(metadata, load_content=False)
assert doc is not None
assert doc.text == ""
assert doc.doc_id == "f1"
@pytest.mark.unit
def test_unsupported_mime_type_returns_none(self, loader):
metadata = {
"id": "f1",
"name": "test.zip",
"mimeType": "application/zip",
}
doc = loader._process_file(metadata)
assert doc is None
@pytest.mark.unit
def test_download_failure_returns_none(self, loader):
loader._download_file_content = MagicMock(return_value=None)
metadata = {
"id": "f1",
"name": "test.txt",
"mimeType": "text/plain",
}
doc = loader._process_file(metadata, load_content=True)
assert doc is None
@pytest.mark.unit
def test_exception_returns_none(self, loader):
loader._download_file_content = MagicMock(side_effect=Exception("fail"))
metadata = {
"id": "f1",
"name": "test.txt",
"mimeType": "text/plain",
}
doc = loader._process_file(metadata, load_content=True)
assert doc is None
class TestLoadData:
@pytest.mark.unit
def test_load_specific_files(self, loader):
doc = Document(text="content", doc_id="f1", extra_info={"file_name": "test.pdf"})
loader._load_file_by_id = MagicMock(return_value=doc)
result = loader.load_data({"file_ids": ["f1"]})
assert len(result) == 1
assert result[0].doc_id == "f1"
@pytest.mark.unit
def test_load_files_with_search_filter(self, loader):
doc = Document(text="c", doc_id="f1", extra_info={"file_name": "report.pdf"})
loader._load_file_by_id = MagicMock(return_value=doc)
result = loader.load_data({"file_ids": ["f1"], "search_query": "report"})
assert len(result) == 1
result = loader.load_data({"file_ids": ["f1"], "search_query": "other"})
assert len(result) == 0
@pytest.mark.unit
def test_load_files_error_continues(self, loader):
loader._load_file_by_id = MagicMock(side_effect=Exception("fail"))
result = loader.load_data({"file_ids": ["f1", "f2"]})
assert len(result) == 0
@pytest.mark.unit
def test_browse_mode_uses_list_items(self, loader):
docs = [Document(text="", doc_id="f1", extra_info={})]
loader._list_items_in_parent = MagicMock(return_value=docs)
result = loader.load_data({"folder_id": "folder1", "limit": 50})
loader._list_items_in_parent.assert_called_once_with(
"folder1", limit=50, load_content=True, page_token=None, search_query=None
)
assert len(result) == 1
@pytest.mark.unit
def test_browse_mode_defaults_to_root(self, loader):
loader._list_items_in_parent = MagicMock(return_value=[])
loader.load_data({})
loader._list_items_in_parent.assert_called_once_with(
"root", limit=100, load_content=True, page_token=None, search_query=None
)
@pytest.mark.unit
def test_session_token_mismatch_logs_warning(self, loader):
loader._list_items_in_parent = MagicMock(return_value=[])
loader.load_data({"session_token": "different_token"})
# Should not raise, just logs
@pytest.mark.unit
def test_load_data_with_list_only(self, loader):
loader._list_items_in_parent = MagicMock(return_value=[])
loader.load_data({"list_only": True})
loader._list_items_in_parent.assert_called_once_with(
"root", limit=100, load_content=False, page_token=None, search_query=None
)
@pytest.mark.unit
def test_load_data_with_page_token(self, loader):
loader._list_items_in_parent = MagicMock(return_value=[])
loader.load_data({"page_token": "next_page"})
loader._list_items_in_parent.assert_called_once_with(
"root", limit=100, load_content=True, page_token="next_page", search_query=None
)
@pytest.mark.unit
def test_credential_refresh_retry(self, loader):
"""When _load_file_by_id returns None and _credential_refreshed is set, retry."""
loader._credential_refreshed = True
call_count = [0]
def side_effect(fid, load_content=True):
call_count[0] += 1
if call_count[0] == 1:
return None
return Document(text="c", doc_id=fid, extra_info={"file_name": "test.pdf"})
loader._load_file_by_id = MagicMock(side_effect=side_effect)
result = loader.load_data({"file_ids": ["f1"]})
assert len(result) == 1
@pytest.mark.unit
def test_outer_exception_raises(self, loader):
"""The outer try/except in load_data re-raises unexpected errors."""
loader._list_items_in_parent = MagicMock(side_effect=RuntimeError("unexpected"))
with pytest.raises(RuntimeError, match="unexpected"):
loader.load_data({})
class TestLoadFileById:
@pytest.mark.unit
def test_loads_file_metadata_and_processes(self, loader, mock_service):
mock_service.files.return_value.get.return_value.execute.return_value = {
"id": "f1", "name": "test.txt", "mimeType": "text/plain"
}
loader._process_file = MagicMock(return_value=Document(text="t", doc_id="f1", extra_info={}))
doc = loader._load_file_by_id("f1")
assert doc is not None
@pytest.mark.unit
def test_http_401_refreshes_credentials(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 401
mock_service.files.return_value.get.return_value.execute.side_effect = HttpError(resp, b"unauth")
with patch("google.auth.transport.requests.Request"):
result = loader._load_file_by_id("f1")
assert result is None
loader.credentials.refresh.assert_called_once()
@pytest.mark.unit
def test_http_401_no_refresh_token_raises(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 401
mock_service.files.return_value.get.return_value.execute.side_effect = HttpError(resp, b"unauth")
loader.credentials.refresh_token = None
with pytest.raises(ValueError, match="missing refresh_token"):
loader._load_file_by_id("f1")
@pytest.mark.unit
def test_http_500_returns_none(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 500
mock_service.files.return_value.get.return_value.execute.side_effect = HttpError(resp, b"server error")
result = loader._load_file_by_id("f1")
assert result is None
@pytest.mark.unit
def test_general_exception_returns_none(self, loader, mock_service):
mock_service.files.return_value.get.return_value.execute.side_effect = Exception("fail")
result = loader._load_file_by_id("f1")
assert result is None
@pytest.mark.unit
def test_ensure_service_called(self, loader):
loader.service = None
loader.auth.build_drive_service.return_value = MagicMock()
loader.auth.build_drive_service.return_value.files.return_value.get.return_value.execute.return_value = {
"id": "f1", "name": "t.txt", "mimeType": "text/plain"
}
loader._process_file = MagicMock(return_value=None)
loader._load_file_by_id("f1")
loader.auth.build_drive_service.assert_called()
@pytest.mark.unit
def test_http_401_refresh_failure_raises(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 401
mock_service.files.return_value.get.return_value.execute.side_effect = HttpError(resp, b"unauth")
loader.credentials.refresh_token = "rt"
with patch("google.auth.transport.requests.Request"):
loader.credentials.refresh.side_effect = Exception("refresh broke")
with pytest.raises(ValueError, match="could not be refreshed"):
loader._load_file_by_id("f1")
class TestListItemsInParent:
@pytest.mark.unit
def test_lists_files_and_folders(self, loader, mock_service):
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [
{"id": "folder1", "name": "Docs", "mimeType": "application/vnd.google-apps.folder"},
{"id": "file1", "name": "test.txt", "mimeType": "text/plain"},
],
"nextPageToken": None,
}
loader._process_file = MagicMock(return_value=Document(text="", doc_id="file1", extra_info={}))
docs = loader._list_items_in_parent("root", limit=100)
assert len(docs) == 2
assert docs[0].extra_info.get("is_folder") is True
@pytest.mark.unit
def test_search_query_modifies_drive_query(self, loader, mock_service):
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [], "nextPageToken": None
}
loader._list_items_in_parent("root", search_query="report")
call_args = mock_service.files.return_value.list.call_args
assert "name contains 'report'" in call_args.kwargs.get("q", "")
@pytest.mark.unit
def test_limit_stops_early(self, loader, mock_service):
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [
{"id": f"f{i}", "name": f"file{i}.txt", "mimeType": "text/plain"} for i in range(10)
],
"nextPageToken": "next",
}
loader._process_file = MagicMock(side_effect=lambda m, **kw: Document(text="", doc_id=m["id"], extra_info={}))
docs = loader._list_items_in_parent("root", limit=3)
assert len(docs) == 3
@pytest.mark.unit
def test_pagination_stores_next_page_token(self, loader, mock_service):
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [{"id": "f1", "name": "f.txt", "mimeType": "text/plain"}],
"nextPageToken": "page2",
}
loader._process_file = MagicMock(return_value=Document(text="", doc_id="f1", extra_info={}))
loader._list_items_in_parent("root", limit=1)
assert loader.next_page_token == "page2"
@pytest.mark.unit
def test_limit_breaks_loop_when_remaining_zero(self, loader, mock_service):
"""When limit is exactly met after first page, the while loop breaks via remaining==0."""
call_count = [0]
def list_side_effect(**kw):
call_count[0] += 1
mock = MagicMock()
if call_count[0] == 1:
mock.execute.return_value = {
"files": [
{"id": "f1", "name": "f1.txt", "mimeType": "text/plain"},
{"id": "f2", "name": "f2.txt", "mimeType": "text/plain"},
],
"nextPageToken": "page2",
}
else:
# Should not reach here if break works correctly
mock.execute.return_value = {"files": [], "nextPageToken": None}
return mock
mock_service.files.return_value.list.side_effect = list_side_effect
loader._process_file = MagicMock(side_effect=lambda m, **kw: Document(text="", doc_id=m["id"], extra_info={}))
# limit=2, first page returns exactly 2 files with nextPageToken
# Since items don't hit the inner limit check (it checks after each item),
# it should loop back and break at remaining==0
docs = loader._list_items_in_parent("root", limit=2)
assert len(docs) == 2
@pytest.mark.unit
def test_exception_returns_partial_results(self, loader, mock_service):
mock_service.files.return_value.list.return_value.execute.side_effect = Exception("api error")
docs = loader._list_items_in_parent("root")
assert docs == []
class TestDownloadFileContent:
@pytest.mark.unit
def test_download_regular_file(self, loader, mock_service):
mock_request = MagicMock()
mock_service.files.return_value.get_media.return_value = mock_request
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDownload:
mock_dl = MagicMock()
mock_dl.next_chunk.side_effect = [(None, False), (None, True)]
MockDownload.return_value = mock_dl
with patch("io.BytesIO") as MockBytesIO:
mock_bio = MagicMock()
mock_bio.getvalue.return_value = b"file content"
MockBytesIO.return_value = mock_bio
content = loader._download_file_content("f1", "text/plain")
assert content == "file content"
@pytest.mark.unit
def test_download_google_workspace_file_uses_export(self, loader, mock_service):
mock_request = MagicMock()
mock_service.files.return_value.export_media.return_value = mock_request
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDownload:
mock_dl = MagicMock()
mock_dl.next_chunk.return_value = (None, True)
MockDownload.return_value = mock_dl
with patch("io.BytesIO") as MockBytesIO:
mock_bio = MagicMock()
mock_bio.getvalue.return_value = b"exported"
MockBytesIO.return_value = mock_bio
content = loader._download_file_content("f1", "application/vnd.google-apps.document")
mock_service.files.return_value.export_media.assert_called_once()
assert content == "exported"
@pytest.mark.unit
def test_unicode_decode_error_returns_none(self, loader, mock_service):
mock_request = MagicMock()
mock_service.files.return_value.get_media.return_value = mock_request
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDownload:
mock_dl = MagicMock()
mock_dl.next_chunk.return_value = (None, True)
MockDownload.return_value = mock_dl
with patch("io.BytesIO") as MockBytesIO:
mock_bio = MagicMock()
mock_bio.getvalue.return_value = MagicMock()
mock_bio.getvalue.return_value.decode.side_effect = UnicodeDecodeError("utf-8", b"", 0, 1, "bad")
MockBytesIO.return_value = mock_bio
result = loader._download_file_content("f1", "application/pdf")
assert result is None
@pytest.mark.unit
def test_no_access_token_with_refresh(self, loader):
loader.credentials.token = None
loader.credentials.refresh_token = "rt"
loader.credentials.expired = False
with patch("google.auth.transport.requests.Request"):
loader.credentials.refresh = MagicMock()
# After refresh, set token
def set_token(req):
loader.credentials.token = "new_at"
loader.credentials.refresh.side_effect = set_token
loader._ensure_service = MagicMock()
loader.service.files.return_value.get_media.return_value = MagicMock()
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDownload:
mock_dl = MagicMock()
mock_dl.next_chunk.return_value = (None, True)
MockDownload.return_value = mock_dl
with patch("io.BytesIO") as MockBytesIO:
mock_bio = MagicMock()
mock_bio.getvalue.return_value = b"data"
MockBytesIO.return_value = mock_bio
content = loader._download_file_content("f1", "text/plain")
assert content == "data"
@pytest.mark.unit
def test_no_access_token_no_refresh_raises(self, loader):
loader.credentials.token = None
loader.credentials.refresh_token = None
with pytest.raises(ValueError, match="missing refresh_token"):
loader._download_file_content("f1", "text/plain")
@pytest.mark.unit
def test_no_token_refresh_fails_raises(self, loader):
loader.credentials.token = None
loader.credentials.refresh_token = "rt"
with patch("google.auth.transport.requests.Request"):
loader.credentials.refresh.side_effect = Exception("fail")
with pytest.raises(ValueError, match="missing or invalid refresh_token"):
loader._download_file_content("f1", "text/plain")
@pytest.mark.unit
def test_expired_credentials_refresh(self, loader):
loader.credentials.token = "at"
loader.credentials.expired = True
loader.credentials.refresh_token = "rt"
with patch("google.auth.transport.requests.Request"):
loader.credentials.refresh = MagicMock()
def fix_expired(req):
loader.credentials.expired = False
loader.credentials.refresh.side_effect = fix_expired
loader._ensure_service = MagicMock()
loader.service.files.return_value.get_media.return_value = MagicMock()
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDownload:
mock_dl = MagicMock()
mock_dl.next_chunk.return_value = (None, True)
MockDownload.return_value = mock_dl
with patch("io.BytesIO") as MockBytesIO:
mock_bio = MagicMock()
mock_bio.getvalue.return_value = b"ok"
MockBytesIO.return_value = mock_bio
content = loader._download_file_content("f1", "text/plain")
assert content == "ok"
@pytest.mark.unit
def test_expired_no_refresh_token_raises(self, loader):
loader.credentials.token = "at"
loader.credentials.expired = True
loader.credentials.refresh_token = None
with pytest.raises(ValueError, match="missing refresh_token"):
loader._download_file_content("f1", "text/plain")
@pytest.mark.unit
def test_expired_refresh_fails_raises(self, loader):
loader.credentials.token = "at"
loader.credentials.expired = True
loader.credentials.refresh_token = "rt"
with patch("google.auth.transport.requests.Request"):
loader.credentials.refresh.side_effect = Exception("fail")
with pytest.raises(ValueError, match="expired credentials"):
loader._download_file_content("f1", "text/plain")
@pytest.mark.unit
def test_http_error_during_download_chunk_returns_none(self, loader, mock_service):
from googleapiclient.errors import HttpError
mock_request = MagicMock()
mock_service.files.return_value.get_media.return_value = mock_request
resp = MagicMock()
resp.status = 500
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDownload:
mock_dl = MagicMock()
mock_dl.next_chunk.side_effect = HttpError(resp, b"server error")
MockDownload.return_value = mock_dl
with patch("io.BytesIO") as MockBytesIO:
MockBytesIO.return_value = MagicMock()
result = loader._download_file_content("f1", "text/plain")
assert result is None
@pytest.mark.unit
def test_general_error_during_download_chunk_returns_none(self, loader, mock_service):
mock_request = MagicMock()
mock_service.files.return_value.get_media.return_value = mock_request
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDownload:
mock_dl = MagicMock()
mock_dl.next_chunk.side_effect = RuntimeError("chunk fail")
MockDownload.return_value = mock_dl
with patch("io.BytesIO") as MockBytesIO:
MockBytesIO.return_value = MagicMock()
result = loader._download_file_content("f1", "text/plain")
assert result is None
@pytest.mark.unit
def test_http_401_during_download_refreshes_and_returns_none(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 401
mock_service.files.return_value.get_media.side_effect = HttpError(resp, b"unauth")
loader.credentials.refresh_token = "rt"
with patch("google.auth.transport.requests.Request"):
loader.credentials.refresh = MagicMock()
loader._ensure_service = MagicMock()
result = loader._download_file_content("f1", "text/plain")
assert result is None
assert loader._credential_refreshed is True
@pytest.mark.unit
def test_http_401_no_refresh_token_raises(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 401
mock_service.files.return_value.get_media.side_effect = HttpError(resp, b"unauth")
loader.credentials.refresh_token = None
with pytest.raises(ValueError, match="missing refresh_token"):
loader._download_file_content("f1", "text/plain")
@pytest.mark.unit
def test_http_401_refresh_fails_raises(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 401
mock_service.files.return_value.get_media.side_effect = HttpError(resp, b"unauth")
loader.credentials.refresh_token = "rt"
with patch("google.auth.transport.requests.Request"):
loader.credentials.refresh.side_effect = Exception("refresh fail")
with pytest.raises(ValueError, match="could not be refreshed"):
loader._download_file_content("f1", "text/plain")
@pytest.mark.unit
def test_http_500_returns_none(self, loader, mock_service):
from googleapiclient.errors import HttpError
resp = MagicMock()
resp.status = 500
mock_service.files.return_value.get_media.side_effect = HttpError(resp, b"error")
result = loader._download_file_content("f1", "text/plain")
assert result is None
@pytest.mark.unit
def test_general_exception_returns_none(self, loader, mock_service):
mock_service.files.return_value.get_media.side_effect = RuntimeError("fail")
result = loader._download_file_content("f1", "text/plain")
assert result is None
class TestDownloadToDirectory:
@pytest.mark.unit
def test_download_files(self, loader, tmp_path):
loader._download_file_to_directory = MagicMock(return_value=True)
result = loader.download_to_directory(str(tmp_path), {"file_ids": ["f1", "f2"]})
assert result["files_downloaded"] == 2
assert result["source_type"] == "google_drive"
assert result["empty_result"] is False
@pytest.mark.unit
def test_download_files_string_id(self, loader, tmp_path):
loader._download_file_to_directory = MagicMock(return_value=True)
result = loader.download_to_directory(str(tmp_path), {"file_ids": "single_id"})
assert result["files_downloaded"] == 1
@pytest.mark.unit
def test_download_folders(self, loader, tmp_path, mock_service):
mock_service.files.return_value.get.return_value.execute.return_value = {"name": "MyFolder"}
loader._download_folder_recursive = MagicMock(return_value=3)
result = loader.download_to_directory(str(tmp_path), {"folder_ids": ["folder1"]})
assert result["files_downloaded"] == 3
@pytest.mark.unit
def test_download_folders_string_id(self, loader, tmp_path, mock_service):
mock_service.files.return_value.get.return_value.execute.return_value = {"name": "F"}
loader._download_folder_recursive = MagicMock(return_value=1)
result = loader.download_to_directory(str(tmp_path), {"folder_ids": "single_folder"})
assert result["files_downloaded"] == 1
@pytest.mark.unit
def test_no_ids_returns_error(self, loader, tmp_path):
result = loader.download_to_directory(str(tmp_path), {})
assert "error" in result
assert result["empty_result"] is True
@pytest.mark.unit
def test_none_config_uses_empty(self, loader, tmp_path):
result = loader.download_to_directory(str(tmp_path))
assert "error" in result
@pytest.mark.unit
def test_folder_error_continues(self, loader, tmp_path, mock_service):
mock_service.files.return_value.get.return_value.execute.side_effect = Exception("fail")
loader._download_file_to_directory = MagicMock(return_value=True)
result = loader.download_to_directory(str(tmp_path), {"file_ids": ["f1"], "folder_ids": ["bad_folder"]})
assert result["files_downloaded"] == 1
class TestDownloadSingleFile:
@pytest.mark.unit
def test_downloads_supported_file(self, loader, mock_service, tmp_path):
mock_service.files.return_value.get.return_value.execute.return_value = {
"name": "test.txt", "mimeType": "text/plain"
}
mock_request = MagicMock()
mock_service.files.return_value.get_media.return_value = mock_request
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDl:
mock_dl = MagicMock()
mock_dl.next_chunk.return_value = (None, True)
MockDl.return_value = mock_dl
result = loader._download_single_file("f1", str(tmp_path))
assert result is True
@pytest.mark.unit
def test_unsupported_mime_returns_false(self, loader, mock_service, tmp_path):
mock_service.files.return_value.get.return_value.execute.return_value = {
"name": "test.zip", "mimeType": "application/zip"
}
result = loader._download_single_file("f1", str(tmp_path))
assert result is False
@pytest.mark.unit
def test_google_workspace_file_export(self, loader, mock_service, tmp_path):
mock_service.files.return_value.get.return_value.execute.return_value = {
"name": "doc", "mimeType": "application/vnd.google-apps.document"
}
mock_request = MagicMock()
mock_service.files.return_value.export_media.return_value = mock_request
with patch("application.parser.connectors.google_drive.loader.MediaIoBaseDownload") as MockDl:
mock_dl = MagicMock()
mock_dl.next_chunk.return_value = (None, True)
MockDl.return_value = mock_dl
result = loader._download_single_file("f1", str(tmp_path))
assert result is True
mock_service.files.return_value.export_media.assert_called_once()
class TestDownloadFolderRecursive:
@pytest.mark.unit
def test_downloads_files_in_folder(self, loader, mock_service, tmp_path):
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [
{"id": "f1", "name": "file1.txt", "mimeType": "text/plain"},
],
"nextPageToken": None,
}
loader._download_single_file = MagicMock(return_value=True)
count = loader._download_folder_recursive("folder1", str(tmp_path))
assert count == 1
@pytest.mark.unit
def test_recurses_into_subfolders(self, loader, mock_service, tmp_path):
# First call: folder with subfolder and file
# Second call: subfolder contents
call_count = [0]
def list_side_effect():
mock = MagicMock()
if call_count[0] == 0:
call_count[0] += 1
mock.execute.return_value = {
"files": [
{"id": "sub1", "name": "subfolder", "mimeType": "application/vnd.google-apps.folder"},
{"id": "f1", "name": "file1.txt", "mimeType": "text/plain"},
],
"nextPageToken": None,
}
else:
mock.execute.return_value = {
"files": [
{"id": "f2", "name": "file2.txt", "mimeType": "text/plain"},
],
"nextPageToken": None,
}
return mock
mock_service.files.return_value.list.side_effect = lambda **kw: list_side_effect()
loader._download_single_file = MagicMock(return_value=True)
count = loader._download_folder_recursive("folder1", str(tmp_path), recursive=True)
assert count == 2
@pytest.mark.unit
def test_non_recursive_skips_subfolders(self, loader, mock_service, tmp_path):
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [
{"id": "sub1", "name": "subfolder", "mimeType": "application/vnd.google-apps.folder"},
{"id": "f1", "name": "file1.txt", "mimeType": "text/plain"},
],
"nextPageToken": None,
}
loader._download_single_file = MagicMock(return_value=True)
count = loader._download_folder_recursive("folder1", str(tmp_path), recursive=False)
assert count == 1
@pytest.mark.unit
def test_download_failure_continues(self, loader, mock_service, tmp_path):
mock_service.files.return_value.list.return_value.execute.return_value = {
"files": [
{"id": "f1", "name": "fail.txt", "mimeType": "text/plain"},
{"id": "f2", "name": "ok.txt", "mimeType": "text/plain"},
],
"nextPageToken": None,
}
loader._download_single_file = MagicMock(side_effect=[False, True])
count = loader._download_folder_recursive("folder1", str(tmp_path))
assert count == 1
@pytest.mark.unit
def test_exception_returns_partial_count(self, loader, mock_service, tmp_path):
mock_service.files.return_value.list.return_value.execute.side_effect = Exception("fail")
count = loader._download_folder_recursive("folder1", str(tmp_path))
assert count == 0
class TestDownloadFileToDirectory:
@pytest.mark.unit
def test_delegates_to_download_single(self, loader, tmp_path):
loader._download_single_file = MagicMock(return_value=True)
assert loader._download_file_to_directory("f1", str(tmp_path)) is True
@pytest.mark.unit
def test_exception_returns_false(self, loader, tmp_path):
loader._download_single_file = MagicMock(side_effect=Exception("fail"))
assert loader._download_file_to_directory("f1", str(tmp_path)) is False
class TestDownloadFolderContents:
@pytest.mark.unit
def test_delegates_to_recursive(self, loader, tmp_path):
loader._download_folder_recursive = MagicMock(return_value=5)
count = loader._download_folder_contents("folder1", str(tmp_path))
assert count == 5
@pytest.mark.unit
def test_exception_returns_zero(self, loader, tmp_path):
loader.service = None
loader.auth.build_drive_service.side_effect = Exception("fail")
count = loader._download_folder_contents("folder1", str(tmp_path))
assert count == 0
class TestEnsureService:
@pytest.mark.unit
def test_builds_service_when_none(self, loader):
loader.service = None
mock_svc = MagicMock()
loader.auth.build_drive_service.return_value = mock_svc
loader._ensure_service()
assert loader.service == mock_svc
@pytest.mark.unit
def test_noop_when_service_exists(self, loader, mock_service):
loader._ensure_service()
assert loader.service == mock_service
@pytest.mark.unit
def test_build_failure_raises(self, loader):
loader.service = None
loader.auth.build_drive_service.side_effect = Exception("fail")
with pytest.raises(ValueError, match="Cannot access Google Drive"):
loader._ensure_service()
class TestGetExtensionForMimeType:
@pytest.mark.unit
def test_known_mime_types(self, loader):
assert loader._get_extension_for_mime_type("application/pdf") == ".pdf"
assert loader._get_extension_for_mime_type("text/plain") == ".txt"
assert loader._get_extension_for_mime_type("text/html") == ".html"
assert loader._get_extension_for_mime_type("text/markdown") == ".md"
@pytest.mark.unit
def test_unknown_returns_bin(self, loader):
assert loader._get_extension_for_mime_type("application/unknown") == ".bin"
@@ -0,0 +1,326 @@
"""Tests for SharePointAuth."""
import datetime
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def mock_settings():
s = MagicMock()
s.MICROSOFT_CLIENT_ID = "ms-client-id"
s.MICROSOFT_CLIENT_SECRET = "ms-client-secret"
s.MICROSOFT_TENANT_ID = "tenant-id-123"
s.CONNECTOR_REDIRECT_BASE_URI = "https://redirect.example.com/callback"
s.MONGO_DB_NAME = "test_db"
# Delete MICROSOFT_AUTHORITY so getattr falls back to default
del s.MICROSOFT_AUTHORITY
return s
@pytest.fixture
def mock_msal():
with patch("application.parser.connectors.share_point.auth.ConfidentialClientApplication") as MockMSAL:
mock_app = MagicMock()
MockMSAL.return_value = mock_app
yield mock_app
@pytest.fixture
def auth(mock_settings, mock_msal):
with patch("application.parser.connectors.share_point.auth.settings", mock_settings):
from application.parser.connectors.share_point.auth import SharePointAuth
return SharePointAuth()
class TestSharePointAuthInit:
@pytest.mark.unit
def test_init_sets_attributes(self, auth, mock_settings):
assert auth.client_id == "ms-client-id"
assert auth.client_secret == "ms-client-secret"
assert auth.redirect_uri == "https://redirect.example.com/callback"
assert auth.tenant_id == "tenant-id-123"
@pytest.mark.unit
def test_missing_client_id_raises(self, mock_settings):
mock_settings.MICROSOFT_CLIENT_ID = None
with patch("application.parser.connectors.share_point.auth.settings", mock_settings), \
patch("application.parser.connectors.share_point.auth.ConfidentialClientApplication"):
from application.parser.connectors.share_point.auth import SharePointAuth
with pytest.raises(ValueError, match="MICROSOFT_CLIENT_ID"):
SharePointAuth()
@pytest.mark.unit
def test_missing_client_secret_raises(self, mock_settings):
mock_settings.MICROSOFT_CLIENT_SECRET = None
with patch("application.parser.connectors.share_point.auth.settings", mock_settings), \
patch("application.parser.connectors.share_point.auth.ConfidentialClientApplication"):
from application.parser.connectors.share_point.auth import SharePointAuth
with pytest.raises(ValueError, match="MICROSOFT_CLIENT_SECRET"):
SharePointAuth()
@pytest.mark.unit
def test_default_authority(self, auth):
assert "login.microsoftonline.com" in auth.authority
assert "tenant-id-123" in auth.authority
class TestGetAuthorizationUrl:
@pytest.mark.unit
def test_returns_url(self, auth, mock_msal):
mock_msal.get_authorization_request_url.return_value = "https://login.microsoftonline.com/auth?state=s1"
url = auth.get_authorization_url(state="s1")
assert url == "https://login.microsoftonline.com/auth?state=s1"
mock_msal.get_authorization_request_url.assert_called_once()
class TestExchangeCodeForTokens:
@pytest.mark.unit
def test_successful_exchange(self, auth, mock_msal):
mock_msal.acquire_token_by_authorization_code.return_value = {
"access_token": "at",
"refresh_token": "rt",
"scope": ["Files.Read"],
"id_token_claims": {
"iss": "https://login.microsoftonline.com/tid/v2.0",
"exp": 1700000000,
"tid": "work-tenant-id",
"name": "Test User",
"preferred_username": "test@example.com",
},
}
result = auth.exchange_code_for_tokens("auth_code")
assert result["access_token"] == "at"
assert result["refresh_token"] == "rt"
assert result["user_info"]["name"] == "Test User"
assert result["user_info"]["email"] == "test@example.com"
assert result["allows_shared_content"] is True
@pytest.mark.unit
def test_error_in_response_raises(self, auth, mock_msal):
mock_msal.acquire_token_by_authorization_code.return_value = {
"error": "invalid_grant",
"error_description": "Code expired",
}
with pytest.raises(ValueError, match="Code expired"):
auth.exchange_code_for_tokens("bad_code")
class TestRefreshAccessToken:
@pytest.mark.unit
def test_successful_refresh(self, auth, mock_msal):
mock_msal.acquire_token_by_refresh_token.return_value = {
"access_token": "new_at",
"refresh_token": "new_rt",
"scope": ["Files.Read"],
"id_token_claims": {
"iss": "https://issuer",
"exp": 1700001000,
"tid": "work-tid",
"name": "User",
"preferred_username": "u@example.com",
},
}
result = auth.refresh_access_token("old_rt")
assert result["access_token"] == "new_at"
assert result["refresh_token"] == "new_rt"
@pytest.mark.unit
def test_error_in_response_raises(self, auth, mock_msal):
mock_msal.acquire_token_by_refresh_token.return_value = {
"error": "invalid_grant",
"error_description": "Token revoked",
}
with pytest.raises(ValueError, match="Token revoked"):
auth.refresh_access_token("bad_rt")
class TestIsTokenExpired:
@pytest.mark.unit
def test_expired_token(self, auth):
past = int((datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp())
assert auth.is_token_expired({"expiry": past}) is True
@pytest.mark.unit
def test_valid_token(self, auth):
future = int((datetime.datetime.now() + datetime.timedelta(hours=1)).timestamp())
assert auth.is_token_expired({"expiry": future}) is False
@pytest.mark.unit
def test_within_buffer(self, auth):
almost_expired = int((datetime.datetime.now() + datetime.timedelta(seconds=30)).timestamp())
assert auth.is_token_expired({"expiry": almost_expired}) is True
@pytest.mark.unit
def test_none_token_info(self, auth):
assert auth.is_token_expired(None) is True
@pytest.mark.unit
def test_missing_expiry(self, auth):
assert auth.is_token_expired({}) is True
@pytest.mark.unit
def test_none_expiry(self, auth):
assert auth.is_token_expired({"expiry": None}) is True
class TestSanitizeTokenInfo:
@pytest.mark.unit
def test_includes_allows_shared_content(self, auth):
token_info = {
"access_token": "at",
"refresh_token": "rt",
"token_uri": "https://uri",
"expiry": 123,
"allows_shared_content": True,
}
result = auth.sanitize_token_info(token_info)
assert result["allows_shared_content"] is True
assert result["access_token"] == "at"
@pytest.mark.unit
def test_defaults_allows_shared_content_to_false(self, auth):
token_info = {
"access_token": "at",
"refresh_token": "rt",
"token_uri": "https://uri",
"expiry": 123,
}
result = auth.sanitize_token_info(token_info)
assert result["allows_shared_content"] is False
@pytest.mark.unit
def test_with_extra_fields(self, auth):
token_info = {
"access_token": "at",
"refresh_token": "rt",
"token_uri": "https://uri",
"expiry": 123,
"allows_shared_content": True,
}
result = auth.sanitize_token_info(token_info, custom="val")
assert result["custom"] == "val"
class TestAllowsSharedContent:
@pytest.mark.unit
def test_work_account_returns_true(self, auth):
claims = {"tid": "some-work-tenant-id"}
assert auth._allows_shared_content(claims) is True
@pytest.mark.unit
def test_personal_account_returns_false(self, auth):
claims = {"tid": "9188040d-6c67-4c5b-b112-36a304b66dad"}
assert auth._allows_shared_content(claims) is False
@pytest.mark.unit
def test_empty_tid_returns_false(self, auth):
assert auth._allows_shared_content({"tid": ""}) is False
assert auth._allows_shared_content({}) is False
class TestMapTokenResponse:
@pytest.mark.unit
def test_maps_all_fields(self, auth):
result = {
"access_token": "at",
"refresh_token": "rt",
"scope": ["Files.Read"],
"id_token_claims": {
"iss": "https://issuer",
"exp": 1700000000,
"tid": "work-tid",
"name": "User Name",
"preferred_username": "user@example.com",
},
}
mapped = auth.map_token_response(result)
assert mapped["access_token"] == "at"
assert mapped["refresh_token"] == "rt"
assert mapped["token_uri"] == "https://issuer"
assert mapped["scopes"] == ["Files.Read"]
assert mapped["expiry"] == 1700000000
assert mapped["user_info"]["name"] == "User Name"
assert mapped["user_info"]["email"] == "user@example.com"
assert mapped["allows_shared_content"] is True
@pytest.mark.unit
def test_missing_claims_uses_defaults(self, auth):
result = {"access_token": "at", "refresh_token": "rt"}
mapped = auth.map_token_response(result)
assert mapped["token_uri"] is None
assert mapped["expiry"] is None
assert mapped["user_info"]["name"] is None
assert mapped["allows_shared_content"] is False
class TestGetTokenInfoFromSession:
def _mock_mongo(self, mock_settings, find_one_return):
mock_collection = MagicMock()
mock_collection.find_one.return_value = find_one_return
mock_db = MagicMock()
mock_db.__getitem__ = MagicMock(return_value=mock_collection)
mock_client = {mock_settings.MONGO_DB_NAME: mock_db}
return mock_client
@pytest.mark.unit
def test_valid_session(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {
"session_token": "st",
"token_info": {"access_token": "at", "refresh_token": "rt"},
})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
result = auth.get_token_info_from_session("st")
assert result["access_token"] == "at"
assert "token_uri" in result
@pytest.mark.unit
def test_session_not_found_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, None)
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve SharePoint token"):
auth.get_token_info_from_session("bad")
@pytest.mark.unit
def test_missing_token_info_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {"session_token": "st"})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve SharePoint token"):
auth.get_token_info_from_session("st")
@pytest.mark.unit
def test_empty_token_info_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {"session_token": "st", "token_info": None})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve SharePoint token"):
auth.get_token_info_from_session("st")
@pytest.mark.unit
def test_missing_required_fields_raises(self, auth, mock_settings):
mock_client = self._mock_mongo(mock_settings, {
"session_token": "st",
"token_info": {"access_token": "at"},
})
with patch("application.core.mongo_db.MongoDB.get_client", return_value=mock_client), \
patch("application.core.settings.settings", mock_settings):
with pytest.raises(ValueError, match="Failed to retrieve SharePoint token"):
auth.get_token_info_from_session("st")
File diff suppressed because it is too large. Load diff