207 lines
7.5 KiB
Python
207 lines
7.5 KiB
Python
|
|
"""
|
|||
|
|
Tests für die job_preview-View.
|
|||
|
|
|
|||
|
|
Hängt nicht am echten LibreOffice – build_preview wird auf Service-Ebene
|
|||
|
|
gepatcht, sodass die Tests in <1 s laufen.
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|||
|
|
from django.urls import reverse
|
|||
|
|
|
|||
|
|
from mailmerge.services.preview import PreviewError, PreviewResult
|
|||
|
|
|
|||
|
|
|
|||
|
|
PREVIEW_URL_NAME = "job-preview"
|
|||
|
|
FAKE_PDF = b"%PDF-1.4 fake preview\n"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# HTTP-Method / Auth
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
class TestPreviewAccess:
|
|||
|
|
def test_login_required(self, client, db):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = client.post(url, {})
|
|||
|
|
# Django leitet auf Login um (302)
|
|||
|
|
assert response.status_code == 302
|
|||
|
|
assert "/login" in response["Location"].lower() or "login" in response["Location"].lower()
|
|||
|
|
|
|||
|
|
def test_get_not_allowed(self, auth_client):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.get(url)
|
|||
|
|
assert response.status_code == 405
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Form-Validierung
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
class TestPreviewFormValidation:
|
|||
|
|
def test_missing_files_returns_form(self, auth_client):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {})
|
|||
|
|
# Kein PDF, sondern Form-Template zurück
|
|||
|
|
assert response.status_code == 200
|
|||
|
|
assert response["Content-Type"].startswith("text/html")
|
|||
|
|
|
|||
|
|
def test_rejects_non_csv_extension(
|
|||
|
|
self, auth_client, letter_template, docx_bytes
|
|||
|
|
):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
bogus = SimpleUploadedFile("file.txt", b"foo", content_type="text/plain")
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": bogus,
|
|||
|
|
})
|
|||
|
|
assert response.status_code == 200
|
|||
|
|
assert b"Nur .csv-Dateien" in response.content
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Happy Path
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def patched_build_preview(monkeypatch):
|
|||
|
|
"""Ersetzt build_preview in der Views-Importebene durch einen Stub."""
|
|||
|
|
def fake_build_preview(template_path: Path, csv_file):
|
|||
|
|
return PreviewResult(
|
|||
|
|
pdf_bytes=FAKE_PDF,
|
|||
|
|
used_row={"nachname": "Huber"},
|
|||
|
|
placeholders=["anrede_brief", "nachname", "vorname", "ort"],
|
|||
|
|
csv_columns=["anrede_brief", "nachname", "vorname", "ort"],
|
|||
|
|
missing_columns=[],
|
|||
|
|
extra_columns=[],
|
|||
|
|
)
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
"mailmerge.views.build_preview", fake_build_preview
|
|||
|
|
)
|
|||
|
|
return fake_build_preview
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestPreviewHappyPath:
|
|||
|
|
def test_returns_pdf_content_type(
|
|||
|
|
self, auth_client, letter_template, csv_uploaded_valid, patched_build_preview
|
|||
|
|
):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": csv_uploaded_valid,
|
|||
|
|
})
|
|||
|
|
assert response.status_code == 200
|
|||
|
|
assert response["Content-Type"] == "application/pdf"
|
|||
|
|
|
|||
|
|
def test_returns_pdf_body(
|
|||
|
|
self, auth_client, letter_template, csv_uploaded_valid, patched_build_preview
|
|||
|
|
):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": csv_uploaded_valid,
|
|||
|
|
})
|
|||
|
|
assert response.content == FAKE_PDF
|
|||
|
|
|
|||
|
|
def test_content_disposition_inline(
|
|||
|
|
self, auth_client, letter_template, csv_uploaded_valid, patched_build_preview
|
|||
|
|
):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": csv_uploaded_valid,
|
|||
|
|
})
|
|||
|
|
assert response["Content-Disposition"].startswith("inline")
|
|||
|
|
|
|||
|
|
def test_placeholders_header_set(
|
|||
|
|
self, auth_client, letter_template, csv_uploaded_valid, patched_build_preview
|
|||
|
|
):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": csv_uploaded_valid,
|
|||
|
|
})
|
|||
|
|
assert "X-Preview-Placeholders" in response
|
|||
|
|
assert "nachname" in response["X-Preview-Placeholders"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Extra-Spalten als Hinweis-Header
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
class TestPreviewExtraColumnsHeader:
|
|||
|
|
@pytest.fixture
|
|||
|
|
def patched_with_extras(self, monkeypatch):
|
|||
|
|
def fake_build_preview(template_path, csv_file):
|
|||
|
|
return PreviewResult(
|
|||
|
|
pdf_bytes=FAKE_PDF,
|
|||
|
|
used_row={"nachname": "Huber"},
|
|||
|
|
placeholders=["nachname"],
|
|||
|
|
csv_columns=["nachname", "personalnr"],
|
|||
|
|
missing_columns=[],
|
|||
|
|
extra_columns=["personalnr"],
|
|||
|
|
)
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
"mailmerge.views.build_preview", fake_build_preview
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_extra_columns_header_contains_value(
|
|||
|
|
self, auth_client, letter_template, csv_uploaded_valid, patched_with_extras
|
|||
|
|
):
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": csv_uploaded_valid,
|
|||
|
|
})
|
|||
|
|
assert response["X-Preview-Extra-Columns"] == "personalnr"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Fehlerpfade: PreviewError und unerwartete Exceptions
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
class TestPreviewErrorHandling:
|
|||
|
|
def test_preview_error_renders_form_with_message(
|
|||
|
|
self, auth_client, letter_template, csv_uploaded_valid, monkeypatch
|
|||
|
|
):
|
|||
|
|
def fake_build_preview(template_path, csv_file):
|
|||
|
|
raise PreviewError("Spalte 'ort' fehlt")
|
|||
|
|
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
"mailmerge.views.build_preview", fake_build_preview
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": csv_uploaded_valid,
|
|||
|
|
})
|
|||
|
|
assert response.status_code == 200
|
|||
|
|
assert response["Content-Type"].startswith("text/html")
|
|||
|
|
# Django escaped Apostrophe in Templates ("'" -> "'")
|
|||
|
|
body = response.content.decode("utf-8")
|
|||
|
|
assert "Spalte" in body and "ort" in body and "fehlt" in body
|
|||
|
|
|
|||
|
|
def test_unexpected_exception_renders_form(
|
|||
|
|
self, auth_client, letter_template, csv_uploaded_valid, monkeypatch
|
|||
|
|
):
|
|||
|
|
def fake_build_preview(template_path, csv_file):
|
|||
|
|
raise RuntimeError("LibreOffice timeout")
|
|||
|
|
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
"mailmerge.views.build_preview", fake_build_preview
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
url = reverse(PREVIEW_URL_NAME)
|
|||
|
|
response = auth_client.post(url, {
|
|||
|
|
"template": str(letter_template.pk),
|
|||
|
|
"recipients_csv": csv_uploaded_valid,
|
|||
|
|
})
|
|||
|
|
assert response.status_code == 200
|
|||
|
|
assert response["Content-Type"].startswith("text/html")
|
|||
|
|
assert b"Vorschau konnte nicht erstellt werden" in response.content
|
|||
|
|
assert b"LibreOffice timeout" in response.content
|