""" 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