"""Tests for app/tasks/upload_to_google_drive.py module.""" import json from unittest.mock import Mock, patch import pytest from google.auth.exceptions import RefreshError from app.tasks.upload_to_google_drive import ( extract_metadata_from_file, get_drive_service_oauth, get_google_drive_service, truncate_property_value, upload_to_google_drive, ) @pytest.mark.unit class TestGetDriveServiceOAuth: """Tests for get_drive_service_oauth function.""" @patch("app.tasks.upload_to_google_drive.build") @patch("app.tasks.upload_to_google_drive.OAuthCredentials") @patch("app.tasks.upload_to_google_drive.Request") @patch("app.tasks.upload_to_google_drive.settings") def test_creates_service_with_oauth(self, mock_settings, mock_request, mock_creds, mock_build): """Test creates Google Drive service with OAuth.""" mock_settings.google_drive_client_id = "client_id" mock_settings.google_drive_client_secret = "client_secret" mock_settings.google_drive_refresh_token = "refresh_token" mock_credentials = Mock() mock_creds.return_value = mock_credentials mock_service = Mock() mock_build.return_value = mock_service result = get_drive_service_oauth() assert result == mock_service mock_credentials.refresh.assert_called_once() @patch("app.tasks.upload_to_google_drive.settings") def test_returns_none_when_credentials_incomplete(self, mock_settings): """Test returns None when OAuth credentials are incomplete.""" mock_settings.google_drive_client_id = None mock_settings.google_drive_client_secret = "secret" mock_settings.google_drive_refresh_token = "token" result = get_drive_service_oauth() assert result is None @patch("app.tasks.upload_to_google_drive.OAuthCredentials") @patch("app.tasks.upload_to_google_drive.settings") def test_handles_refresh_error(self, mock_settings, mock_creds): """Test handles token refresh error.""" mock_settings.google_drive_client_id = "client_id" mock_settings.google_drive_client_secret = "client_secret" mock_settings.google_drive_refresh_token = "refresh_token" mock_credentials = Mock() mock_credentials.refresh.side_effect = RefreshError("Token expired") mock_creds.return_value = mock_credentials with pytest.raises(RefreshError): get_drive_service_oauth() @patch("app.tasks.upload_to_google_drive.OAuthCredentials") @patch("app.tasks.upload_to_google_drive.settings") def test_handles_generic_exception(self, mock_settings, mock_creds): """Test handles generic exception during OAuth setup.""" mock_settings.google_drive_client_id = "client_id" mock_settings.google_drive_client_secret = "client_secret" mock_settings.google_drive_refresh_token = "refresh_token" mock_credentials = Mock() mock_credentials.refresh.side_effect = Exception("Network error") mock_creds.return_value = mock_credentials result = get_drive_service_oauth() assert result is None @pytest.mark.unit class TestGetGoogleDriveService: """Tests for get_google_drive_service function.""" @patch("app.tasks.upload_to_google_drive.get_drive_service_oauth") @patch("app.tasks.upload_to_google_drive.settings") def test_uses_oauth_when_configured(self, mock_settings, mock_oauth): """Test uses OAuth service when configured.""" mock_settings.google_drive_use_oauth = True mock_service = Mock() mock_oauth.return_value = mock_service result = get_google_drive_service() assert result == mock_service mock_oauth.assert_called_once() @patch("app.tasks.upload_to_google_drive.build") @patch("app.tasks.upload_to_google_drive.Credentials") @patch("app.tasks.upload_to_google_drive.settings") def test_uses_service_account_by_default(self, mock_settings, mock_creds, mock_build): """Test uses service account by default.""" mock_settings.google_drive_use_oauth = False mock_settings.google_drive_credentials_json = '{"type": "service_account"}' mock_settings.google_drive_delegate_to = None mock_credentials = Mock() mock_creds.from_service_account_info.return_value = mock_credentials mock_service = Mock() mock_build.return_value = mock_service result = get_google_drive_service() assert result == mock_service @patch("app.tasks.upload_to_google_drive.settings") def test_returns_none_when_no_credentials(self, mock_settings): """Test returns None when no credentials configured.""" mock_settings.google_drive_use_oauth = False mock_settings.google_drive_credentials_json = None result = get_google_drive_service() assert result is None @patch("app.tasks.upload_to_google_drive.Credentials") @patch("app.tasks.upload_to_google_drive.settings") def test_delegates_to_user_when_configured(self, mock_settings, mock_creds): """Test delegates to user when configured.""" mock_settings.google_drive_use_oauth = False mock_settings.google_drive_credentials_json = '{"type": "service_account"}' mock_settings.google_drive_delegate_to = "user@example.com" mock_credentials = Mock() mock_delegated_creds = Mock() mock_credentials.with_subject.return_value = mock_delegated_creds mock_creds.from_service_account_info.return_value = mock_credentials get_google_drive_service() mock_credentials.with_subject.assert_called_once_with("user@example.com") @patch("app.tasks.upload_to_google_drive.Credentials") @patch("app.tasks.upload_to_google_drive.settings") def test_handles_invalid_json_credentials(self, mock_settings, mock_creds): """Test handles invalid JSON credentials.""" mock_settings.google_drive_use_oauth = False mock_settings.google_drive_credentials_json = "invalid json {" result = get_google_drive_service() assert result is None @pytest.mark.unit class TestExtractMetadataFromFile: """Tests for extract_metadata_from_file function.""" def test_returns_empty_dict_when_no_metadata(self, tmp_path): """Test returns empty dict when no metadata file exists.""" file_path = tmp_path / "test.pdf" file_path.write_text("test content") result = extract_metadata_from_file(str(file_path)) assert result == {} def test_loads_metadata_from_json_file(self, tmp_path): """Test loads metadata from JSON file.""" file_path = tmp_path / "test.pdf" file_path.write_text("test content") metadata = {"document_type": "invoice", "amount": 100.00} json_path = tmp_path / "test.json" json_path.write_text(json.dumps(metadata)) result = extract_metadata_from_file(str(file_path)) assert result == metadata def test_handles_invalid_json_gracefully(self, tmp_path): """Test handles invalid JSON gracefully.""" file_path = tmp_path / "test.pdf" file_path.write_text("test content") json_path = tmp_path / "test.json" json_path.write_text("invalid json {") result = extract_metadata_from_file(str(file_path)) assert result == {} @pytest.mark.unit class TestTruncatePropertyValue: """Tests for truncate_property_value function.""" def test_returns_original_value_when_under_limit(self): """Test returns original value when under byte limit.""" result = truncate_property_value("short_key", "short value") assert result == "short value" def test_truncates_long_value(self): """Test truncates long value to fit byte limit.""" long_value = "x" * 200 result = truncate_property_value("key", long_value, max_bytes=100) assert len(result.encode("utf-8")) < 100 assert result.endswith("...") def test_handles_unicode_characters(self): """Test handles Unicode characters correctly.""" unicode_value = "日本語テキスト" * 20 # Japanese text result = truncate_property_value("key", unicode_value, max_bytes=50) assert len(result.encode("utf-8")) < 100 # Should be truncated def test_handles_non_string_values(self): """Test converts non-string values to string.""" result = truncate_property_value("key", 12345) assert result == "12345" def test_respects_key_size_in_calculation(self): """Test respects key size in byte calculation.""" long_key = "very_long_key_name_that_takes_bytes" value = "x" * 100 result = truncate_property_value(long_key, value, max_bytes=100) total_bytes = len(long_key.encode("utf-8")) + len(result.encode("utf-8")) assert total_bytes <= 100 @pytest.mark.unit class TestUploadToGoogleDriveTask: """Tests for upload_to_google_drive task.""" @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") @patch("app.tasks.upload_to_google_drive.MediaFileUpload") @patch("app.tasks.upload_to_google_drive.settings") def test_uploads_file_successfully( self, mock_settings, mock_media, mock_exists, mock_log, mock_extract, mock_service, mock_basename, mock_splitext ): """Test uploads file to Google Drive successfully.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_settings.google_drive_folder_id = "folder_123" mock_extract.return_value = {} # Mock Google Drive service mock_drive_service = Mock() mock_files = Mock() mock_create = Mock() mock_execute = Mock( return_value={ "id": "file_123", "name": "test.pdf", "webViewLink": "https://drive.google.com/file/d/file_123", } ) mock_create.execute = mock_execute mock_files.create.return_value = mock_create mock_drive_service.files.return_value = mock_files mock_service.return_value = mock_drive_service mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_google_drive(mock_self, "/tmp/test.pdf") assert result["status"] == "Completed" assert result["google_drive_file_id"] == "file_123" assert result["google_drive_web_link"] == "https://drive.google.com/file/d/file_123" @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") def test_raises_error_when_file_not_found(self, mock_exists, mock_log, mock_basename): """Test raises error when file not found.""" mock_exists.return_value = False mock_basename.return_value = "file.pdf" mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(FileNotFoundError): upload_to_google_drive(mock_self, "/nonexistent/file.pdf") @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") def test_raises_error_when_service_init_fails( self, mock_exists, mock_log, mock_service, mock_basename, mock_splitext ): """Test raises error when service initialization fails.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_service.return_value = None mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(Exception, match="Failed to initialize Google Drive service"): upload_to_google_drive(mock_self, "/tmp/test.pdf") @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") @patch("app.tasks.upload_to_google_drive.MediaFileUpload") @patch("app.tasks.upload_to_google_drive.settings") def test_includes_metadata_in_upload( self, mock_settings, mock_media, mock_exists, mock_log, mock_extract, mock_service, mock_basename, mock_splitext ): """Test includes metadata in upload.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_settings.google_drive_folder_id = None metadata = {"document_type": "invoice", "amount": "100.00", "date": "2024-01-01"} mock_extract.return_value = metadata mock_drive_service = Mock() mock_files = Mock() mock_create = Mock() mock_execute = Mock( return_value={ "id": "file_123", "name": "test.pdf", "webViewLink": "https://drive.google.com/file/d/file_123", "properties": {"document_type": "invoice"}, } ) mock_create.execute = mock_execute mock_files.create.return_value = mock_create mock_drive_service.files.return_value = mock_files mock_service.return_value = mock_drive_service mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_google_drive(mock_self, "/tmp/test.pdf", True) assert result["metadata_included"] is True @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") @patch("app.tasks.upload_to_google_drive.MediaFileUpload") @patch("app.tasks.upload_to_google_drive.settings") def test_skips_nested_metadata_objects( self, mock_settings, mock_media, mock_exists, mock_log, mock_extract, mock_service, mock_basename, mock_splitext ): """Test skips nested objects in metadata.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_settings.google_drive_folder_id = None metadata = {"simple_field": "value", "nested_object": {"key": "value"}, "nested_list": [1, 2, 3]} mock_extract.return_value = metadata mock_drive_service = Mock() mock_files = Mock() mock_create = Mock() mock_execute = Mock( return_value={ "id": "file_123", "name": "test.pdf", "webViewLink": "https://drive.google.com/file/d/file_123", } ) mock_create.execute = mock_execute mock_files.create.return_value = mock_create mock_drive_service.files.return_value = mock_files mock_service.return_value = mock_drive_service mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_google_drive(mock_self, "/tmp/test.pdf", True) # Verify the create call was made mock_files.create.assert_called_once() call_args = mock_files.create.call_args file_metadata = call_args.kwargs["body"] # Nested objects should not be in properties if "properties" in file_metadata: assert "nested_object" not in file_metadata["properties"] assert "nested_list" not in file_metadata["properties"] @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") @patch("app.tasks.upload_to_google_drive.MediaFileUpload") @patch("app.tasks.upload_to_google_drive.settings") def test_handles_upload_exception( self, mock_settings, mock_media, mock_exists, mock_log, mock_service, mock_basename, mock_splitext ): """Test handles upload exception.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_settings.google_drive_folder_id = None mock_drive_service = Mock() mock_files = Mock() mock_files.create.side_effect = Exception("Upload failed") mock_drive_service.files.return_value = mock_files mock_service.return_value = mock_drive_service mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(Exception, match="Failed to upload"): upload_to_google_drive(mock_self, "/tmp/test.pdf") @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") @patch("app.tasks.upload_to_google_drive.MediaFileUpload") @patch("app.tasks.upload_to_google_drive.settings") def test_sets_parent_folder_when_configured( self, mock_settings, mock_media, mock_exists, mock_log, mock_extract, mock_service, mock_basename, mock_splitext ): """Test sets parent folder when configured.""" mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_exists.return_value = True mock_settings.google_drive_folder_id = "parent_folder_123" mock_extract.return_value = {} mock_drive_service = Mock() mock_files = Mock() mock_create = Mock() mock_execute = Mock( return_value={ "id": "file_123", "name": "test.pdf", "webViewLink": "https://drive.google.com/file/d/file_123", } ) mock_create.execute = mock_execute mock_files.create.return_value = mock_create mock_drive_service.files.return_value = mock_files mock_service.return_value = mock_drive_service mock_self = Mock() mock_self.request.id = "test-task-id" upload_to_google_drive(mock_self, "/tmp/test.pdf") # Verify parent folder was set call_args = mock_files.create.call_args file_metadata = call_args.kwargs["body"] assert file_metadata["parents"] == ["parent_folder_123"] @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") @patch("app.tasks.upload_to_google_drive.MediaFileUpload") @patch("app.tasks.upload_to_google_drive.settings") def test_skips_metadata_when_disabled( self, mock_settings, mock_media, mock_exists, mock_log, mock_service, mock_basename, mock_splitext ): """Test skips metadata extraction when include_metadata is False.""" mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_exists.return_value = True mock_settings.google_drive_folder_id = None mock_drive_service = Mock() mock_files = Mock() mock_create = Mock() mock_execute = Mock( return_value={ "id": "file_123", "name": "test.pdf", "webViewLink": "https://drive.google.com/file/d/file_123", } ) mock_create.execute = mock_execute mock_files.create.return_value = mock_create mock_drive_service.files.return_value = mock_files mock_service.return_value = mock_drive_service mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_google_drive(mock_self, "/tmp/test.pdf", False) assert result["status"] == "Completed" assert "metadata_included" not in result @patch("app.tasks.upload_to_google_drive.truncate_property_value") @patch("app.tasks.upload_to_google_drive.os.path.splitext") @patch("app.tasks.upload_to_google_drive.os.path.basename") @patch("app.tasks.upload_to_google_drive.get_google_drive_service") @patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") @patch("app.tasks.upload_to_google_drive.log_task_progress") @patch("app.tasks.upload_to_google_drive.os.path.exists") @patch("app.tasks.upload_to_google_drive.MediaFileUpload") @patch("app.tasks.upload_to_google_drive.settings") def test_handles_truncation_error_gracefully( self, mock_settings, mock_media, mock_exists, mock_log, mock_extract, mock_service, mock_basename, mock_splitext, mock_truncate, ): """Test handles truncation error gracefully and logs warning.""" mock_basename.return_value = "test.pdf" mock_splitext.return_value = ("/tmp/test", ".pdf") mock_exists.return_value = True mock_settings.google_drive_folder_id = None metadata = {"problematic_key": "value"} mock_extract.return_value = metadata mock_truncate.side_effect = Exception("Encoding error") mock_drive_service = Mock() mock_files = Mock() mock_create = Mock() mock_execute = Mock( return_value={ "id": "file_123", "name": "test.pdf", "webViewLink": "https://drive.google.com/file/d/file_123", } ) mock_create.execute = mock_execute mock_files.create.return_value = mock_create mock_drive_service.files.return_value = mock_files mock_service.return_value = mock_drive_service mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_google_drive(mock_self, "/tmp/test.pdf", True) # Should complete successfully despite truncation error assert result["status"] == "Completed" # Should still include metadata flag assert result["metadata_included"] is True