265 lines
7.7 KiB
Elixir
265 lines
7.7 KiB
Elixir
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
|