fix(05-12): close 3 UAT gaps — OAuth 400 preflight, 502 cloud fallback, upload hint
- oauth_initiate: pre-flight check returns 400 with env-var hint when GOOGLE_CLIENT_ID/SECRET or ONEDRIVE_CLIENT_ID/SECRET are not configured, preventing opaque MSAL/OAuth library 500 errors on misconfigured servers - stream_document_content: broad except-clause catches non-CloudConnectionError exceptions and returns 502 with user-friendly message (was raw 500) - docker-compose.yml: add volumes: - ./backend:/app to celery-worker so code changes are picked up by docker compose restart without a rebuild - CloudStorageView: upload hint paragraph directs users to navigate into a cloud folder; no DropZone added (no folder context at overview level) - 3 new backend tests pass; 2 existing tests patched with credential monkeypatch; full suite: 293 passed, 0 new failures, 1 pre-existing (test_extract_docx) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -345,6 +345,17 @@ async def oauth_initiate(
|
||||
|
||||
redirect_uri = f"{settings.backend_url}/api/cloud/oauth/callback/{provider}"
|
||||
|
||||
if provider == "google_drive" and (not settings.google_client_id or not settings.google_client_secret):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Google Drive OAuth is not configured on this server. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in your environment.",
|
||||
)
|
||||
if provider == "onedrive" and (not settings.onedrive_client_id or not settings.onedrive_client_secret):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="OneDrive OAuth is not configured on this server. Set ONEDRIVE_CLIENT_ID, ONEDRIVE_CLIENT_SECRET, and ONEDRIVE_TENANT_ID in your environment.",
|
||||
)
|
||||
|
||||
if provider == "google_drive":
|
||||
from google_auth_oauthlib.flow import Flow # lazy import
|
||||
|
||||
|
||||
@@ -756,6 +756,11 @@ async def stream_document_content(
|
||||
status_code=503,
|
||||
detail="Cloud connection requires re-authentication. Please reconnect in Settings.",
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Cloud backend unreachable. Please try again or reconnect in Settings.",
|
||||
) from exc
|
||||
file_size = len(file_bytes)
|
||||
|
||||
headers = {
|
||||
|
||||
@@ -184,9 +184,14 @@ async def test_connect_google_drive(async_client, db_session, monkeypatch):
|
||||
so the frontend can inject the Bearer Authorization header before navigating.
|
||||
"""
|
||||
from main import app
|
||||
from config import settings
|
||||
|
||||
auth = await _create_user_and_token(db_session, role="user")
|
||||
|
||||
# Ensure pre-flight config check passes (plan 05-12)
|
||||
monkeypatch.setattr(settings, "google_client_id", "test_google_client_id")
|
||||
monkeypatch.setattr(settings, "google_client_secret", "test_google_client_secret")
|
||||
|
||||
# Mock Redis to avoid needing a real Redis connection
|
||||
fake_redis = FakeRedis()
|
||||
app.state.redis = fake_redis
|
||||
@@ -728,7 +733,7 @@ async def test_reanalyze_cloud_document_routes_to_cloud_backend():
|
||||
# ── Plan 10 tests: OAuth initiate returns JSON URL ────────────────────────────
|
||||
|
||||
|
||||
async def test_oauth_initiate_returns_json_url(async_client, db_session):
|
||||
async def test_oauth_initiate_returns_json_url(async_client, db_session, monkeypatch):
|
||||
"""GET /api/cloud/oauth/initiate/google_drive returns 200 JSON {url} (not 302).
|
||||
|
||||
Verifies the fix for CLOUD-01 / T-05-10-01: authenticated users receive
|
||||
@@ -736,9 +741,14 @@ async def test_oauth_initiate_returns_json_url(async_client, db_session):
|
||||
header before navigating (plan 05-10).
|
||||
"""
|
||||
from main import app
|
||||
from config import settings
|
||||
|
||||
auth = await _create_user_and_token(db_session, role="user")
|
||||
|
||||
# Ensure pre-flight config check passes (plan 05-12)
|
||||
monkeypatch.setattr(settings, "google_client_id", "test_google_client_id")
|
||||
monkeypatch.setattr(settings, "google_client_secret", "test_google_client_secret")
|
||||
|
||||
# Set up fake Redis so state token storage works
|
||||
fake_redis = FakeRedis()
|
||||
app.state.redis = fake_redis
|
||||
@@ -771,6 +781,60 @@ async def test_oauth_initiate_returns_json_url(async_client, db_session):
|
||||
app.state.redis = None
|
||||
|
||||
|
||||
async def test_oauth_initiate_google_drive_not_configured(async_client, db_session, monkeypatch):
|
||||
"""GET /api/cloud/oauth/initiate/google_drive returns 400 with env-var hint when creds missing.
|
||||
|
||||
Pre-flight check (plan 05-12): empty GOOGLE_CLIENT_ID/SECRET → 400 before touching OAuth libs.
|
||||
"""
|
||||
from main import app
|
||||
from config import settings
|
||||
|
||||
auth = await _create_user_and_token(db_session, role="user")
|
||||
fake_redis = FakeRedis()
|
||||
app.state.redis = fake_redis
|
||||
|
||||
monkeypatch.setattr(settings, "google_client_id", "")
|
||||
monkeypatch.setattr(settings, "google_client_secret", "")
|
||||
|
||||
resp = await async_client.get(
|
||||
"/api/cloud/oauth/initiate/google_drive",
|
||||
headers=auth["headers"],
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
app.state.redis = None
|
||||
|
||||
assert resp.status_code == 400, f"Expected 400, got {resp.status_code}: {resp.text}"
|
||||
assert "GOOGLE_CLIENT_ID" in resp.json()["detail"], f"Unexpected detail: {resp.json()['detail']}"
|
||||
|
||||
|
||||
async def test_oauth_initiate_onedrive_not_configured(async_client, db_session, monkeypatch):
|
||||
"""GET /api/cloud/oauth/initiate/onedrive returns 400 with env-var hint when creds missing.
|
||||
|
||||
Pre-flight check (plan 05-12): empty ONEDRIVE_CLIENT_ID → 400 before touching MSAL.
|
||||
"""
|
||||
from main import app
|
||||
from config import settings
|
||||
|
||||
auth = await _create_user_and_token(db_session, role="user")
|
||||
fake_redis = FakeRedis()
|
||||
app.state.redis = fake_redis
|
||||
|
||||
monkeypatch.setattr(settings, "onedrive_client_id", "")
|
||||
monkeypatch.setattr(settings, "onedrive_client_secret", "")
|
||||
|
||||
resp = await async_client.get(
|
||||
"/api/cloud/oauth/initiate/onedrive",
|
||||
headers=auth["headers"],
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
app.state.redis = None
|
||||
|
||||
assert resp.status_code == 400, f"Expected 400, got {resp.status_code}: {resp.text}"
|
||||
assert "ONEDRIVE_CLIENT_ID" in resp.json()["detail"], f"Unexpected detail: {resp.json()['detail']}"
|
||||
|
||||
|
||||
async def test_oauth_initiate_requires_auth(async_client, db_session):
|
||||
"""GET /api/cloud/oauth/initiate/google_drive without token returns 401 or 403.
|
||||
|
||||
|
||||
@@ -593,3 +593,40 @@ async def test_parse_range_416(async_client, auth_user, db_session, monkeypatch)
|
||||
headers={**auth_user["headers"], "Range": "bytes=100-200"},
|
||||
)
|
||||
assert resp.status_code == 416
|
||||
|
||||
|
||||
async def test_stream_document_content_cloud_backend_error(async_client, auth_user, db_session, monkeypatch):
|
||||
"""GET /api/documents/{id}/content returns 502 when cloud backend raises a non-CloudConnectionError exception.
|
||||
|
||||
Plan 05-12 gap closure: broad except-clause catches RuntimeError, timeout, etc. and
|
||||
returns a user-friendly 502 instead of an opaque 500.
|
||||
"""
|
||||
import uuid as _uuid
|
||||
from unittest.mock import AsyncMock
|
||||
from db.models import Document
|
||||
|
||||
doc_id = _uuid.uuid4()
|
||||
doc = Document(
|
||||
id=doc_id,
|
||||
user_id=auth_user["user"].id,
|
||||
filename="cloud_doc.pdf",
|
||||
content_type="application/pdf",
|
||||
size_bytes=1024,
|
||||
storage_backend="google_drive",
|
||||
status="uploaded",
|
||||
object_key=f"{auth_user['user'].id}/{doc_id}/{_uuid.uuid4()}.pdf",
|
||||
)
|
||||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
async def raise_runtime_error(*args, **kwargs):
|
||||
raise RuntimeError("connection timeout")
|
||||
|
||||
monkeypatch.setattr("api.documents.get_storage_backend_for_document", raise_runtime_error)
|
||||
|
||||
resp = await async_client.get(
|
||||
f"/api/documents/{doc_id}/content",
|
||||
headers=auth_user["headers"],
|
||||
)
|
||||
assert resp.status_code == 502, f"Expected 502, got {resp.status_code}: {resp.text}"
|
||||
assert "Cloud backend unreachable" in resp.json()["detail"]
|
||||
|
||||
Reference in New Issue
Block a user