From a2fee4d632530db29881d2b2e4e7d2800911b1c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:18:56 +0000 Subject: [PATCH] Add tests for google_drive.py - achieved 85.61% coverage (up from 81.55%) Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- tests/test_api_google_drive_coverage.py | 299 ++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 tests/test_api_google_drive_coverage.py diff --git a/tests/test_api_google_drive_coverage.py b/tests/test_api_google_drive_coverage.py new file mode 100644 index 00000000..b2569db5 --- /dev/null +++ b/tests/test_api_google_drive_coverage.py @@ -0,0 +1,299 @@ +""" +Additional tests for app/api/google_drive.py to increase coverage from 81.55% to 85%+. + +Focuses on: +- Missing OAuth credentials validation paths (lines 127-128, 237-238, 248-249) +- Token retrieval error paths +""" + +from datetime import datetime, timedelta +from unittest.mock import MagicMock, Mock, patch + +import pytest +from fastapi import HTTPException +from fastapi.testclient import TestClient + + +@pytest.mark.unit +class TestTestGoogleDriveTokenMissingCredentials: + """Test missing credentials scenarios in test_google_drive_token endpoint.""" + + @patch("app.api.google_drive.settings") + def test_test_token_oauth_missing_client_id(self, mock_settings, client: TestClient): + """Test OAuth token test with missing client_id.""" + # Configure settings to trigger OAuth path + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: None) + type(mock_settings).google_drive_client_secret = property(lambda self: "secret") + type(mock_settings).google_drive_refresh_token = property(lambda self: "token") + + response = client.get("/api/google-drive/test-token") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not fully configured" in data["message"].lower() + + @patch("app.api.google_drive.settings") + def test_test_token_oauth_missing_client_secret(self, mock_settings, client: TestClient): + """Test OAuth token test with missing client_secret.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: "client_id") + type(mock_settings).google_drive_client_secret = property(lambda self: None) + type(mock_settings).google_drive_refresh_token = property(lambda self: "token") + + response = client.get("/api/google-drive/test-token") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not fully configured" in data["message"].lower() + + @patch("app.api.google_drive.settings") + def test_test_token_oauth_missing_refresh_token(self, mock_settings, client: TestClient): + """Test OAuth token test with missing refresh_token.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: "client_id") + type(mock_settings).google_drive_client_secret = property(lambda self: "secret") + type(mock_settings).google_drive_refresh_token = property(lambda self: None) + + response = client.get("/api/google-drive/test-token") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not fully configured" in data["message"].lower() + + @patch("app.api.google_drive.settings") + def test_test_token_oauth_empty_credentials(self, mock_settings, client: TestClient): + """Test OAuth token test with empty string credentials.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: "") + type(mock_settings).google_drive_client_secret = property(lambda self: "") + type(mock_settings).google_drive_refresh_token = property(lambda self: "") + + response = client.get("/api/google-drive/test-token") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not fully configured" in data["message"].lower() + + +@pytest.mark.unit +class TestGetGoogleDriveTokenInfo: + """Test get_google_drive_token_info endpoint edge cases.""" + + @patch("app.api.google_drive.settings") + def test_get_token_info_oauth_disabled(self, mock_settings, client: TestClient): + """Test get_token_info when OAuth is disabled.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: False) + + response = client.get("/api/google-drive/get-token-info") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not enabled" in data["message"].lower() or "service account" in data["message"].lower() + + @patch("app.api.google_drive.settings") + def test_get_token_info_missing_client_id(self, mock_settings, client: TestClient): + """Test get_token_info with missing client_id.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: None) + type(mock_settings).google_drive_client_secret = property(lambda self: "secret") + type(mock_settings).google_drive_refresh_token = property(lambda self: "token") + + response = client.get("/api/google-drive/get-token-info") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not fully configured" in data["message"].lower() + + @patch("app.api.google_drive.settings") + def test_get_token_info_missing_client_secret(self, mock_settings, client: TestClient): + """Test get_token_info with missing client_secret.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: "client_id") + type(mock_settings).google_drive_client_secret = property(lambda self: None) + type(mock_settings).google_drive_refresh_token = property(lambda self: "token") + + response = client.get("/api/google-drive/get-token-info") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not fully configured" in data["message"].lower() + + @patch("app.api.google_drive.settings") + def test_get_token_info_missing_refresh_token(self, mock_settings, client: TestClient): + """Test get_token_info with missing refresh_token.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: "client_id") + type(mock_settings).google_drive_client_secret = property(lambda self: "secret") + type(mock_settings).google_drive_refresh_token = property(lambda self: None) + + response = client.get("/api/google-drive/get-token-info") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert "not fully configured" in data["message"].lower() + + @patch("google.auth.transport.requests.Request") + @patch("google.oauth2.credentials.Credentials") + @patch("app.api.google_drive.settings") + def test_get_token_info_token_refresh_error( + self, mock_settings, mock_credentials_class, mock_request, client: TestClient + ): + """Test get_token_info when token refresh fails.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: "client_id") + type(mock_settings).google_drive_client_secret = property(lambda self: "secret") + type(mock_settings).google_drive_refresh_token = property(lambda self: "token") + + # Mock credentials to raise error on refresh + mock_creds = MagicMock() + mock_creds.valid = False + mock_creds.refresh.side_effect = Exception("invalid_grant: Token has been revoked") + mock_credentials_class.return_value = mock_creds + + response = client.get("/api/google-drive/get-token-info") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + # Should detect invalid_grant error + assert "needs_reauth" in data or "invalid_grant" in data["message"].lower() or "error" in data["status"] + + @patch("google.auth.transport.requests.Request") + @patch("google.oauth2.credentials.Credentials") + @patch("app.api.google_drive.settings") + def test_get_token_info_generic_error( + self, mock_settings, mock_credentials_class, mock_request, client: TestClient + ): + """Test get_token_info with generic error.""" + type(mock_settings).google_drive_use_oauth = property(lambda self: True) + type(mock_settings).google_drive_client_id = property(lambda self: "client_id") + type(mock_settings).google_drive_client_secret = property(lambda self: "secret") + type(mock_settings).google_drive_refresh_token = property(lambda self: "token") + + # Mock credentials to raise generic error + mock_creds = MagicMock() + mock_creds.valid = False + mock_creds.refresh.side_effect = Exception("Network timeout") + mock_credentials_class.return_value = mock_creds + + response = client.get("/api/google-drive/get-token-info") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + +@pytest.mark.unit +class TestSaveGoogleDriveSettings: + """Test save_google_drive_settings endpoint edge cases.""" + + @patch("app.api.google_drive.settings") + @patch("os.path.exists") + def test_save_settings_env_file_not_exists(self, mock_exists, mock_settings, client: TestClient): + """Test saving settings when .env file doesn't exist (Docker container scenario).""" + mock_exists.return_value = False + + response = client.post( + "/api/google-drive/save-settings", + data={ + "refresh_token": "new_token", + "client_id": "new_id", + "client_secret": "new_secret", + "folder_id": "folder123", + } + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["in_memory_only"] is True + + @patch("app.api.google_drive.settings") + @patch("os.path.exists") + def test_save_settings_only_refresh_token(self, mock_exists, mock_settings, client: TestClient): + """Test saving with only refresh_token (minimal required field).""" + mock_exists.return_value = False + + response = client.post( + "/api/google-drive/save-settings", + data={"refresh_token": "new_token"} + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + @patch("app.api.google_drive.settings") + @patch("builtins.open", side_effect=PermissionError("Permission denied")) + @patch("os.path.exists") + def test_save_settings_env_file_write_error(self, mock_exists, mock_open, mock_settings, client: TestClient): + """Test saving when .env file write fails but continues with in-memory update.""" + mock_exists.return_value = True + + response = client.post( + "/api/google-drive/save-settings", + data={"refresh_token": "new_token"} + ) + + # Should succeed (in-memory update) even if file write fails + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + +@pytest.mark.unit +class TestHelperFunctions: + """Test helper functions in google_drive module.""" + + def test_format_time_remaining_expired(self): + """Test format_time_remaining with negative timedelta.""" + from app.api.google_drive import format_time_remaining + from datetime import timedelta + + # Expired time + delta = timedelta(seconds=-1) + result = format_time_remaining(delta) + assert result == "Expired" + + def test_format_time_remaining_days(self): + """Test format_time_remaining with days.""" + from app.api.google_drive import format_time_remaining + from datetime import timedelta + + # 2 days, 3 hours + delta = timedelta(days=2, hours=3) + result = format_time_remaining(delta) + assert "2 days" in result + assert "3 hours" in result + + def test_format_time_remaining_hours_only(self): + """Test format_time_remaining with hours but no days.""" + from app.api.google_drive import format_time_remaining + from datetime import timedelta + + # 5 hours, 30 minutes + delta = timedelta(hours=5, minutes=30) + result = format_time_remaining(delta) + assert "5 hours" in result + assert "30 minutes" in result + + def test_format_time_remaining_singular_units(self): + """Test format_time_remaining with singular units.""" + from app.api.google_drive import format_time_remaining + from datetime import timedelta + + # 1 day, 1 hour + delta = timedelta(days=1, hours=1) + result = format_time_remaining(delta) + # Should use singular form (no 's') + assert "1 day" in result and "1 days" not in result + assert "1 hour" in result and "1 hours" not in result