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] 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: