defmodule Mv.Membership.MembersPDFTest do @moduledoc """ Tests for MembersPDF module. Tests verify that the module correctly: - Loads the Typst template - Converts export data to template format - Generates valid PDF binary (starts with "%PDF") - Handles errors gracefully """ use ExUnit.Case, async: true alias Mv.Config alias Mv.Membership.MembersPDF describe "render/1" do test "rejects export when row count exceeds limit" do max_rows = Config.pdf_export_row_limit() rows_over_limit = max_rows + 1 export_data = %{ columns: [ %{key: "first_name", kind: :member_field, label: "Vorname"} ], rows: Enum.map(1..rows_over_limit, fn i -> ["Member #{i}"] end), meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: rows_over_limit } } result = MembersPDF.render(export_data) assert {:error, {:row_limit_exceeded, ^rows_over_limit, ^max_rows}} = result end test "allows export when row count equals limit" do max_rows = Config.pdf_export_row_limit() export_data = %{ columns: [ %{key: "first_name", kind: :member_field, label: "Vorname"} ], rows: Enum.map(1..max_rows, fn i -> ["Member #{i}"] end), meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: max_rows } } result = MembersPDF.render(export_data) assert {:ok, pdf_binary} = result assert String.starts_with?(pdf_binary, "%PDF") end test "allows export when row count is below limit" do max_rows = Config.pdf_export_row_limit() rows_below_limit = max(1, max_rows - 10) export_data = %{ columns: [ %{key: "first_name", kind: :member_field, label: "Vorname"} ], rows: Enum.map(1..rows_below_limit, fn i -> ["Member #{i}"] end), meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: rows_below_limit } } result = MembersPDF.render(export_data) assert {:ok, pdf_binary} = result assert String.starts_with?(pdf_binary, "%PDF") end test "generates valid PDF from minimal dataset" do export_data = %{ columns: [ %{key: "first_name", kind: :member_field, label: "Vorname"}, %{key: "last_name", kind: :member_field, label: "Nachname"} ], rows: [ ["Max", "Mustermann"], ["Anna", "Schmidt"] ], meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: 2 } } result = MembersPDF.render(export_data) assert {:ok, pdf_binary} = result assert is_binary(pdf_binary) assert String.starts_with?(pdf_binary, "%PDF") assert byte_size(pdf_binary) > 1000 end test "generates valid PDF with custom fields and computed fields" do export_data = %{ columns: [ %{key: "first_name", kind: :member_field, label: "Vorname"}, %{key: "last_name", kind: :member_field, label: "Nachname"}, %{key: "email", kind: :member_field, label: "E-Mail"}, %{key: :membership_fee_status, kind: :computed, label: "Beitragsstatus"}, %{ key: "550e8400-e29b-41d4-a716-446655440000", kind: :custom_field, label: "Mitgliedsnummer", custom_field: %{ id: "550e8400-e29b-41d4-a716-446655440000", name: "Mitgliedsnummer", value_type: :string } } ], rows: [ ["Max", "Mustermann", "max@example.com", "paid", "M-2024-001"], ["Anna", "Schmidt", "anna@example.com", "unpaid", "M-2024-002"] ], meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: 2 } } result = MembersPDF.render(export_data) assert {:ok, pdf_binary} = result assert is_binary(pdf_binary) assert String.starts_with?(pdf_binary, "%PDF") end test "maintains deterministic column and row order" do export_data = %{ columns: [ %{key: "first_name", kind: :member_field, label: "Vorname"}, %{key: "last_name", kind: :member_field, label: "Nachname"}, %{key: "email", kind: :member_field, label: "E-Mail"} ], rows: [ ["Max", "Mustermann", "max@example.com"], ["Anna", "Schmidt", "anna@example.com"], ["Peter", "Müller", "peter@example.com"] ], meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: 3 } } # Render twice and verify identical output {:ok, pdf1} = MembersPDF.render(export_data) {:ok, pdf2} = MembersPDF.render(export_data) assert pdf1 == pdf2 assert String.starts_with?(pdf1, "%PDF") assert String.starts_with?(pdf2, "%PDF") end test "returns error when template file is missing" do # Temporarily rename template to simulate missing file template_path = Path.join(Application.app_dir(:mv, "priv"), "pdf_templates/members_export.typ") original_content = File.read!(template_path) File.rm(template_path) export_data = %{ columns: [%{key: "first_name", kind: :member_field, label: "Vorname"}], rows: [["Max"]], meta: %{generated_at: "2024-01-15T14:30:00Z", member_count: 1} } result = MembersPDF.render(export_data) assert {:error, {:template_not_found, _reason}} = result # Restore template File.write!(template_path, original_content) end test "handles empty rows gracefully" do export_data = %{ columns: [ %{key: "first_name", kind: :member_field, label: "Vorname"}, %{key: "last_name", kind: :member_field, label: "Nachname"} ], rows: [], meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: 0 } } result = MembersPDF.render(export_data) assert {:ok, pdf_binary} = result assert is_binary(pdf_binary) assert String.starts_with?(pdf_binary, "%PDF") end test "handles many columns correctly" do # Test with 10 columns to ensure dynamic column width calculation works columns = Enum.map(1..10, fn i -> %{key: "field_#{i}", kind: :member_field, label: "Feld #{i}"} end) export_data = %{ columns: columns, rows: [Enum.map(1..10, &"Wert #{&1}")], meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: 1 } } result = MembersPDF.render(export_data) assert {:ok, pdf_binary} = result assert is_binary(pdf_binary) assert String.starts_with?(pdf_binary, "%PDF") end test "creates and cleans up temp directory" do export_data = %{ columns: [%{key: "first_name", kind: :member_field, label: "Vorname"}], rows: [["Max"]], meta: %{ generated_at: "2024-01-15T14:30:00Z", member_count: 1 } } # Get temp base directory temp_base = System.tmp_dir!() # Count temp directories before before_count = temp_base |> File.ls!() |> Enum.count(fn name -> String.starts_with?(name, "mv_pdf_export_") end) result = MembersPDF.render(export_data) assert {:ok, _pdf_binary} = result # Wait a bit for cleanup (async cleanup might take a moment) Process.sleep(100) # Count temp directories after after_count = temp_base |> File.ls!() |> Enum.count(fn name -> String.starts_with?(name, "mv_pdf_export_") end) # Should have same or fewer temp dirs (cleanup should have run) assert after_count <= before_count + 1 end end end