diff --git a/tests/inbox_frontend.py b/tests/inbox_frontend.py index b2055ab..a12f662 100644 --- a/tests/inbox_frontend.py +++ b/tests/inbox_frontend.py @@ -124,6 +124,32 @@ def _ingest_csv(rel_name, text, bundle, provenance, rejected): _materialize_text(rel_name, text, "csv", bundle, provenance) +def _extract_docx_text(fs_path) -> str: + """Extract text from a ``.docx``, including the regions a human reviewing the + document in Word does not see: hidden/vanish runs (still runs, so ``paragraph + .text`` includes them), core metadata properties, and review comments. + + ``python-docx`` is imported lazily — it is a dev/showcase-scoped parser, not a + core dependency; a consumer would guard the import behind their own extra. + """ + from docx import Document + + doc = Document(str(fs_path)) + parts = [para.text for para in doc.paragraphs if para.text] + + cp = doc.core_properties + for attr in ("title", "subject", "keywords", "comments", "category", "author"): + value = getattr(cp, attr, None) + if value: + parts.append(str(value)) + + for comment in doc.comments: + if comment.text: + parts.append(comment.text) + + return "\n".join(parts) + + def _ingest_regular_file(fs_path, rel_name, bundle, provenance, rejected, *, strict): """Dispatch one on-disk file by suffix. ``strict`` raises on an unsupported suffix (a top-level drop); a folder walk passes ``strict=False`` to skip it.""" @@ -131,6 +157,8 @@ def _ingest_regular_file(fs_path, rel_name, bundle, provenance, rejected, *, str if suffix == ".csv": text = Path(fs_path).read_text(encoding="utf-8", errors="replace") _ingest_csv(rel_name, text, bundle, provenance, rejected) + elif suffix == ".docx": + _materialize_text(rel_name, _extract_docx_text(fs_path), "docx", bundle, provenance) elif suffix in _TEXT_SUFFIXES: text = Path(fs_path).read_text(encoding="utf-8", errors="replace") _materialize_text(rel_name, text, suffix.lstrip("."), bundle, provenance) diff --git a/tests/test_okf_inbox_uploads.py b/tests/test_okf_inbox_uploads.py index 719b433..09031f8 100644 --- a/tests/test_okf_inbox_uploads.py +++ b/tests/test_okf_inbox_uploads.py @@ -224,3 +224,63 @@ def test_clean_folder_admits(tmp_path): extracted, _result, verdict = receive([folder]) assert set(extracted.bundle) == {"uploads/tables/users.md", "uploads/notes.md"} assert verdict == "ADMIT" + + +# --- slice 2d: .docx (python-docx) ------------------------------------------ +# The payload hides where a human reviewing the doc in Word does not look: a +# hidden (vanish) run, a review comment, or a core metadata property. The +# extractor must surface all three regions so the stage-2 scan catches them. + + +def _make_docx(tmp_path, name="doc.docx", *, body="A normal paragraph.", + hidden=None, comment=None, subject=None): + from docx import Document + + d = Document() + p = d.add_paragraph() + p.add_run(body) + if hidden is not None: + run = p.add_run(hidden) + run.font.hidden = True # w:vanish — invisible in Word + if subject is not None: + d.core_properties.subject = subject # core metadata property + if comment is not None: + d.add_comment(runs=p.runs, text=comment, author="m", initials="m") + fp = tmp_path / name + d.save(str(fp)) + return fp + + +def test_docx_hidden_run_injection_is_caught(tmp_path): + fp = _make_docx(tmp_path, hidden=_INJECTION) + extracted, result, verdict = receive([fp]) + assert extracted.provenance[0].source_type == "docx" + assert result.disposition is Disposition.FAIL_SECURE + assert verdict == "REJECT" + + +def test_docx_comment_injection_is_caught(tmp_path): + fp = _make_docx(tmp_path, comment=_INJECTION) + _extracted, _result, verdict = receive([fp]) + assert verdict == "REJECT" + + +def test_docx_core_metadata_injection_is_caught(tmp_path): + fp = _make_docx(tmp_path, subject=_INJECTION) + _extracted, _result, verdict = receive([fp]) + assert verdict == "REJECT" + + +def test_clean_docx_admits(tmp_path): + fp = _make_docx(tmp_path, name="clean.docx", body="A routine paragraph. No behavior change.") + extracted, _result, verdict = receive([fp]) + assert "uploads/clean.md" in extracted.bundle + assert verdict == "ADMIT" + + +def test_docx_hidden_run_detach_proof(tmp_path): + # The same visible body WITHOUT the hidden run ADMITs, proving it is the + # extractor surfacing the hidden region that caught it (not just "a docx"). + fp = _make_docx(tmp_path, body="A normal paragraph.") + _extracted, _result, verdict = receive([fp]) + assert verdict == "ADMIT"