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:
curo1305
2026-05-30 17:55:08 +02:00
parent f4f340545b
commit 10175ee4b5
8 changed files with 479 additions and 1 deletions
+65 -1
View File
@@ -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.
+37
View File
@@ -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"]