Spaces:
Sleeping
Sleeping
| """Integration: upload → worker OCR → extracted_text populated.""" | |
| import io | |
| import time | |
| import uuid | |
| import httpx | |
| import pytest | |
| from tests.integration.conftest import STRONG_PASSWORD, mark_user_verified | |
| pytestmark = pytest.mark.integration | |
| BASE_URL = "http://127.0.0.1:8000/api/v1" | |
| def unique_email() -> str: | |
| return f"ocr-{uuid.uuid4().hex[:12]}@example.com" | |
| def http(): | |
| with httpx.Client(base_url=BASE_URL, timeout=30.0) as client: | |
| yield client | |
| def auth(http): | |
| email = unique_email() | |
| http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| mark_user_verified(email) | |
| tok = http.post( | |
| "/auth/login", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ).json()["access_token"] | |
| return {"Authorization": f"Bearer {tok}"} | |
| def wait_for_status( | |
| http: httpx.Client, headers: dict, doc_id: str, *, timeout: float = 60.0 | |
| ) -> str: | |
| deadline = time.time() + timeout | |
| last = None | |
| while time.time() < deadline: | |
| last = http.get(f"/documents/{doc_id}", headers=headers).json()["status"] | |
| if last in ("processed", "failed"): | |
| return last | |
| time.sleep(0.5) | |
| return last or "timeout" | |
| def test_text_plain_round_trip(http, auth): | |
| body = b"hello from the OCR pipeline\nsecond line" | |
| upload = http.post( | |
| "/documents", | |
| headers=auth, | |
| files={"file": ("note.txt", io.BytesIO(body), "text/plain")}, | |
| ).json() | |
| status = wait_for_status(http, auth, upload["id"]) | |
| assert status == "processed" | |
| text = http.get(f"/documents/{upload['id']}/text", headers=auth).json() | |
| assert text["status"] == "processed" | |
| assert text["extracted_text"] is not None | |
| assert "hello from the OCR pipeline" in text["extracted_text"] | |
| assert "second line" in text["extracted_text"] | |
| def test_markdown_round_trip(http, auth): | |
| body = b"# Title\n\nSome body text" | |
| upload = http.post( | |
| "/documents", | |
| headers=auth, | |
| files={"file": ("doc.md", io.BytesIO(body), "text/markdown")}, | |
| ).json() | |
| status = wait_for_status(http, auth, upload["id"]) | |
| assert status == "processed" | |
| assert ( | |
| "Title" | |
| in http.get(f"/documents/{upload['id']}/text", headers=auth).json()["extracted_text"] | |
| ) | |
| def test_text_endpoint_404_for_other_users_doc(http, auth): | |
| body = b"private notes" | |
| upload = http.post( | |
| "/documents", | |
| headers=auth, | |
| files={"file": ("p.txt", io.BytesIO(body), "text/plain")}, | |
| ).json() | |
| wait_for_status(http, auth, upload["id"]) | |
| # second user | |
| other_email = unique_email() | |
| httpx.post( | |
| f"{BASE_URL}/auth/register", | |
| json={ | |
| "email": other_email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| mark_user_verified(other_email) | |
| other_tok = httpx.post( | |
| f"{BASE_URL}/auth/login", | |
| json={ | |
| "email": other_email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ).json()["access_token"] | |
| r = http.get( | |
| f"/documents/{upload['id']}/text", | |
| headers={"Authorization": f"Bearer {other_tok}"}, | |
| ) | |
| assert r.status_code == 404 | |
| def test_audio_upload_with_invalid_bytes_marks_failed(http, auth): | |
| # Invalid audio bytes should cause Groq Whisper to reject the file and | |
| # the worker to mark the doc FAILED. Happy path lives in test_audio_flow. | |
| body = b"fake mp3 bytes" | |
| upload = http.post( | |
| "/documents", | |
| headers=auth, | |
| files={"file": ("clip.mp3", io.BytesIO(body), "audio/mpeg")}, | |
| ).json() | |
| status = wait_for_status(http, auth, upload["id"]) | |
| assert status == "failed" | |
| # error_message is on the main doc response (not /text) so the UI can | |
| # surface a reason without pulling the full extracted-text payload. | |
| doc = http.get(f"/documents/{upload['id']}", headers=auth).json() | |
| assert doc["error_message"] is not None | |
| assert doc["error_message"] != "" | |