diff --git a/backend/api/admin.py b/backend/api/admin.py index 4b538b2..a1aea53 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -146,6 +146,9 @@ class CloudConnectionOut(BaseModel): Any admin or user endpoint returning CloudConnection ORM objects MUST use this model to prevent accidental exposure of encrypted credentials. Safe-by-default: whitelist of allowed fields (not blacklist). + + Note: id is declared as str and coerced via validator so UUID ORM values + serialize correctly without json_encoders (Rule 1 fix — T-05-06 test suite). """ id: str @@ -155,6 +158,12 @@ class CloudConnectionOut(BaseModel): connected_at: datetime model_config = {"from_attributes": True} + @field_validator("id", mode="before") + @classmethod + def coerce_id_to_str(cls, v) -> str: + """Coerce UUID objects to str so the model validates from ORM instances.""" + return str(v) + # ── Endpoints ───────────────────────────────────────────────────────────────── diff --git a/backend/tests/test_cloud.py b/backend/tests/test_cloud.py index 8a10373..1e765be 100644 --- a/backend/tests/test_cloud.py +++ b/backend/tests/test_cloud.py @@ -204,7 +204,6 @@ async def test_connect_google_drive(async_client, db_session, monkeypatch): async def test_oauth_callback_valid_state(async_client, db_session, monkeypatch): """GET /api/cloud/oauth/callback/google_drive with valid state stores credentials and redirects.""" from main import app - from services.auth import hash_password # Create a user in DB (callback looks up user from Redis-stored user_id) auth = await _create_user_and_token(db_session, role="user") @@ -215,7 +214,7 @@ async def test_oauth_callback_valid_state(async_client, db_session, monkeypatch) fake_redis = FakeRedis(initial={f"oauth_state:{state_token}": user_id.encode()}) app.state.redis = fake_redis - # Mock Flow.fetch_token to avoid real OAuth network call + # Mock Flow credentials — the callback does asyncio.to_thread(flow.fetch_token, code=code) mock_creds = MagicMock() mock_creds.token = "ya29.test_access_token" mock_creds.refresh_token = "1//test_refresh_token" @@ -224,15 +223,14 @@ async def test_oauth_callback_valid_state(async_client, db_session, monkeypatch) mock_creds.client_secret = "test_client_secret" mock_creds.expiry = None - def fake_fetch_token(code): - pass # no-op — credentials are set below - mock_flow = MagicMock() mock_flow.credentials = mock_creds - mock_flow.authorization_url.return_value = ("https://accounts.google.com/auth", "state") - mock_flow.fetch_token = fake_fetch_token + mock_flow.fetch_token = MagicMock(return_value=None) # sync — called via to_thread - with patch("api.cloud.Flow") as mock_flow_class: + # Flow is imported lazily inside oauth_callback with: + # from google_auth_oauthlib.flow import Flow + # We patch the module-level name so the lazy import picks up our mock. + with patch("google_auth_oauthlib.flow.Flow") as mock_flow_class: mock_flow_class.from_client_config.return_value = mock_flow resp = await async_client.get( @@ -360,10 +358,15 @@ async def test_cloud_upload_no_presigned( credentials_enc=credentials_enc, ) - # Mock GoogleDriveBackend.put_object to avoid real Google Drive call + # Mock GoogleDriveBackend.put_object to avoid real Google Drive call. + # GoogleDriveBackend is imported lazily inside the endpoint function body, so we + # patch at the source module (storage.google_drive_backend) rather than api.documents. + # Also mock extract_and_classify.delay to avoid Celery/Redis connection in unit tests. mock_put = AsyncMock(return_value="drive_file_id_123") + mock_delay = MagicMock() + monkeypatch.setattr("api.documents.extract_and_classify.delay", mock_delay) - with patch("api.documents.GoogleDriveBackend") as mock_gd_class: + with patch("storage.google_drive_backend.GoogleDriveBackend") as mock_gd_class: mock_instance = MagicMock() mock_instance.put_object = mock_put mock_gd_class.return_value = mock_instance