"""Comprehensive unit tests for app/api/dropbox.py module.""" from unittest.mock import MagicMock, patch import pytest @pytest.mark.unit class TestExchangeDropboxToken: """Tests for POST /dropbox/exchange-token endpoint.""" @patch("app.api.dropbox.exchange_oauth_token") def test_exchange_token_success(self, mock_exchange): """Test successful token exchange.""" mock_exchange.return_value = { "refresh_token": "refresh_token_value", "access_token": "access_token_value", "expires_in": 14400, } # Response should include refresh_token, access_token, expires_in pass @patch("app.api.dropbox.exchange_oauth_token") def test_exchange_token_without_expires_in(self, mock_exchange): """Test token exchange without expires_in field.""" mock_exchange.return_value = { "refresh_token": "refresh_token_value", "access_token": "access_token_value", } # Should use default expires_in of 14400 pass @patch("app.api.dropbox.exchange_oauth_token") def test_exchange_token_calls_oauth_helper(self, mock_exchange): """Test that exchange_oauth_token is called correctly.""" mock_exchange.return_value = { "refresh_token": "token", "access_token": "access", "expires_in": 3600, } # Should call with provider_name="Dropbox" # Should call with correct token_url # Should pass payload with all form data pass @pytest.mark.unit class TestUpdateDropboxSettings: """Tests for POST /dropbox/update-settings endpoint.""" def test_update_settings_refresh_token(self): """Test updating only refresh token.""" # Should update settings.dropbox_refresh_token pass def test_update_settings_all_fields(self): """Test updating all Dropbox settings.""" # Should update all fields: refresh_token, app_key, app_secret, folder_path pass def test_update_settings_partial_fields(self): """Test updating some fields (not all).""" # Should only update provided fields pass def test_update_settings_logs_updates(self): """Test that updates are logged.""" # Should log each updated field pass def test_update_settings_exception_handling(self): """Test handling of unexpected errors.""" # Should raise HTTPException with 500 status pass @pytest.mark.unit class TestTestDropboxToken: """Tests for GET /dropbox/test-token endpoint.""" @patch("app.api.dropbox.requests.post") def test_test_token_success(self, mock_post): """Test successful token validation.""" from app.config import settings mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "email": "test@example.com", "name": {"display_name": "Test User"}, } mock_post.return_value = mock_response with patch.object(settings, "dropbox_refresh_token", "token"): with patch.object(settings, "dropbox_app_key", "key"): with patch.object(settings, "dropbox_app_secret", "secret"): # Should return success # Should include account email and name pass @patch("app.api.dropbox.requests.post") def test_test_token_not_configured(self, mock_post): """Test when credentials are not configured.""" from app.config import settings with patch.object(settings, "dropbox_refresh_token", None): # Should return error indicating not configured pass @patch("app.api.dropbox.requests.post") def test_test_token_partial_config(self, mock_post): """Test with partial configuration (missing some credentials).""" from app.config import settings with patch.object(settings, "dropbox_refresh_token", "token"): with patch.object(settings, "dropbox_app_key", None): # Should return error pass @patch("app.api.dropbox.requests.post") def test_test_token_expired_requires_refresh(self, mock_post): """Test when access token is expired and needs refresh.""" from app.config import settings # First call returns 401 (expired) mock_response_401 = MagicMock() mock_response_401.status_code = 401 # Second call (refresh) returns success mock_refresh_response = MagicMock() mock_refresh_response.status_code = 200 mock_refresh_response.json.return_value = {"access_token": "new_token"} # Third call with new token succeeds mock_success_response = MagicMock() mock_success_response.status_code = 200 mock_success_response.json.return_value = { "email": "test@example.com", "name": {"display_name": "Test User"}, } mock_post.side_effect = [mock_response_401, mock_refresh_response, mock_success_response] with patch.object(settings, "dropbox_refresh_token", "token"): with patch.object(settings, "dropbox_app_key", "key"): with patch.object(settings, "dropbox_app_secret", "secret"): # Should refresh and succeed pass @patch("app.api.dropbox.requests.post") def test_test_token_refresh_failed(self, mock_post): """Test when refresh token is invalid.""" from app.config import settings # First call returns 401 (expired) mock_response_401 = MagicMock() mock_response_401.status_code = 401 # Refresh call fails mock_refresh_response = MagicMock() mock_refresh_response.status_code = 400 mock_refresh_response.text = "Invalid refresh token" mock_post.side_effect = [mock_response_401, mock_refresh_response] with patch.object(settings, "dropbox_refresh_token", "token"): with patch.object(settings, "dropbox_app_key", "key"): with patch.object(settings, "dropbox_app_secret", "secret"): # Should return error with needs_reauth: True pass @patch("app.api.dropbox.requests.post") def test_test_token_perpetual_token_info(self, mock_post): """Test that perpetual token info is returned.""" from app.config import settings mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "email": "test@example.com", "name": {"display_name": "Test User"}, } mock_post.return_value = mock_response with patch.object(settings, "dropbox_refresh_token", "token"): with patch.object(settings, "dropbox_app_key", "key"): with patch.object(settings, "dropbox_app_secret", "secret"): # token_info should indicate never expires pass @patch("app.api.dropbox.requests.post") def test_test_token_exception_handling(self, mock_post): """Test handling of exceptions.""" from app.config import settings mock_post.side_effect = Exception("Network error") with patch.object(settings, "dropbox_refresh_token", "token"): with patch.object(settings, "dropbox_app_key", "key"): with patch.object(settings, "dropbox_app_secret", "secret"): # Should return error with exception message pass @pytest.mark.unit class TestSaveDropboxSettings: """Tests for POST /dropbox/save-settings endpoint.""" @patch("builtins.open", create=True) @patch("os.path.exists") def test_save_settings_success(self, mock_exists, mock_open): """Test successful saving to .env file.""" mock_exists.return_value = True mock_file = MagicMock() mock_file.readlines.return_value = ["DROPBOX_REFRESH_TOKEN=old_token\n"] mock_open.return_value.__enter__.return_value = mock_file # Should update .env file and in-memory settings pass @patch("os.path.exists") def test_save_settings_no_env_file(self, mock_exists): """Test when .env file doesn't exist.""" mock_exists.return_value = False # Should raise HTTPException pass @patch("builtins.open", create=True) @patch("os.path.exists") def test_save_settings_uncomments_commented_line(self, mock_exists, mock_open): """Test that commented settings are uncommented.""" mock_exists.return_value = True mock_file = MagicMock() mock_file.readlines.return_value = ["# DROPBOX_REFRESH_TOKEN=old_token\n"] mock_open.return_value.__enter__.return_value = mock_file # Should uncomment the line pass @patch("builtins.open", create=True) @patch("os.path.exists") def test_save_settings_adds_missing_settings(self, mock_exists, mock_open): """Test that missing settings are added.""" mock_exists.return_value = True mock_file = MagicMock() mock_file.readlines.return_value = ["OTHER_SETTING=value\n"] mock_open.return_value.__enter__.return_value = mock_file # Should append new settings pass @patch("builtins.open", create=True) @patch("os.path.exists") def test_save_settings_optional_fields(self, mock_exists, mock_open): """Test saving with optional fields provided.""" mock_exists.return_value = True mock_file = MagicMock() mock_file.readlines.return_value = [] mock_open.return_value.__enter__.return_value = mock_file # Should save all provided fields pass @patch("builtins.open", create=True) @patch("os.path.exists") def test_save_settings_updates_memory(self, mock_exists, mock_open): """Test that in-memory settings are updated.""" mock_exists.return_value = True mock_file = MagicMock() mock_file.readlines.return_value = [] mock_open.return_value.__enter__.return_value = mock_file # Should update settings object pass @patch("builtins.open", create=True) @patch("os.path.exists") def test_save_settings_exception_handling(self, mock_exists, mock_open): """Test handling of file I/O errors.""" mock_exists.return_value = True mock_open.side_effect = IOError("Permission denied") # Should raise HTTPException with 500 status