defmodule Mv.Membership.MembersCSVTest do use ExUnit.Case, async: true alias Mv.Membership.MembersCSV describe "export/3" do test "returns CSV with header and one data row (member fields only)" do member = %{first_name: "Jane", email: "jane@example.com"} member_fields = ["first_name", "email"] custom_fields_by_id = %{} iodata = MembersCSV.export([member], member_fields, custom_fields_by_id) csv = IO.iodata_to_binary(iodata) assert csv =~ "first_name" assert csv =~ "email" assert csv =~ "Jane" assert csv =~ "jane@example.com" # One header line, one data line lines = String.split(csv, "\n", trim: true) assert length(lines) == 2 end test "escapes cell containing comma (RFC 4180 quoted)" do member = %{first_name: "Doe, John", email: "john@example.com"} member_fields = ["first_name", "email"] custom_fields_by_id = %{} iodata = MembersCSV.export([member], member_fields, custom_fields_by_id) csv = IO.iodata_to_binary(iodata) # Comma inside value must be quoted so the cell is one field assert csv =~ ~s("Doe, John") assert csv =~ "john@example.com" end test "escapes cell containing double-quote (RFC 4180 doubled and quoted)" do member = %{first_name: ~s(He said "Hi"), email: "a@b.com"} member_fields = ["first_name", "email"] custom_fields_by_id = %{} iodata = MembersCSV.export([member], member_fields, custom_fields_by_id) csv = IO.iodata_to_binary(iodata) # Double-quote inside value must be doubled and cell quoted assert csv =~ ~s("He said ""Hi""") assert csv =~ "a@b.com" end test "formats date as ISO8601 for member fields" do member = %{first_name: "D", email: "d@d.com", join_date: ~D[2024-03-15]} iodata = MembersCSV.export([member], ["first_name", "email", "join_date"], %{}) csv = IO.iodata_to_binary(iodata) assert csv =~ "2024-03-15" assert csv =~ "join_date" end test "formats nil as empty string" do member = %{first_name: "Only", last_name: nil, email: "x@y.com"} member_fields = ["first_name", "last_name", "email"] custom_fields_by_id = %{} iodata = MembersCSV.export([member], member_fields, custom_fields_by_id) csv = IO.iodata_to_binary(iodata) assert csv =~ "first_name" assert csv =~ "Only" assert csv =~ "x@y.com" # Nil becomes empty; between Only and x@y we have empty (e.g. Only,,x@y.com) assert csv =~ "Only,,x@y" end test "formats boolean as true/false" do # Use a field we can set to boolean via a custom-like struct - member has no boolean field. # So we test via custom field instead. custom_cf = %{id: "cf-1", name: "Active", value_type: :boolean} custom_fields_by_id = %{"cf-1" => custom_cf} member_with_cfv = %{ first_name: "Test", email: "e@e.com", custom_field_values: [ %{custom_field_id: "cf-1", value: true, custom_field: custom_cf} ] } iodata = MembersCSV.export( [member_with_cfv], ["first_name", "email"], custom_fields_by_id ) csv = IO.iodata_to_binary(iodata) assert csv =~ "Active" # Formatter yields "Yes" for true (gettext) assert csv =~ "Yes" end test "includes custom field columns in header and rows (order from map)" do cf1 = %{id: "a", name: "Custom1", value_type: :string} cf2 = %{id: "b", name: "Custom2", value_type: :string} # Map order: a then b custom_fields_by_id = %{"a" => cf1, "b" => cf2} member = %{ first_name: "M", email: "m@m.com", custom_field_values: [ %{custom_field_id: "a", value: "v1", custom_field: cf1}, %{custom_field_id: "b", value: "v2", custom_field: cf2} ] } iodata = MembersCSV.export([member], ["first_name", "email"], custom_fields_by_id) csv = IO.iodata_to_binary(iodata) assert csv =~ "first_name,email,Custom1,Custom2" assert csv =~ "v1" assert csv =~ "v2" end end end