Files
serienbrief_django/app/mailmerge/tests/test_preview_service.py
T
2026-05-22 08:40:04 +02:00

174 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Unit-Tests für mailmerge.services.preview.
LibreOffice wird gemockt die Tests prüfen nur die Logik:
CSV-Parsing, Header-Validierung, Fehlerpfade, Result-Aufbau.
"""
from __future__ import annotations
import io
from pathlib import Path
from unittest.mock import patch
import pytest
from mailmerge.services.preview import (
PreviewError,
PreviewResult,
_read_first_row,
build_preview,
)
from mailmerge.tests.conftest import (
CSV_EXTRA_COLUMN,
CSV_MISSING_COLUMN,
CSV_NO_DATA,
CSV_NO_HEADER,
CSV_VALID,
)
# ---------------------------------------------------------------------------
# _read_first_row
# ---------------------------------------------------------------------------
class TestReadFirstRow:
def test_reads_header_and_first_row(self):
f = io.BytesIO(CSV_VALID.encode("utf-8"))
columns, row = _read_first_row(f)
assert columns == ["anrede_brief", "nachname", "vorname", "ort"]
assert row["nachname"] == "Huber"
assert row["vorname"] == "Andrea"
assert row["ort"] == "Eisenstadt"
def test_strips_utf8_bom(self):
"""CSV mit BOM darf nicht zu '\\ufeffanrede_brief' als erstem Header führen."""
f = io.BytesIO(("\ufeff" + CSV_VALID).encode("utf-8"))
columns, _ = _read_first_row(f)
assert columns[0] == "anrede_brief"
def test_accepts_path(self, tmp_path):
p = tmp_path / "r.csv"
p.write_text(CSV_VALID, encoding="utf-8")
columns, row = _read_first_row(p)
assert row["nachname"] == "Huber"
assert "ort" in columns
def test_no_header_raises(self):
f = io.BytesIO(CSV_NO_HEADER.encode("utf-8"))
with pytest.raises(PreviewError, match="keine Header"):
_read_first_row(f)
def test_no_data_row_raises(self):
f = io.BytesIO(CSV_NO_DATA.encode("utf-8"))
with pytest.raises(PreviewError, match="keine Datenzeile"):
_read_first_row(f)
# ---------------------------------------------------------------------------
# build_preview Header-Validierung
# ---------------------------------------------------------------------------
class TestBuildPreviewValidation:
def test_missing_column_is_reported(self, docx_file_on_disk):
csv_buf = io.BytesIO(CSV_MISSING_COLUMN.encode("utf-8"))
with pytest.raises(PreviewError, match="fehlen aber als Spalte"):
build_preview(docx_file_on_disk, csv_buf)
def test_missing_column_lists_specific_name(self, docx_file_on_disk):
csv_buf = io.BytesIO(CSV_MISSING_COLUMN.encode("utf-8"))
with pytest.raises(PreviewError) as excinfo:
build_preview(docx_file_on_disk, csv_buf)
assert "ort" in str(excinfo.value)
# ---------------------------------------------------------------------------
# build_preview Happy Path (mit gemocktem LibreOffice)
# ---------------------------------------------------------------------------
FAKE_PDF_BYTES = b"%PDF-1.4 fake test pdf"
@pytest.fixture
def patched_libreoffice(monkeypatch, tmp_path):
"""Ersetzt docx_to_pdf durch eine Funktion, die eine Dummy-PDF-Datei
in das Out-Dir legt und deren Pfad zurückgibt."""
def fake_docx_to_pdf(docx_path: Path, out_dir: Path) -> Path:
out_dir.mkdir(parents=True, exist_ok=True)
pdf_path = out_dir / (docx_path.stem + ".pdf")
pdf_path.write_bytes(FAKE_PDF_BYTES)
return pdf_path
monkeypatch.setattr(
"mailmerge.services.preview.docx_to_pdf", fake_docx_to_pdf
)
class TestBuildPreviewHappyPath:
def test_returns_preview_result(self, docx_file_on_disk, patched_libreoffice):
csv_buf = io.BytesIO(CSV_VALID.encode("utf-8"))
result = build_preview(docx_file_on_disk, csv_buf)
assert isinstance(result, PreviewResult)
assert result.pdf_bytes == FAKE_PDF_BYTES
def test_first_row_is_used(self, docx_file_on_disk, patched_libreoffice):
csv_buf = io.BytesIO(CSV_VALID.encode("utf-8"))
result = build_preview(docx_file_on_disk, csv_buf)
assert result.used_row["nachname"] == "Huber"
assert result.used_row["vorname"] == "Andrea"
def test_placeholders_are_listed(
self, docx_file_on_disk, patched_libreoffice, template_placeholders
):
csv_buf = io.BytesIO(CSV_VALID.encode("utf-8"))
result = build_preview(docx_file_on_disk, csv_buf)
assert set(result.placeholders) == set(template_placeholders)
def test_extra_columns_are_reported(self, docx_file_on_disk, patched_libreoffice):
csv_buf = io.BytesIO(CSV_EXTRA_COLUMN.encode("utf-8"))
result = build_preview(docx_file_on_disk, csv_buf)
assert "personalnr" in result.extra_columns
assert result.missing_columns == []
def test_calls_libreoffice_exactly_once(
self, docx_file_on_disk, monkeypatch, tmp_path
):
"""Sicherstellen, dass auch wirklich nur EIN Render-Vorgang läuft."""
calls = []
def fake_docx_to_pdf(docx_path: Path, out_dir: Path) -> Path:
calls.append(docx_path)
out_dir.mkdir(parents=True, exist_ok=True)
pdf_path = out_dir / (docx_path.stem + ".pdf")
pdf_path.write_bytes(FAKE_PDF_BYTES)
return pdf_path
monkeypatch.setattr(
"mailmerge.services.preview.docx_to_pdf", fake_docx_to_pdf
)
csv_buf = io.BytesIO(CSV_VALID.encode("utf-8"))
build_preview(docx_file_on_disk, csv_buf)
assert len(calls) == 1
# ---------------------------------------------------------------------------
# build_preview LibreOffice-Fehler propagieren
# ---------------------------------------------------------------------------
class TestBuildPreviewLibreOfficeFailure:
def test_runtime_error_bubbles_up(self, docx_file_on_disk, monkeypatch):
def fake_docx_to_pdf(docx_path, out_dir):
raise RuntimeError("LibreOffice ist abgestürzt")
monkeypatch.setattr(
"mailmerge.services.preview.docx_to_pdf", fake_docx_to_pdf
)
csv_buf = io.BytesIO(CSV_VALID.encode("utf-8"))
with pytest.raises(RuntimeError, match="LibreOffice"):
build_preview(docx_file_on_disk, csv_buf)