defmodule MvWeb.MemberExportControllerTest do use MvWeb.ConnCase, async: true alias Mv.Fixtures defp csrf_token_from_conn(conn) do get_session(conn, "_csrf_token") || csrf_token_from_html(response(conn, 200)) end defp csrf_token_from_html(html) when is_binary(html) do case Regex.run(~r/name="csrf-token"\s+content="([^"]+)"/, html) do [_, token] -> token _ -> nil end end describe "POST /members/export.csv" do setup %{conn: conn} do # Create 3 members for export tests m1 = Fixtures.member_fixture(%{ first_name: "Alice", last_name: "One", email: "alice.one@example.com" }) m2 = Fixtures.member_fixture(%{ first_name: "Bob", last_name: "Two", email: "bob.two@example.com" }) m3 = Fixtures.member_fixture(%{ first_name: "Carol", last_name: "Three", email: "carol.three@example.com" }) %{member1: m1, member2: m2, member3: m3, conn: conn} end test "selected export: returns 200, text/csv, header + exactly 2 data rows", %{ conn: conn, member1: m1, member2: m2 } do payload = %{ "selected_ids" => [m1.id, m2.id], "member_fields" => ["first_name", "last_name", "email"], "custom_field_ids" => [], "query" => nil, "sort_field" => nil, "sort_order" => nil } conn = get(conn, "/members") csrf_token = csrf_token_from_conn(conn) conn = post(conn, "/members/export.csv", %{ "payload" => Jason.encode!(payload), "_csrf_token" => csrf_token }) assert conn.status == 200 assert get_resp_header(conn, "content-type") |> List.first() =~ "text/csv" body = response(conn, 200) lines = String.split(body, "\n", trim: true) # Header + 2 data rows assert length(lines) == 3 assert hd(lines) =~ "first_name" assert hd(lines) =~ "email" assert body =~ "Alice" assert body =~ "Bob" refute body =~ "Carol" end test "all export: selected_ids=[] returns all members (at least 3 data rows)", %{ conn: conn, member1: _m1, member2: _m2, member3: _m3 } do payload = %{ "selected_ids" => [], "member_fields" => ["first_name", "email"], "custom_field_ids" => [], "query" => nil, "sort_field" => nil, "sort_order" => nil } conn = get(conn, "/members") csrf_token = csrf_token_from_conn(conn) conn = post(conn, "/members/export.csv", %{ "payload" => Jason.encode!(payload), "_csrf_token" => csrf_token }) assert conn.status == 200 body = response(conn, 200) lines = String.split(body, "\n", trim: true) # Header + at least 3 data rows assert length(lines) >= 4 assert hd(lines) =~ "first_name" assert body =~ "Alice" assert body =~ "Bob" assert body =~ "Carol" end test "whitelist: unknown member_fields are not in header", %{conn: conn, member1: m1} do payload = %{ "selected_ids" => [m1.id], "member_fields" => ["first_name", "unknown_field", "email"], "custom_field_ids" => [], "query" => nil, "sort_field" => nil, "sort_order" => nil } conn = get(conn, "/members") csrf_token = csrf_token_from_conn(conn) conn = post(conn, "/members/export.csv", %{ "payload" => Jason.encode!(payload), "_csrf_token" => csrf_token }) assert conn.status == 200 body = response(conn, 200) header = body |> String.split("\n", trim: true) |> hd() assert header =~ "first_name" assert header =~ "email" refute header =~ "unknown_field" end end end