HR-ATS-Portal/tests/unit/test_pdf.py

105 lines
4.1 KiB
Python

from __future__ import annotations
import pytest
from app.core.errors import EncryptedPDFError, InvalidPDFError, PDFTextUnavailableError
from app.services.pdf import extract_resume, sanitize_filename
from tests.conftest import make_encrypted_pdf, make_pdf, resume_pdf
class TestSanitizeFilename:
@pytest.mark.parametrize(
("raw", "expected"),
[
("resume.pdf", "resume.pdf"),
("../../etc/passwd.pdf", "passwd.pdf"),
# PurePosixPath alone leaves this untouched on POSIX -- the reason
# PureWindowsPath runs first.
(r"..\..\windows\system32\evil.pdf", "evil.pdf"),
(r"C:\Users\me\Desktop\cv.pdf", "cv.pdf"),
("/absolute/path/cv.pdf", "cv.pdf"),
("mixed/sep\\cv.pdf", "cv.pdf"),
('cv<>:"|?*.pdf', "cv_______.pdf"),
("", "resume.pdf"),
(None, "resume.pdf"),
("..", "resume.pdf"),
("...", "resume.pdf"),
],
)
def test_reduces_to_safe_basename(self, raw: str | None, expected: str) -> None:
assert sanitize_filename(raw) == expected
def test_strips_null_bytes(self) -> None:
assert sanitize_filename("cv\x00.pdf") == "cv_.pdf"
def test_caps_length(self) -> None:
assert len(sanitize_filename("a" * 400 + ".pdf")) == 255
class TestExtractResume:
def test_extracts_text_from_a_valid_pdf(self) -> None:
resume = extract_resume(resume_pdf("Ada"), "ada.pdf", 60_000)
assert "Candidate Ada" in resume.text
assert resume.page_count == 1
assert resume.truncated is False
assert resume.candidate_id
def test_joins_pages_in_order_with_separators(self) -> None:
data = make_pdf([["First page content here."], ["Second page content here."]])
resume = extract_resume(data, "multi.pdf", 60_000)
assert resume.page_count == 2
assert "[Page 1]" in resume.text
assert "[Page 2]" in resume.text
assert resume.text.index("[Page 1]") < resume.text.index("[Page 2]")
assert resume.text.index("First page") < resume.text.index("Second page")
def test_rejects_a_pdf_with_no_extractable_text(self) -> None:
"""Stands in for a scanned/image-only resume."""
with pytest.raises(PDFTextUnavailableError):
extract_resume(make_pdf([[]]), "scanned.pdf", 60_000)
def test_rejects_text_below_the_usable_threshold(self) -> None:
with pytest.raises(PDFTextUnavailableError):
extract_resume(make_pdf([["hi"]]), "tiny.pdf", 60_000)
def test_rejects_missing_pdf_signature(self) -> None:
with pytest.raises(InvalidPDFError):
extract_resume(b"this is plain text, not a pdf at all", "fake.pdf", 60_000)
def test_rejects_malformed_pdf(self) -> None:
with pytest.raises(InvalidPDFError):
extract_resume(b"%PDF-1.4\ngarbage garbage garbage", "broken.pdf", 60_000)
def test_rejects_encrypted_pdf(self) -> None:
with pytest.raises(EncryptedPDFError):
extract_resume(make_encrypted_pdf(), "locked.pdf", 60_000)
def test_truncates_at_the_configured_limit_and_records_it(self) -> None:
long_pdf = make_pdf([[f"Line {i} of a very long resume document." for i in range(400)]])
resume = extract_resume(long_pdf, "long.pdf", 500)
assert resume.truncated is True
assert len(resume.text) <= 500
def test_does_not_flag_truncation_when_under_the_limit(self) -> None:
resume = extract_resume(resume_pdf("Ada"), "ada.pdf", 60_000)
assert resume.truncated is False
def test_normalises_whitespace_without_destroying_line_breaks(self) -> None:
data = make_pdf([["Header line for the resume", "Body line for the resume"]])
resume = extract_resume(data, "spaced.pdf", 60_000)
assert "\n" in resume.text
assert " " not in resume.text
assert "\x00" not in resume.text
def test_tolerates_junk_bytes_before_the_signature(self) -> None:
data = b"\n\n" + resume_pdf("Ada")
resume = extract_resume(data, "ada.pdf", 60_000)
assert "Candidate Ada" in resume.text