from __future__ import annotations def test_document_upload_extracts_and_chunks_text_file(client) -> None: response = client.post( "/documents/upload", data={ "title": "Upload smoke test", "subject": "Biology", "chapter": "Photosynthesis", }, files={ "file": ( "photosynthesis.txt", b"Photosynthesis uses light to make glucose and oxygen.", "text/plain", ), }, ) assert response.status_code == 201 payload = response.json() assert payload["status"] == "ready" assert payload["chunk_count"] >= 1 # extracted_text was removed from API response (security hardening) assert "extracted_text" not in payload def test_document_upload_returns_failed_document_when_indexing_fails(client, monkeypatch) -> None: from app.routes import documents def fail_indexing(*_args, **_kwargs) -> None: raise RuntimeError("embedding backend unavailable") monkeypatch.setattr(documents, "replace_document_chunks", fail_indexing) response = client.post( "/documents/upload", data={"title": "Indexing failure"}, files={ "file": ( "notes.txt", b"This text extracts but indexing fails.", "text/plain", ), }, ) assert response.status_code == 201 payload = response.json() assert payload["status"] == "failed" assert payload["chunk_count"] == 0 # extraction_error is stored server-side but must NOT be in the API response assert "extraction_error" not in payload def test_document_upload_strips_lone_surrogates_before_database_write(client, monkeypatch) -> None: from app.routes import documents monkeypatch.setattr( documents, "extract_text_from_file", lambda *_args, **_kwargs: "Clean biology text \ud835 with invalid surrogate.", ) response = client.post( "/documents/upload", data={"title": "Unicode repair", "subject": "Biology"}, files={ "file": ( "unicode.txt", b"placeholder", "text/plain", ), }, ) assert response.status_code == 201 payload = response.json() assert payload["status"] == "ready" # extracted_text was removed from API response (security hardening); # verify it is NOT leaked to the client and the upload still succeeds assert "extracted_text" not in payload def test_upload_rejects_unsupported_mime_type(client) -> None: response = client.post( "/documents/upload", data={"title": "Bad type"}, files={"file": ("notes.exe", b"MZ\x90\x00", "application/x-msdownload")}, ) assert response.status_code == 415 def test_upload_rejects_images_before_creating_a_failed_document(client) -> None: response = client.post( "/documents/upload", data={"title": "Scanned notes"}, files={"file": ("notes.png", b"\x89PNG\r\n\x1a\n", "image/png")}, ) assert response.status_code == 415 detail = response.json()["detail"] assert "Image OCR" in detail assert client.get("/documents").json() == [] def test_upload_rejects_file_over_the_size_limit(client) -> None: # 20 MB + 1 byte of allowed-type content must be rejected as 413, not stored. oversized = b"a" * (20 * 1024 * 1024 + 1) response = client.post( "/documents/upload", data={"title": "Too big"}, files={"file": ("huge.txt", oversized, "text/plain")}, ) assert response.status_code == 413 def test_upload_accepts_a_malayalam_filename_and_sanitises_it(client) -> None: response = client.post( "/documents/upload", data={"title": "Malayalam name"}, files={ "file": ( "ശബ്ദം physics notes.txt", b"Sound travels as a longitudinal wave.", "text/plain", ), }, ) assert response.status_code == 201 assert response.json()["status"] == "ready" def test_upload_requires_authentication(auth_client) -> None: # No Authorization header -> the upload endpoint must refuse. response = auth_client.post( "/documents/upload", data={"title": "Anon"}, files={"file": ("x.txt", b"hello", "text/plain")}, ) assert response.status_code in (401, 403)