""" Tests for app/tasks/upload_to_sharepoint.py module. Covers get_sharepoint_token, resolve_sharepoint_drive, create_sharepoint_upload_session, upload_large_file_sharepoint, and upload_to_sharepoint Celery task. """ from unittest.mock import Mock, patch import pytest @pytest.mark.unit class TestGetSharepointToken: """Tests for get_sharepoint_token function.""" @patch("app.tasks.upload_to_sharepoint.msal.ConfidentialClientApplication") @patch("app.tasks.upload_to_sharepoint.settings") def test_refresh_token_flow(self, mock_settings, mock_msal): """Test token acquisition using refresh token.""" from app.tasks.upload_to_sharepoint import get_sharepoint_token mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "client-secret" mock_settings.sharepoint_refresh_token = "refresh-token" mock_settings.sharepoint_tenant_id = "common" mock_app = Mock() mock_app.acquire_token_by_refresh_token.return_value = { "access_token": "new-access-token", } mock_msal.return_value = mock_app token = get_sharepoint_token() assert token == "new-access-token" @patch("app.tasks.upload_to_sharepoint.msal.ConfidentialClientApplication") @patch("app.tasks.upload_to_sharepoint.settings") def test_refresh_token_updates_new_token(self, mock_settings, mock_msal): """Test that a new refresh token updates settings.""" from app.tasks.upload_to_sharepoint import get_sharepoint_token mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "client-secret" mock_settings.sharepoint_refresh_token = "old-refresh-token" mock_settings.sharepoint_tenant_id = "common" mock_app = Mock() mock_app.acquire_token_by_refresh_token.return_value = { "access_token": "access-token", "refresh_token": "new-refresh-token", } mock_msal.return_value = mock_app get_sharepoint_token() assert mock_settings.sharepoint_refresh_token == "new-refresh-token" @patch("app.tasks.upload_to_sharepoint.msal.ConfidentialClientApplication") @patch("app.tasks.upload_to_sharepoint.settings") def test_refresh_token_failure(self, mock_settings, mock_msal): """Test error handling when refresh token fails.""" from app.tasks.upload_to_sharepoint import get_sharepoint_token mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "client-secret" mock_settings.sharepoint_refresh_token = "expired-token" mock_settings.sharepoint_tenant_id = "common" mock_app = Mock() mock_app.acquire_token_by_refresh_token.return_value = { "error": "invalid_grant", "error_description": "Token expired", } mock_msal.return_value = mock_app with pytest.raises(ValueError, match="Failed to get SharePoint access token"): get_sharepoint_token() @patch("app.tasks.upload_to_sharepoint.msal.ConfidentialClientApplication") @patch("app.tasks.upload_to_sharepoint.settings") def test_client_credentials_flow(self, mock_settings, mock_msal): """Test token acquisition using client credentials (org accounts).""" from app.tasks.upload_to_sharepoint import get_sharepoint_token mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "client-secret" mock_settings.sharepoint_refresh_token = "" mock_settings.sharepoint_tenant_id = "org-tenant-id" mock_app = Mock() mock_app.acquire_token_for_client.return_value = { "access_token": "client-cred-token", } mock_msal.return_value = mock_app token = get_sharepoint_token() assert token == "client-cred-token" @patch("app.tasks.upload_to_sharepoint.msal.ConfidentialClientApplication") @patch("app.tasks.upload_to_sharepoint.settings") def test_client_credentials_failure(self, mock_settings, mock_msal): """Test error handling when client credentials flow fails.""" from app.tasks.upload_to_sharepoint import get_sharepoint_token mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "client-secret" mock_settings.sharepoint_refresh_token = "" mock_settings.sharepoint_tenant_id = "org-tenant-id" mock_app = Mock() mock_app.acquire_token_for_client.return_value = { "error": "unauthorized_client", "error_description": "Not authorized", } mock_msal.return_value = mock_app with pytest.raises(ValueError, match="Failed to get SharePoint access token"): get_sharepoint_token() @patch("app.tasks.upload_to_sharepoint.settings") def test_missing_client_id(self, mock_settings): """Test error when client ID is missing.""" from app.tasks.upload_to_sharepoint import get_sharepoint_token mock_settings.sharepoint_client_id = "" mock_settings.sharepoint_client_secret = "secret" with pytest.raises(ValueError, match="client ID and client secret"): get_sharepoint_token() @patch("app.tasks.upload_to_sharepoint.settings") def test_no_refresh_token_common_tenant(self, mock_settings): """Test error for common tenant without refresh token.""" from app.tasks.upload_to_sharepoint import get_sharepoint_token mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "secret" mock_settings.sharepoint_refresh_token = "" mock_settings.sharepoint_tenant_id = "common" with pytest.raises(ValueError, match="either a refresh token or a non-'common' tenant ID"): get_sharepoint_token() @pytest.mark.unit class TestResolveSharepointDrive: """Tests for resolve_sharepoint_drive function.""" @patch("app.tasks.upload_to_sharepoint.requests.get") @patch("app.tasks.upload_to_sharepoint.settings") def test_successful_resolution(self, mock_settings, mock_get): """Test successful site and drive resolution.""" from app.tasks.upload_to_sharepoint import resolve_sharepoint_drive mock_settings.http_request_timeout = 30 site_resp = Mock() site_resp.status_code = 200 site_resp.json.return_value = {"id": "site-id-123"} drives_resp = Mock() drives_resp.status_code = 200 drives_resp.json.return_value = { "value": [ {"id": "drive-1", "name": "Documents"}, {"id": "drive-2", "name": "Site Assets"}, ] } mock_get.side_effect = [site_resp, drives_resp] site_id, drive_id = resolve_sharepoint_drive( "access-token", "https://tenant.sharepoint.com/sites/mysite", "Documents" ) assert site_id == "site-id-123" assert drive_id == "drive-1" @patch("app.tasks.upload_to_sharepoint.requests.get") @patch("app.tasks.upload_to_sharepoint.settings") def test_library_not_found(self, mock_settings, mock_get): """Test error when document library is not found.""" from app.tasks.upload_to_sharepoint import resolve_sharepoint_drive mock_settings.http_request_timeout = 30 site_resp = Mock() site_resp.status_code = 200 site_resp.json.return_value = {"id": "site-id-123"} drives_resp = Mock() drives_resp.status_code = 200 drives_resp.json.return_value = { "value": [ {"id": "drive-1", "name": "Documents"}, ] } mock_get.side_effect = [site_resp, drives_resp] with pytest.raises(RuntimeError, match="not found on site"): resolve_sharepoint_drive("access-token", "https://tenant.sharepoint.com/sites/mysite", "NonExistentLibrary") @patch("app.tasks.upload_to_sharepoint.requests.get") @patch("app.tasks.upload_to_sharepoint.settings") def test_site_resolution_failure(self, mock_settings, mock_get): """Test error when site resolution fails.""" from app.tasks.upload_to_sharepoint import resolve_sharepoint_drive mock_settings.http_request_timeout = 30 site_resp = Mock() site_resp.status_code = 404 site_resp.text = "Site not found" mock_get.return_value = site_resp with pytest.raises(RuntimeError, match="Failed to resolve SharePoint site"): resolve_sharepoint_drive("access-token", "https://tenant.sharepoint.com/sites/nonexistent", "Documents") def test_invalid_site_url(self): """Test error with invalid site URL.""" from app.tasks.upload_to_sharepoint import resolve_sharepoint_drive with pytest.raises(ValueError, match="Invalid SharePoint site URL"): resolve_sharepoint_drive("access-token", "not-a-url", "Documents") @patch("app.tasks.upload_to_sharepoint.requests.get") @patch("app.tasks.upload_to_sharepoint.settings") def test_case_insensitive_library_match(self, mock_settings, mock_get): """Test that library name matching is case-insensitive.""" from app.tasks.upload_to_sharepoint import resolve_sharepoint_drive mock_settings.http_request_timeout = 30 site_resp = Mock() site_resp.status_code = 200 site_resp.json.return_value = {"id": "site-id"} drives_resp = Mock() drives_resp.status_code = 200 drives_resp.json.return_value = { "value": [ {"id": "drive-1", "name": "Shared Documents"}, ] } mock_get.side_effect = [site_resp, drives_resp] site_id, drive_id = resolve_sharepoint_drive( "access-token", "https://tenant.sharepoint.com/sites/mysite", "shared documents" ) assert drive_id == "drive-1" @pytest.mark.unit class TestCreateSharepointUploadSession: """Tests for create_sharepoint_upload_session function.""" @patch("app.tasks.upload_to_sharepoint.requests.post") @patch("app.tasks.upload_to_sharepoint.settings") def test_successful_session_creation(self, mock_settings, mock_post): """Test successful upload session creation.""" from app.tasks.upload_to_sharepoint import create_sharepoint_upload_session mock_settings.http_request_timeout = 30 mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"uploadUrl": "https://upload.url/session123"} mock_post.return_value = mock_response url = create_sharepoint_upload_session("test.pdf", "Uploads", "drive-id", "site-id", "access-token") assert url == "https://upload.url/session123" @patch("app.tasks.upload_to_sharepoint.requests.post") @patch("app.tasks.upload_to_sharepoint.settings") def test_session_without_folder(self, mock_settings, mock_post): """Test upload session creation without folder path.""" from app.tasks.upload_to_sharepoint import create_sharepoint_upload_session mock_settings.http_request_timeout = 30 mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"uploadUrl": "https://upload.url/session456"} mock_post.return_value = mock_response url = create_sharepoint_upload_session("test.pdf", None, "drive-id", "site-id", "access-token") assert url == "https://upload.url/session456" @patch("app.tasks.upload_to_sharepoint.requests.post") @patch("app.tasks.upload_to_sharepoint.settings") def test_session_creation_failure(self, mock_settings, mock_post): """Test error handling when session creation fails.""" from app.tasks.upload_to_sharepoint import create_sharepoint_upload_session mock_settings.http_request_timeout = 30 mock_response = Mock() mock_response.status_code = 403 mock_response.text = "Access denied" mock_post.return_value = mock_response with pytest.raises(RuntimeError, match="Failed to create SharePoint upload session"): create_sharepoint_upload_session("test.pdf", "Uploads", "drive-id", "site-id", "access-token") @patch("app.tasks.upload_to_sharepoint.requests.post") @patch("app.tasks.upload_to_sharepoint.settings") def test_url_encoding_special_characters(self, mock_settings, mock_post): """Test that special characters in folder path are URL-encoded.""" from app.tasks.upload_to_sharepoint import create_sharepoint_upload_session mock_settings.http_request_timeout = 30 mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"uploadUrl": "https://upload.url/session"} mock_post.return_value = mock_response create_sharepoint_upload_session("file with spaces.pdf", "My Documents/Uploads", "drive-id", "site-id", "token") call_url = mock_post.call_args[0][0] assert "My%20Documents" in call_url assert "file%20with%20spaces.pdf" in call_url @pytest.mark.unit class TestUploadLargeFileSharepoint: """Tests for upload_large_file_sharepoint function.""" @patch("app.tasks.upload_to_sharepoint.requests.put") @patch("app.tasks.upload_to_sharepoint.settings") def test_small_single_chunk_upload(self, mock_settings, mock_put, tmp_path): """Test uploading a file that fits in a single chunk.""" from app.tasks.upload_to_sharepoint import upload_large_file_sharepoint mock_settings.http_request_timeout = 30 test_file = tmp_path / "small.pdf" test_file.write_bytes(b"small content") mock_response = Mock() mock_response.status_code = 201 mock_response.json.return_value = {"id": "file123", "name": "small.pdf"} mock_put.return_value = mock_response result = upload_large_file_sharepoint(str(test_file), "https://upload.url/session") assert result["id"] == "file123" @patch("app.tasks.upload_to_sharepoint.time.sleep") @patch("app.tasks.upload_to_sharepoint.requests.put") @patch("app.tasks.upload_to_sharepoint.settings") def test_chunk_upload_retry_on_failure(self, mock_settings, mock_put, mock_sleep, tmp_path): """Test retry logic when a chunk upload fails.""" from app.tasks.upload_to_sharepoint import upload_large_file_sharepoint mock_settings.http_request_timeout = 30 test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") mock_fail = Mock() mock_fail.status_code = 500 mock_success = Mock() mock_success.status_code = 201 mock_success.json.return_value = {"id": "file123"} mock_put.side_effect = [mock_fail, mock_success] result = upload_large_file_sharepoint(str(test_file), "https://upload.url/session") assert result["id"] == "file123" @patch("app.tasks.upload_to_sharepoint.time.sleep") @patch("app.tasks.upload_to_sharepoint.requests.put") @patch("app.tasks.upload_to_sharepoint.settings") def test_chunk_upload_retry_on_exception(self, mock_settings, mock_put, mock_sleep, tmp_path): """Test retry logic when an exception occurs during upload.""" from app.tasks.upload_to_sharepoint import upload_large_file_sharepoint mock_settings.http_request_timeout = 30 test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") mock_success = Mock() mock_success.status_code = 201 mock_success.json.return_value = {"id": "file123"} mock_put.side_effect = [Exception("Network error"), mock_success] result = upload_large_file_sharepoint(str(test_file), "https://upload.url/session") assert result["id"] == "file123" @patch("app.tasks.upload_to_sharepoint.time.sleep") @patch("app.tasks.upload_to_sharepoint.requests.put") @patch("app.tasks.upload_to_sharepoint.settings") def test_all_retries_exhausted(self, mock_settings, mock_put, mock_sleep, tmp_path): """Test that exhausting all retries raises an exception.""" from app.tasks.upload_to_sharepoint import upload_large_file_sharepoint mock_settings.http_request_timeout = 30 test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") mock_fail = Mock() mock_fail.status_code = 500 mock_fail.text = "Server Error" mock_put.return_value = mock_fail with pytest.raises(RuntimeError, match="Failed to upload chunk"): upload_large_file_sharepoint(str(test_file), "https://upload.url/session") @pytest.mark.unit class TestUploadToSharepoint: """Tests for upload_to_sharepoint Celery task.""" @patch("app.tasks.upload_to_sharepoint.log_task_progress") def test_file_not_found(self, mock_log): """Test that missing file raises FileNotFoundError.""" from app.tasks.upload_to_sharepoint import upload_to_sharepoint with pytest.raises(FileNotFoundError): upload_to_sharepoint.__wrapped__("/nonexistent/file.pdf", file_id=1) @patch("app.tasks.upload_to_sharepoint.log_task_progress") @patch("app.tasks.upload_to_sharepoint.settings") def test_missing_client_id(self, mock_settings, mock_log, tmp_path): """Test error when SharePoint client ID is not configured.""" from app.tasks.upload_to_sharepoint import upload_to_sharepoint mock_settings.sharepoint_client_id = "" test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") with pytest.raises(ValueError, match="client ID is not configured"): upload_to_sharepoint.__wrapped__(str(test_file), file_id=1) @patch("app.tasks.upload_to_sharepoint.log_task_progress") @patch("app.tasks.upload_to_sharepoint.settings") def test_missing_site_url(self, mock_settings, mock_log, tmp_path): """Test error when SharePoint site URL is not configured.""" from app.tasks.upload_to_sharepoint import upload_to_sharepoint mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_site_url = "" test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") with pytest.raises(ValueError, match="site URL is not configured"): upload_to_sharepoint.__wrapped__(str(test_file), file_id=1) @patch("app.tasks.upload_to_sharepoint.upload_large_file_sharepoint") @patch("app.tasks.upload_to_sharepoint.create_sharepoint_upload_session") @patch("app.tasks.upload_to_sharepoint.resolve_sharepoint_drive") @patch("app.tasks.upload_to_sharepoint.get_sharepoint_token") @patch("app.tasks.upload_to_sharepoint.log_task_progress") @patch("app.tasks.upload_to_sharepoint.settings") def test_successful_upload( self, mock_settings, mock_log, mock_token, mock_resolve, mock_session, mock_upload, tmp_path ): """Test successful SharePoint upload.""" from app.tasks.upload_to_sharepoint import upload_to_sharepoint mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "secret" mock_settings.sharepoint_refresh_token = "token" mock_settings.sharepoint_site_url = "https://tenant.sharepoint.com/sites/mysite" mock_settings.sharepoint_document_library = "Documents" mock_settings.sharepoint_folder_path = "Uploads" mock_settings.sharepoint_tenant_id = "common" test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") mock_token.return_value = "access-token" mock_resolve.return_value = ("site-id", "drive-id") mock_session.return_value = "https://upload.url/session" mock_upload.return_value = {"webUrl": "https://tenant.sharepoint.com/sites/mysite/test.pdf"} result = upload_to_sharepoint.apply(args=[str(test_file)], kwargs={"file_id": 1}).get() assert result["status"] == "Completed" assert "Uploads" in result["sharepoint_path"] assert result["web_url"] == "https://tenant.sharepoint.com/sites/mysite/test.pdf" @patch("app.tasks.upload_to_sharepoint.get_sharepoint_token") @patch("app.tasks.upload_to_sharepoint.log_task_progress") @patch("app.tasks.upload_to_sharepoint.settings") def test_upload_exception_handling(self, mock_settings, mock_log, mock_token, tmp_path): """Test that upload errors are properly handled.""" from app.tasks.upload_to_sharepoint import upload_to_sharepoint mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "secret" mock_settings.sharepoint_site_url = "https://tenant.sharepoint.com/sites/mysite" mock_settings.sharepoint_folder_path = "Uploads" mock_settings.sharepoint_document_library = "Documents" test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") mock_token.side_effect = ValueError("Token error") with pytest.raises(RuntimeError, match="Failed to upload"): upload_to_sharepoint.apply(args=[str(test_file)], kwargs={"file_id": 1}).get() @patch("app.tasks.upload_to_sharepoint.upload_large_file_sharepoint") @patch("app.tasks.upload_to_sharepoint.create_sharepoint_upload_session") @patch("app.tasks.upload_to_sharepoint.resolve_sharepoint_drive") @patch("app.tasks.upload_to_sharepoint.get_sharepoint_token") @patch("app.tasks.upload_to_sharepoint.log_task_progress") @patch("app.tasks.upload_to_sharepoint.settings") def test_folder_override( self, mock_settings, mock_log, mock_token, mock_resolve, mock_session, mock_upload, tmp_path ): """Test that folder_override is used instead of settings.""" from app.tasks.upload_to_sharepoint import upload_to_sharepoint mock_settings.sharepoint_client_id = "client-id" mock_settings.sharepoint_client_secret = "secret" mock_settings.sharepoint_refresh_token = "token" mock_settings.sharepoint_site_url = "https://tenant.sharepoint.com/sites/mysite" mock_settings.sharepoint_document_library = "Documents" mock_settings.sharepoint_folder_path = "DefaultFolder" mock_settings.sharepoint_tenant_id = "common" test_file = tmp_path / "test.pdf" test_file.write_bytes(b"test content") mock_token.return_value = "access-token" mock_resolve.return_value = ("site-id", "drive-id") mock_session.return_value = "https://upload.url/session" mock_upload.return_value = {"webUrl": "https://example.com/test.pdf"} result = upload_to_sharepoint.apply( args=[str(test_file)], kwargs={"file_id": 1, "folder_override": "CustomFolder"} ).get() # Verify the session was created with the override folder mock_session.assert_called_once_with("test.pdf", "CustomFolder", "drive-id", "site-id", "access-token") assert result["status"] == "Completed"