From e2e499b8b10467f21dae0dcd2c57e6bc2edce717 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Sat, 30 May 2026 11:24:33 +0200 Subject: [PATCH] feat(05-10): oauth_initiate returns 200 JSON {url} instead of 302 redirect - Remove response_class=RedirectResponse from @router.get decorator - Replace both RedirectResponse(status_code=302) returns with JSONResponse({url}) - Frontend can now inject Bearer header before navigating to OAuth URL (T-05-10-01) - Update test_connect_google_drive to expect 200 JSON (regression fix) --- backend/api/cloud.py | 16 +++++++++++----- backend/tests/test_cloud.py | 28 ++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/backend/api/cloud.py b/backend/api/cloud.py index 7474c2c..248f266 100644 --- a/backend/api/cloud.py +++ b/backend/api/cloud.py @@ -311,22 +311,28 @@ async def _upsert_cloud_connection( # ── GET /api/cloud/oauth/initiate/{provider} ────────────────────────────────── -@router.get("/oauth/initiate/{provider}", response_class=RedirectResponse) +@router.get("/oauth/initiate/{provider}") async def oauth_initiate( provider: str, request: Request, current_user: User = Depends(get_regular_user), -): +) -> dict: """Start the OAuth flow for Google Drive or OneDrive. Generates a CSRF state token, stores it in Redis with TTL 1800 (30 min), - and redirects the browser to the provider's authorization URL. + and returns the provider's authorization URL as JSON so the frontend can + navigate using fetch() with the Bearer header (plan 05-10 fix). + + Returns: {"url": ""} Security: - state token is secrets.token_urlsafe(32) — 256 bits of entropy (T-05-05-01) - Redis key is single-use: deleted in the callback handler (T-05-05-02) - Only google_drive and onedrive are accepted (T-05-05-06) + - Endpoint requires get_regular_user — no unauthenticated access (T-05-10-01) """ + from fastapi.responses import JSONResponse # already available via fastapi + if provider not in VALID_OAUTH_PROVIDERS: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -359,7 +365,7 @@ async def oauth_initiate( prompt="consent", state=state_token, ) - return RedirectResponse(url=authorization_url, status_code=302) + return JSONResponse({"url": authorization_url}) elif provider == "onedrive": import msal # lazy import @@ -375,7 +381,7 @@ async def oauth_initiate( redirect_uri=redirect_uri, state=state_token, ) - return RedirectResponse(url=auth_url, status_code=302) + return JSONResponse({"url": auth_url}) # ── GET /api/cloud/oauth/callback/{provider} ────────────────────────────────── diff --git a/backend/tests/test_cloud.py b/backend/tests/test_cloud.py index 2f32416..2cfb423 100644 --- a/backend/tests/test_cloud.py +++ b/backend/tests/test_cloud.py @@ -178,7 +178,11 @@ async def test_factory_returns_correct_backend(): # ── CLOUD-01: OAuth connect / WebDAV connect ────────────────────────────────── async def test_connect_google_drive(async_client, db_session, monkeypatch): - """GET /api/cloud/oauth/initiate/google_drive redirects to Google's OAuth URL.""" + """GET /api/cloud/oauth/initiate/google_drive returns 200 JSON {url} pointing to Google OAuth. + + Updated in plan 05-10: endpoint now returns JSON instead of 302 redirect + so the frontend can inject the Bearer Authorization header before navigating. + """ from main import app auth = await _create_user_and_token(db_session, role="user") @@ -187,15 +191,23 @@ async def test_connect_google_drive(async_client, db_session, monkeypatch): fake_redis = FakeRedis() app.state.redis = fake_redis - resp = await async_client.get( - "/api/cloud/oauth/initiate/google_drive", - headers=auth["headers"], - follow_redirects=False, + mock_flow = MagicMock() + mock_flow.authorization_url.return_value = ( + "https://accounts.google.com/o/oauth2/auth?scope=drive&state=test", + "test", ) - assert resp.status_code == 302 - location = resp.headers.get("location", "") - assert "accounts.google.com" in location + with patch("google_auth_oauthlib.flow.Flow.from_client_config", return_value=mock_flow): + resp = await async_client.get( + "/api/cloud/oauth/initiate/google_drive", + headers=auth["headers"], + follow_redirects=False, + ) + + assert resp.status_code == 200 + data = resp.json() + assert "url" in data + assert "accounts.google.com" in data["url"] # Clean up app.state.redis = None