From 7242f3c168396aa5400fd46ad531ede93024b467 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:24:58 +0000 Subject: [PATCH 1/5] perf(onedrive): use async httpx for token refresh Replaces the synchronous `requests.post` and `requests.get` calls in `app/api/onedrive.py:test_onedrive_token` with an asynchronous `httpx.AsyncClient` implementation. This unblocks the FastAPI event loop when this endpoint is hit. Tests were updated to mock `httpx.AsyncClient` and a sync wrapper using `asyncio.run` was added to integration tests to maintain test coverage without massive test refactoring. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/api/onedrive.py | 39 ++++++++-------- benchmark_onedrive.py | 69 +++++++++++++++++++++++++++++ tests/test_api_onedrive_coverage.py | 28 ++++++------ tests/test_external_integrations.py | 17 ++++--- 4 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 benchmark_onedrive.py diff --git a/app/api/onedrive.py b/app/api/onedrive.py index e9f8328d..84964d61 100644 --- a/app/api/onedrive.py +++ b/app/api/onedrive.py @@ -7,6 +7,7 @@ import os from datetime import datetime, timedelta from typing import Annotated, Optional +import httpx import requests from fastapi import APIRouter, Depends, Form, HTTPException, Request, status from sqlalchemy.orm import Session @@ -92,17 +93,18 @@ async def test_onedrive_token(request: Request): "scope": "offline_access Files.ReadWrite", } - response = requests.post(token_url, data=refresh_data, timeout=settings.http_request_timeout) + async with httpx.AsyncClient(timeout=settings.http_request_timeout) as client: + response = await client.post(token_url, data=refresh_data) - if response.status_code != 200: - logger.error(f"Failed to refresh OneDrive token: {response.text}") - return { - "status": "error", - "message": "Refresh token has expired or is invalid", - "needs_reauth": True, - } + if response.status_code != 200: + logger.error(f"Failed to refresh OneDrive token: {response.text}") + return { + "status": "error", + "message": "Refresh token has expired or is invalid", + "needs_reauth": True, + } - token_data = response.json() + token_data = response.json() access_token = token_data.get("access_token") expires_in = token_data.get("expires_in", 3600) # Default to 1 hour if not specified @@ -164,17 +166,18 @@ async def test_onedrive_token(request: Request): user_info_url = "https://graph.microsoft.com/v1.0/me" headers = {"Authorization": f"Bearer {access_token}"} - user_response = requests.get(user_info_url, headers=headers, timeout=settings.http_request_timeout) + async with httpx.AsyncClient(timeout=settings.http_request_timeout) as client: + user_response = await client.get(user_info_url, headers=headers) - if user_response.status_code != 200: - logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}") - return { - "status": "error", - "message": f"Token validation failed with status {user_response.status_code}: {user_response.text}", - } + if user_response.status_code != 200: + logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}") + return { + "status": "error", + "message": f"Token validation failed with status {user_response.status_code}: {user_response.text}", + } - # Get user info - user_info = user_response.json() + # Get user info + user_info = user_response.json() display_name = user_info.get("displayName", "Unknown user") email = user_info.get("userPrincipalName", "Unknown email") diff --git a/benchmark_onedrive.py b/benchmark_onedrive.py new file mode 100644 index 00000000..7dc81447 --- /dev/null +++ b/benchmark_onedrive.py @@ -0,0 +1,69 @@ +import asyncio +import time +import httpx +from unittest.mock import patch, MagicMock, AsyncMock +from app.api.onedrive import test_onedrive_token +from app.config import settings + +settings.onedrive_refresh_token = "dummy" +settings.onedrive_client_id = "dummy" +settings.onedrive_client_secret = "dummy" + +class DummyRequest: + def __init__(self): + self.session = {"user": "dummy"} + +async def run_benchmark(func_name, mock_post, mock_get): + mock_post_resp = MagicMock() + mock_post_resp.status_code = 200 + mock_post_resp.json.return_value = { + "access_token": "dummy_access", + "expires_in": 3600 + } + mock_post.return_value = mock_post_resp + + mock_get_resp = MagicMock() + mock_get_resp.status_code = 200 + mock_get_resp.json.return_value = { + "displayName": "Test User", + "userPrincipalName": "test@example.com" + } + mock_get.return_value = mock_get_resp + + start_time = time.time() + for _ in range(100): + await test_onedrive_token(DummyRequest()) + end_time = time.time() + print(f"{func_name} took {end_time - start_time:.4f} seconds") + +async def run_benchmark_async(func_name, mock_post, mock_get): + mock_post_resp = MagicMock() + mock_post_resp.status_code = 200 + mock_post_resp.json = MagicMock(return_value={ + "access_token": "dummy_access", + "expires_in": 3600 + }) + mock_post.return_value = mock_post_resp + + mock_get_resp = MagicMock() + mock_get_resp.status_code = 200 + mock_get_resp.json = MagicMock(return_value={ + "displayName": "Test User", + "userPrincipalName": "test@example.com" + }) + mock_get.return_value = mock_get_resp + + start_time = time.time() + for _ in range(100): + await test_onedrive_token(DummyRequest()) + end_time = time.time() + print(f"{func_name} took {end_time - start_time:.4f} seconds") + + +@patch('app.api.onedrive.requests.get') +@patch('app.api.onedrive.requests.post') +def benchmark_sync(mock_post, mock_get): + asyncio.run(run_benchmark("Sync requests (baseline)", mock_post, mock_get)) + +if __name__ == "__main__": + benchmark_sync() diff --git a/tests/test_api_onedrive_coverage.py b/tests/test_api_onedrive_coverage.py index fd3e63dd..ea235e97 100644 --- a/tests/test_api_onedrive_coverage.py +++ b/tests/test_api_onedrive_coverage.py @@ -5,7 +5,7 @@ Focuses on uncovered lines: 98-99, 121-143, 160-161, 170-171, 324-326, 400-402, 436-438. """ -from unittest.mock import MagicMock, PropertyMock, patch +from unittest.mock import MagicMock, PropertyMock, patch, AsyncMock import pytest from fastapi.testclient import TestClient @@ -15,7 +15,7 @@ from fastapi.testclient import TestClient class TestTestTokenRefreshFailed: """Cover lines 98-99: token refresh returns non-200.""" - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_test_token_refresh_returns_non_200(self, mock_post, client: TestClient): """Test token refresh returning a failure status hits the error branch.""" from app.config import settings @@ -42,8 +42,8 @@ class TestTestTokenRefreshFailed: class TestTestTokenRotation: """Cover lines 121-143, 160-161: token rotation with .env and DB persist.""" - @patch("app.api.onedrive.requests.get") - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_token_rotation_env_file_exists(self, mock_post, mock_get, client: TestClient, tmp_path): """When a new refresh token is received and .env file exists, it should be updated.""" from app.config import settings @@ -90,8 +90,8 @@ class TestTestTokenRotation: data = response.json() assert data["status"] == "success" - @patch("app.api.onedrive.requests.get") - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_token_rotation_env_not_existing(self, mock_post, mock_get, client: TestClient): """Token rotation when .env doesn't exist still succeeds.""" from app.config import settings @@ -130,8 +130,8 @@ class TestTestTokenRotation: assert response.status_code == 200 assert response.json()["status"] == "success" - @patch("app.api.onedrive.requests.get") - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_token_rotation_env_write_failure(self, mock_post, mock_get, client: TestClient): """Token rotation when .env write fails (lines 142-143) still continues.""" from app.config import settings @@ -171,8 +171,8 @@ class TestTestTokenRotation: assert response.status_code == 200 assert response.json()["status"] == "success" - @patch("app.api.onedrive.requests.get") - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_token_rotation_db_persist_failure(self, mock_post, mock_get, client: TestClient): """Token rotation when DB persist fails (lines 160-161) still continues.""" from app.config import settings @@ -211,8 +211,8 @@ class TestTestTokenRotation: class TestTestTokenUserInfoFailed: """Cover lines 170-171: user info request fails.""" - @patch("app.api.onedrive.requests.get") - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_user_info_returns_non_200(self, mock_post, mock_get, client: TestClient): """Test when user info request fails after successful token refresh.""" from app.config import settings @@ -247,8 +247,8 @@ class TestTestTokenUserInfoFailed: class TestTokenRotationEnvAppendLine: """Cover the branch at line 134 where token line is not found in .env and must be appended.""" - @patch("app.api.onedrive.requests.get") - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_token_rotation_appends_to_env(self, mock_post, mock_get, client: TestClient, tmp_path): """When .env exists but doesn't have ONEDRIVE_REFRESH_TOKEN, it should append.""" from app.config import settings diff --git a/tests/test_external_integrations.py b/tests/test_external_integrations.py index 5fab2016..4def1ed5 100644 --- a/tests/test_external_integrations.py +++ b/tests/test_external_integrations.py @@ -417,14 +417,19 @@ class TestOneDriveIntegration: def test_onedrive_token_refresh_and_user_info(self, original_env: dict) -> None: """Validate token refresh and user info retrieval.""" - import requests + import asyncio + import httpx token = self._get_access_token(original_env) - resp = requests.get( - "https://graph.microsoft.com/v1.0/me", - headers={"Authorization": f"Bearer {token}"}, - timeout=30, - ) + + async def _test(): + async with httpx.AsyncClient(timeout=30) as client: + return await client.get( + "https://graph.microsoft.com/v1.0/me", + headers={"Authorization": f"Bearer {token}"}, + ) + + resp = asyncio.run(_test()) assert resp.status_code == 200, f"OneDrive user info failed: {resp.text}" def test_onedrive_upload_download_delete(self, original_env: dict) -> None: From ca2d023d8130000fb82482fb8fbe0d7e218c94fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Mar 2026 09:25:47 +0000 Subject: [PATCH 2/5] style: apply ruff auto-fix - Auto-formatted code with ruff format - Applied ruff linting fixes with --fix Co-authored-by: github-actions[bot] --- app/api/onedrive.py | 1 - tests/test_api_onedrive_coverage.py | 2 +- tests/test_external_integrations.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/onedrive.py b/app/api/onedrive.py index 84964d61..374172b9 100644 --- a/app/api/onedrive.py +++ b/app/api/onedrive.py @@ -8,7 +8,6 @@ from datetime import datetime, timedelta from typing import Annotated, Optional import httpx -import requests from fastapi import APIRouter, Depends, Form, HTTPException, Request, status from sqlalchemy.orm import Session diff --git a/tests/test_api_onedrive_coverage.py b/tests/test_api_onedrive_coverage.py index ea235e97..dbcabc16 100644 --- a/tests/test_api_onedrive_coverage.py +++ b/tests/test_api_onedrive_coverage.py @@ -5,7 +5,7 @@ Focuses on uncovered lines: 98-99, 121-143, 160-161, 170-171, 324-326, 400-402, 436-438. """ -from unittest.mock import MagicMock, PropertyMock, patch, AsyncMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest from fastapi.testclient import TestClient diff --git a/tests/test_external_integrations.py b/tests/test_external_integrations.py index 4def1ed5..2d7954b5 100644 --- a/tests/test_external_integrations.py +++ b/tests/test_external_integrations.py @@ -418,6 +418,7 @@ class TestOneDriveIntegration: def test_onedrive_token_refresh_and_user_info(self, original_env: dict) -> None: """Validate token refresh and user info retrieval.""" import asyncio + import httpx token = self._get_access_token(original_env) From d1f64ebfba6bb353ea6f75e1535a42fd26a8fe0a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:28:57 +0000 Subject: [PATCH 3/5] perf(onedrive): use async httpx for token refresh Replaces the synchronous `requests.post` and `requests.get` calls in `app/api/onedrive.py:test_onedrive_token` with an asynchronous `httpx.AsyncClient` implementation. This unblocks the FastAPI event loop when this endpoint is hit. Tests were updated to mock `httpx.AsyncClient` and a sync wrapper using `asyncio.run` was added to integration tests to maintain test coverage without massive test refactoring. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> From 24719212042a65ed4a0d99776c6b3974ef97f0e8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:40:02 +0000 Subject: [PATCH 4/5] perf(onedrive): use async httpx for token refresh Replaces the synchronous `requests.post` and `requests.get` calls in `app/api/onedrive.py:test_onedrive_token` with an asynchronous `httpx.AsyncClient` implementation. This unblocks the FastAPI event loop when this endpoint is hit. Fixed unused requests import in `app/api/onedrive.py` and sorted imports in the testing files updated previously to adhere to the repository formatting (`ruff check --fix`). Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> From 827979598eaeab765a1d24e6011deb26fc804b95 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:53:14 +0000 Subject: [PATCH 5/5] perf(onedrive): use async httpx for token refresh Replaces the synchronous `requests.post` and `requests.get` calls in `app/api/onedrive.py:test_onedrive_token` with an asynchronous `httpx.AsyncClient` implementation. This unblocks the FastAPI event loop when this endpoint is hit. Fixed unused requests import in `app/api/onedrive.py` and sorted imports in the testing files updated previously to adhere to the repository formatting (`ruff check --fix`). Tests in `test_api_onedrive_extended.py` were also migrated to use AsyncMock properly. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- tests/test_api_onedrive_extended.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_api_onedrive_extended.py b/tests/test_api_onedrive_extended.py index 3a4085a5..5a5d230a 100644 --- a/tests/test_api_onedrive_extended.py +++ b/tests/test_api_onedrive_extended.py @@ -1,7 +1,7 @@ """Comprehensive unit tests for app/api/onedrive.py module.""" from datetime import timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -48,8 +48,8 @@ class TestExchangeOneDriveToken: class TestTestOneDriveToken: """Tests for GET /onedrive/test-token endpoint.""" - @patch("app.api.onedrive.requests.post") - @patch("app.api.onedrive.requests.get") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) def test_test_token_success(self, mock_get, mock_post): """Test successful token validation.""" from app.config import settings @@ -79,7 +79,7 @@ class TestTestOneDriveToken: # Should return success pass - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_test_token_not_configured(self, mock_post): """Test when credentials are not configured.""" from app.config import settings @@ -88,7 +88,7 @@ class TestTestOneDriveToken: # Should return error pass - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_test_token_refresh_failed(self, mock_post): """Test when token refresh fails.""" from app.config import settings @@ -104,8 +104,8 @@ class TestTestOneDriveToken: # Should return error with needs_reauth pass - @patch("app.api.onedrive.requests.post") - @patch("app.api.onedrive.requests.get") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) def test_test_token_user_info_failed(self, mock_get, mock_post): """Test when user info request fails.""" from app.config import settings @@ -128,8 +128,8 @@ class TestTestOneDriveToken: # Should return error pass - @patch("app.api.onedrive.requests.post") - @patch("app.api.onedrive.requests.get") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) @patch("builtins.open", create=True) @patch("os.path.exists") def test_test_token_updates_refresh_token(self, mock_exists, mock_open, mock_get, mock_post): @@ -167,8 +167,8 @@ class TestTestOneDriveToken: # Should update refresh token in memory and file pass - @patch("app.api.onedrive.requests.post") - @patch("app.api.onedrive.requests.get") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) + @patch("httpx.AsyncClient.get", new_callable=AsyncMock) def test_test_token_expiration_info(self, mock_get, mock_post): """Test that expiration info is included.""" from app.config import settings @@ -195,7 +195,7 @@ class TestTestOneDriveToken: # token_info should include expiration details pass - @patch("app.api.onedrive.requests.post") + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) def test_test_token_exception_handling(self, mock_post): """Test handling of exceptions.""" from app.config import settings