78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
"""Integration tests for the resume upload endpoint guards.
|
|
|
|
POST /api/v1/resumes/upload validates: file type → size → empty file →
|
|
extractable text. These guards are deterministic (no LLM needed) and exercise
|
|
the real resumes router (previously ~18% covered). The empty-extracted-text
|
|
guard pins PR #794 (reject image-based / scanned PDFs).
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.main import app
|
|
from app.routers.resumes import MAX_FILE_SIZE
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
transport = ASGITransport(app=app)
|
|
return AsyncClient(transport=transport, base_url="http://test")
|
|
|
|
|
|
class TestUploadGuards:
|
|
async def test_rejects_unsupported_file_type(self, client):
|
|
async with client:
|
|
resp = await client.post(
|
|
"/api/v1/resumes/upload",
|
|
files={"file": ("resume.txt", b"hello", "text/plain")},
|
|
)
|
|
assert resp.status_code == 400
|
|
assert "Invalid file type" in resp.json()["detail"]
|
|
|
|
async def test_rejects_empty_file(self, client):
|
|
async with client:
|
|
resp = await client.post(
|
|
"/api/v1/resumes/upload",
|
|
files={"file": ("resume.pdf", b"", "application/pdf")},
|
|
)
|
|
assert resp.status_code == 400
|
|
assert resp.json()["detail"] == "Empty file"
|
|
|
|
async def test_rejects_oversized_file(self, client):
|
|
oversized = b"x" * (MAX_FILE_SIZE + 1)
|
|
async with client:
|
|
resp = await client.post(
|
|
"/api/v1/resumes/upload",
|
|
files={"file": ("resume.pdf", oversized, "application/pdf")},
|
|
)
|
|
assert resp.status_code == 413
|
|
|
|
@patch("app.routers.resumes.parse_document", new_callable=AsyncMock)
|
|
async def test_rejects_empty_extracted_text(self, mock_parse, client):
|
|
"""#794: a valid-but-image-based PDF parses to empty text → 422,
|
|
and we must NOT persist anything to the database."""
|
|
mock_parse.return_value = " \n " # whitespace only → "no extractable text"
|
|
with patch("app.routers.resumes.db") as mock_db:
|
|
async with client:
|
|
resp = await client.post(
|
|
"/api/v1/resumes/upload",
|
|
files={"file": ("scanned.pdf", b"%PDF-1.4 image-only", "application/pdf")},
|
|
)
|
|
assert resp.status_code == 422
|
|
assert "extract text" in resp.json()["detail"].lower()
|
|
mock_db.create_resume_atomic_master.assert_not_called()
|
|
|
|
@patch("app.routers.resumes.parse_document", new_callable=AsyncMock)
|
|
async def test_maps_parse_failure_to_422(self, mock_parse, client):
|
|
"""A parser exception is surfaced as a generic 422, not a 500."""
|
|
mock_parse.side_effect = RuntimeError("corrupt file")
|
|
async with client:
|
|
resp = await client.post(
|
|
"/api/v1/resumes/upload",
|
|
files={"file": ("broken.pdf", b"%PDF-1.4 broken", "application/pdf")},
|
|
)
|
|
assert resp.status_code == 422
|
|
assert "Failed to parse" in resp.json()["detail"]
|