Files
chemenu/tools/chemenu/tests/test_raw_derive_binary.py
T
torbenandClaude Code 918157cb41
CI / verify (push) Successful in 7m40s
CI / pwsh (push) Successful in 2m24s
Release / release (push) Successful in 35s
Release / wiki-mirror (push) Skipped
raw derive: a placeholder text/plain no longer displaces the HTML text of a mail (#204)
A mail's text is its plain text unless that is under 10% of the HTML text, then the HTML text
with a warning; one rule for .eml and .msg. The other version of the text in a
multipart/alternative is no longer an attachment.

Files changed:
- CHANGES.md
- VERSION
- docs/reference/commands/raw-material-and-uploads.md
- testfiles/mail/gog-roundcube.eml.expect
- tools/CONTRACT.md
- tools/chemenu/commands/raw_cmd.py
- tools/chemenu/filters/mail.py
- tools/chemenu/filters/msg.py
- tools/chemenu/tests/test_raw_derive.py
- tools/chemenu/tests/test_raw_derive_binary.py

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
2026-10-09 16:55:04 +02:00

273 lines
11 KiB
Python

"""`wikitool raw derive` for the two formats read through a package: PDF (`pypdf`) and
Outlook `.msg` (`olefile`). The documents come from `binary_formats`, so no binary sits
in the repository; whether the builders write valid files is settled by the packages
reading them here. The tests that need a package skip where it is not installed - CI
installs `tools/requirements.txt`, so they run there."""
from __future__ import annotations
import importlib.util
import io
from pathlib import Path
import pytest
from chemenu import filters
from chemenu.commands import doctor
from chemenu.tests.binary_formats import build_msg, build_pdf
from chemenu.tests.test_raw_derive import ( # noqa: F401 (tree is a fixture)
_derive,
_header_and_text,
_put,
_refused,
_snapshot,
make_docx,
make_mail,
make_zip,
tree,
)
needs_pypdf = pytest.mark.skipif(importlib.util.find_spec("pypdf") is None, reason="pypdf is not installed")
needs_olefile = pytest.mark.skipif(importlib.util.find_spec("olefile") is None, reason="olefile is not installed")
LONG_LINE = "A sentence long enough that the page is clearly not a scan of a picture."
# --- PDF ---------------------------------------------------------------------
@needs_pypdf
def test_a_pdf_becomes_a_folder_with_page_markers(tree, capsys):
original = build_pdf([[LONG_LINE, "Second line (with brackets)"], [LONG_LINE + " Two."]], title="Quarterly report")
_put(tree, "Report.pdf", original)
_derive("incoming/Report.pdf")
out = capsys.readouterr().out
folder = tree / "incoming/Report"
assert (folder / "original.pdf").read_bytes() == original
header, text = _header_and_text(folder / "derived.md")
assert header["filter"] == "pdf" and "pypdf" in header["engine"]
assert int(header["pages"]) == 2 and header["title"] == "Quarterly report"
assert text.index("<!-- page 1 -->") < text.index("Second line (with brackets)") < text.index("<!-- page 2 -->")
assert "WARN" not in out
assert out.count("CHECK") == 1 and "page" in out
@needs_pypdf
def test_a_pdf_without_text_is_derived_with_a_scan_warning(tree, capsys):
_put(tree, "scan.pdf", build_pdf([[], [], []]))
_derive("incoming/scan.pdf")
out = capsys.readouterr().out
assert "WARN" in out and "scan" in out and "3 page" in out
assert (tree / "incoming/scan/original.pdf").is_file()
@needs_pypdf
def test_a_pdf_that_is_not_a_pdf_is_refused_untouched(tree):
_put(tree, "fake.pdf", b"this is not a pdf at all")
before = _snapshot(tree / "incoming")
_refused("incoming/fake.pdf")
assert _snapshot(tree / "incoming") == before
@needs_pypdf
def test_an_encrypted_pdf_is_refused_untouched(tree, capsys):
import pypdf
writer = pypdf.PdfWriter(clone_from=pypdf.PdfReader(io.BytesIO(build_pdf([[LONG_LINE]]))))
try:
writer.encrypt("secret", algorithm="RC4-128")
except Exception as exc: # no crypto provider for the algorithm on this machine
pytest.skip(f"cannot build an encrypted PDF here: {exc}")
buffer = io.BytesIO()
writer.write(buffer)
_put(tree, "locked.pdf", buffer.getvalue())
before = _snapshot(tree / "incoming")
_refused("incoming/locked.pdf")
assert "encrypted" in capsys.readouterr().out
assert _snapshot(tree / "incoming") == before
@needs_pypdf
def test_a_pdf_inside_a_zip_gets_its_md(tree):
_put(tree, "bundle.zip", make_zip({"a.pdf": build_pdf([[LONG_LINE]]), "b.txt": b"x"}))
_derive("incoming/bundle.zip")
folder = tree / "incoming/bundle"
assert LONG_LINE in (folder / "a.md").read_text(encoding="utf-8")
assert (folder / "a.pdf").is_file() and not (folder / "b.md").exists()
@needs_pypdf
def test_a_pdf_attached_to_a_mail_gets_its_md(tree):
_put(tree, "mail.eml", make_mail([("offer.pdf", build_pdf([[LONG_LINE]]), "application/pdf")]))
_derive("incoming/mail.eml")
assert LONG_LINE in (tree / "incoming/mail/offer.md").read_text(encoding="utf-8")
def test_a_missing_package_stops_only_its_own_format(tree, monkeypatch, capsys):
pdf_filter = filters.filter_for("a.pdf")
monkeypatch.setattr(type(pdf_filter), "missing", lambda self: ["pypdf"])
_put(tree, "a.pdf", b"%PDF-1.4")
_put(tree, "b.docx", make_docx("<w:p><w:r><w:t>Still works</w:t></w:r></w:p>"))
before = _snapshot(tree / "incoming")
_refused("incoming/a.pdf")
out = capsys.readouterr().out
assert "pypdf" in out and "preflight" in out
assert _snapshot(tree / "incoming") == before
_derive("incoming/b.docx")
assert "Still works" in (tree / "incoming/b/derived.md").read_text(encoding="utf-8")
def test_a_package_missing_inside_a_zip_refuses_the_whole_zip(tree, monkeypatch):
monkeypatch.setattr(type(filters.filter_for("a.pdf")), "missing", lambda self: ["pypdf"])
_put(tree, "bundle.zip", make_zip({"a.pdf": b"%PDF-1.4", "b.txt": b"x"}))
before = _snapshot(tree / "incoming")
_refused("incoming/bundle.zip")
assert _snapshot(tree / "incoming") == before
# --- Outlook .msg --------------------------------------------------------------
SENT = 134_000_000_000_000_000 # 2025-08-18T14:13:20+00:00
@needs_olefile
def test_a_msg_becomes_a_folder_like_a_mail(tree, capsys):
data = [b"attachment bytes" * 400, b"small"]
original = build_msg(
subject="Unterlagen", body="Hello Bob\r\nSee attached.", sender="Anna", sender_address="anna@example.org",
to="bob@example.org; carol@example.org", sent=SENT,
attachments=[("report.bin", data[0]), ("note.txt", data[1])],
)
_put(tree, "Mail.msg", original)
_derive("incoming/Mail.msg")
out = capsys.readouterr().out
folder = tree / "incoming/Mail"
assert (folder / "original.msg").read_bytes() == original
assert (folder / "report.bin").read_bytes() == data[0]
assert (folder / "note.txt").read_bytes() == data[1]
header, text = _header_and_text(folder / "derived.md")
assert header["filter"] == "msg" and "olefile" in header["engine"]
assert header["from"] == "Anna <anna@example.org>"
assert header["to"] == ["bob@example.org", "carol@example.org"]
assert header["subject"] == "Unterlagen" and str(header["date"]).startswith("2025-08-18")
assert header["attachments"] == ["report.bin", "note.txt"]
assert text.strip() == "Hello Bob\nSee attached."
assert "report.bin" in out
@needs_olefile
def test_a_msg_with_only_html_takes_the_html_text(tree):
_put(tree, "h.msg", build_msg(subject="S", html="<html><body><p>Only <b>html</b> here</p></body></html>"))
_derive("incoming/h.msg")
_header, text = _header_and_text(tree / "incoming/h/derived.md")
assert "Only" in text and "html here" in text and "<p>" not in text
_HTML_BODY = "<html><body><p>" + "Real content of the mail. " * 20 + "</p></body></html>"
@needs_olefile
def test_a_msg_with_a_placeholder_plain_text_takes_the_html_text_and_warns(tree, capsys):
_put(tree, "p.msg", build_msg(subject="S", body="Plain text version not available", html=_HTML_BODY))
_derive("incoming/p.msg")
out = capsys.readouterr().out
_header, text = _header_and_text(tree / "incoming/p/derived.md")
assert "Real content of the mail." in text and "not available" not in text
assert "The plain text (32 characters) is under 10% of the HTML text" in out
@needs_olefile
def test_a_msg_with_a_good_plain_text_keeps_it_without_a_warning(tree, capsys):
plain = "Real content of the mail. " * 18
_put(tree, "g.msg", build_msg(subject="S", body=plain, html=_HTML_BODY))
_derive("incoming/g.msg")
out = capsys.readouterr().out
_header, text = _header_and_text(tree / "incoming/g/derived.md")
assert text.strip() == plain.strip()
assert "is under" not in out
@needs_olefile
def test_a_msg_with_only_rtf_warns_and_has_no_text(tree, capsys):
_put(tree, "r.msg", build_msg(subject="S", rtf=b"{\\rtf1 hello}"))
_derive("incoming/r.msg")
out = capsys.readouterr().out
assert "WARN" in out and "RTF" in out
_header, text = _header_and_text(tree / "incoming/r/derived.md")
assert text.strip() == ""
@needs_olefile
def test_a_msg_names_inline_images_and_embedded_messages_without_writing_them(tree, capsys):
_put(tree, "m.msg", build_msg(
subject="S", body="text", inline_images=[("logo.png", b"png")], embedded=["forwarded.msg"],
attachments=[("real.bin", b"x")],
))
_derive("incoming/m.msg")
out = capsys.readouterr().out
assert sorted(p.name for p in (tree / "incoming/m").iterdir()) == ["derived.md", "original.msg", "real.bin"]
assert "inline image" in out and "forwarded.msg" in out
@needs_olefile
def test_a_msg_attached_to_a_mail_is_quoted(tree, capsys):
inner = build_msg(subject="Inner subject", body="Inner words", sender="Dora", to="x@example.org")
_put(tree, "mail.eml", make_mail([("inner.msg", inner, "application/vnd.ms-outlook")]))
_derive("incoming/mail.eml")
folder = tree / "incoming/mail"
assert sorted(p.name for p in folder.iterdir()) == ["derived.md", "original.eml"]
_header, text = _header_and_text(folder / "derived.md")
assert "## Attached message: Inner subject" in text and "> Inner words" in text
@needs_olefile
def test_a_file_that_is_not_a_msg_is_refused_untouched(tree):
_put(tree, "x.msg", b"plain text pretending to be a message")
before = _snapshot(tree / "incoming")
_refused("incoming/x.msg")
assert _snapshot(tree / "incoming") == before
@needs_olefile
def test_a_compound_file_that_is_not_a_message_is_refused(tree):
from chemenu.tests.binary_formats import build_cfb
_put(tree, "x.msg", build_cfb({"WordDocument": b"abc"}))
_refused("incoming/x.msg")
assert not (tree / "incoming/x").exists()
def test_a_msg_without_the_package_is_refused_and_a_docx_still_works(tree, monkeypatch, capsys):
monkeypatch.setattr(type(filters.filter_for("a.msg")), "missing", lambda self: ["olefile"])
_put(tree, "a.msg", b"\xd0\xcf\x11\xe0")
before = _snapshot(tree / "incoming")
_refused("incoming/a.msg")
assert "olefile" in capsys.readouterr().out
assert _snapshot(tree / "incoming") == before
# --- doctor ---------------------------------------------------------------------
def test_doctor_names_each_filter_that_needs_a_package():
names = {check.name for check in doctor.check_import_filters()}
assert names == {"import-filter-pdf", "import-filter-msg"}
def test_doctor_warns_for_a_missing_package_and_never_fails(monkeypatch):
monkeypatch.setattr(type(filters.filter_for("a.pdf")), "missing", lambda self: ["pypdf"])
checks = {check.name: check for check in doctor.check_import_filters()}
assert checks["import-filter-pdf"].status == "WARN"
assert "pypdf" in checks["import-filter-pdf"].detail and checks["import-filter-pdf"].fix
assert all(check.status != "FAIL" for check in checks.values())
@needs_pypdf
def test_doctor_reports_an_installed_package_with_its_version():
check = next(c for c in doctor.check_import_filters() if c.name == "import-filter-pdf")
assert check.status == "OK" and check.detail.startswith("pypdf ") and ".pdf" in check.detail