Spaces:
Sleeping
Sleeping
File size: 6,104 Bytes
7f105c8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | """파서 단위 테스트.
LibreOffice 호출은 mocking. PDF 추출은 pdfplumber에 의존하므로
실 PDF 한 장은 monkeypatch 없이 검증해도 됨 (가벼움).
"""
from pathlib import Path
import pytest
from app.services.parser import (
ParserError,
normalize,
parse_bytes_to_text,
)
# ── normalize ─────────────────────────────────────────────────
def test_normalize_strips_null_bytes():
assert "\x00" not in normalize("a\x00b\x00c")
def test_normalize_collapses_inline_whitespace_but_keeps_newlines():
out = normalize("foo bar\n\n\nbaz")
# 한 줄 안 다중 공백은 1개로
assert out == "foo bar\n\nbaz"
def test_normalize_preserves_paragraph_breaks():
"""줄바꿈은 보존 (윤정님 요구: '줄바꿈만 살리고')."""
src = "안녕하세요\n학부모님께\n\n알려드립니다"
assert normalize(src) == "안녕하세요\n학부모님께\n\n알려드립니다"
def test_normalize_empty_returns_empty():
assert normalize("") == ""
assert normalize(" \n\n ") == ""
# ── parse_bytes_to_text — 평문 ────────────────────────────────
def test_parse_text_passthrough():
raw = "통신문 본문\n\n오늘 안내드립니다".encode("utf-8")
assert parse_bytes_to_text(raw, "notice.txt") == "통신문 본문\n\n오늘 안내드립니다"
def test_parse_no_extension_treated_as_text():
raw = "그냥 텍스트".encode("utf-8")
assert parse_bytes_to_text(raw, "noext") == "그냥 텍스트"
def test_parse_empty_bytes_returns_empty():
assert parse_bytes_to_text(b"", "anything.txt") == ""
def test_parse_unsupported_extension_raises():
with pytest.raises(ParserError):
parse_bytes_to_text(b"x", "image.heic")
def test_parse_unknown_extension_rejected():
"""알 수 없는 suffix는 화이트리스트(ALLOWED_EXTS)에서 거부."""
with pytest.raises(ParserError):
parse_bytes_to_text(b"x", "../../etc/passwd.exe")
with pytest.raises(ParserError):
parse_bytes_to_text(b"x", "weird.bin")
def test_parse_filename_metachars_safe(monkeypatch):
"""쉘 메타문자가 들어간 .hwp 파일명도 LibreOffice 호출에 안전.
원본 filename은 어떤 경로/명령에도 들어가지 않고 tempdir/input.hwp로만 저장.
subprocess 호출 시 list 인자 형태라 명령 주입 표면 자체가 없음.
"""
captured_args = {}
def fake_run(cmd, **kwargs):
captured_args["cmd"] = cmd
# 메타문자 흔적이 cmd 어디에도 안 보임을 검증
for arg in cmd:
assert "rm" not in arg
assert ";" not in arg
assert "$(" not in arg
# 가짜 ODT 만들어서 변환 성공처럼
out_dir = Path(cmd[cmd.index("--outdir") + 1])
(out_dir / "input.odt").write_bytes(b"PK-fake-odt")
class Result:
returncode = 0
stderr = ""
return Result()
monkeypatch.setattr("app.services.parser.subprocess.run", fake_run)
monkeypatch.setattr(
"app.services.parser._odt_to_text",
lambda p: "ok",
)
parse_bytes_to_text(b"HWP-data", "$(rm -rf /).hwp")
# 명령 주입은커녕 원본 filename이 cmd에 등장조차 안 함
assert all("$(rm" not in arg for arg in captured_args["cmd"])
def test_parse_libreoffice_timeout_raises_with_message(monkeypatch):
"""타임아웃 발생 시 ParserError + 환경변수 안내 메시지."""
import subprocess as sp
def boom(*args, **kwargs):
raise sp.TimeoutExpired(cmd=args[0], timeout=kwargs.get("timeout", 0))
monkeypatch.setattr("app.services.parser.subprocess.run", boom)
with pytest.raises(ParserError) as exc:
parse_bytes_to_text(b"HWP", "x.hwp")
assert "타임아웃" in str(exc.value)
assert "PARSER_LIBREOFFICE_TIMEOUT" in str(exc.value)
# ── parse_bytes_to_text — HWP (LibreOffice 모킹) ──────────────
def test_parse_hwp_calls_libreoffice_and_odt_extractor(monkeypatch, tmp_path):
"""HWP 입력 → LibreOffice ODT 변환 호출 + content.xml 추출 호출 확인."""
called = {}
def fake_hwp_to_odt(hwp_path: Path, out_dir: Path) -> Path:
called["hwp_path"] = hwp_path
called["out_dir"] = out_dir
fake_odt = out_dir / "fake.odt"
fake_odt.write_bytes(b"PK-fake-odt")
return fake_odt
def fake_odt_to_text(odt_path: Path) -> str:
called["odt_path"] = odt_path
return "본문 추출 결과"
monkeypatch.setattr("app.services.parser._hwp_to_odt", fake_hwp_to_odt)
monkeypatch.setattr("app.services.parser._odt_to_text", fake_odt_to_text)
out = parse_bytes_to_text(b"HWP-bytes", "안내.hwp")
assert out == "본문 추출 결과"
assert called["hwp_path"].suffix == ".hwp"
assert called["odt_path"].suffix == ".odt"
def test_parse_pdf_calls_pdfplumber_only(monkeypatch):
"""PDF 입력 → LibreOffice 우회, pdfplumber만 호출."""
called = {"hwp": False, "pdf": False}
def fake_hwp_to_odt(*args, **kwargs):
called["hwp"] = True
raise AssertionError("PDF 입력에선 LibreOffice가 호출되면 안 됨")
def fake_pdf_to_text(pdf_path: Path) -> str:
called["pdf"] = True
return "PDF 추출 결과"
monkeypatch.setattr("app.services.parser._hwp_to_odt", fake_hwp_to_odt)
monkeypatch.setattr("app.services.parser._pdf_to_text", fake_pdf_to_text)
out = parse_bytes_to_text(b"%PDF-1.4 fake", "doc.pdf")
assert out == "PDF 추출 결과"
assert called["pdf"] is True
assert called["hwp"] is False
def test_parse_libreoffice_failure_raises_parser_error(monkeypatch):
def boom(*args, **kwargs):
raise ParserError("LibreOffice 변환 실패")
monkeypatch.setattr("app.services.parser._hwp_to_odt", boom)
with pytest.raises(ParserError):
parse_bytes_to_text(b"HWP", "x.hwp")
|