defmodule Mv.Membership.Import.MemberCSVTest do use Mv.DataCase, async: false alias Mv.Membership.Import.MemberCSV describe "Error struct" do test "Error struct exists with required fields" do # This will fail at runtime if the struct doesn't exist # We use struct/2 to create the struct at runtime error = struct(MemberCSV.Error, %{ csv_line_number: 5, field: :email, message: "is not a valid email" }) assert error.csv_line_number == 5 assert error.field == :email assert error.message == "is not a valid email" end test "Error struct allows nil field" do # This will fail at runtime if the struct doesn't exist error = struct(MemberCSV.Error, %{ csv_line_number: 10, field: nil, message: "Row is empty" }) assert error.csv_line_number == 10 assert error.field == nil assert error.message == "Row is empty" end end describe "prepare/2" do test "function exists and accepts file_content and opts" do file_content = "email\njohn@example.com" opts = [] # This will fail until the function is implemented result = MemberCSV.prepare(file_content, opts) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "returns {:ok, import_state} on success" do file_content = "email\njohn@example.com" opts = [] assert {:ok, import_state} = MemberCSV.prepare(file_content, opts) # Check that import_state contains expected fields assert Map.has_key?(import_state, :chunks) assert Map.has_key?(import_state, :column_map) assert Map.has_key?(import_state, :custom_field_map) assert Map.has_key?(import_state, :warnings) assert import_state.column_map[:email] == 0 assert import_state.chunks != [] end test "returns {:error, reason} on failure" do file_content = "" opts = [] assert {:error, _reason} = MemberCSV.prepare(file_content, opts) end test "function has documentation" do # Check that @doc exists by reading the module assert function_exported?(MemberCSV, :prepare, 2) end end describe "process_chunk/3" do test "function exists and accepts chunk_rows_with_lines, column_map, custom_field_map, and opts" do chunk_rows_with_lines = [{2, %{member: %{email: "john@example.com"}, custom: %{}}}] column_map = %{email: 0} custom_field_map = %{} opts = [] # This will fail until the function is implemented result = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "creates member successfully with valid data" do chunk_rows_with_lines = [ {2, %{member: %{email: "john@example.com", first_name: "John"}, custom: %{}}} ] column_map = %{email: 0, first_name: 1} custom_field_map = %{} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 1 assert chunk_result.failed == 0 assert chunk_result.errors == [] # Verify member was created members = Mv.Membership.list_members!() assert Enum.any?(members, &(&1.email == "john@example.com")) end test "returns error for invalid email" do chunk_rows_with_lines = [ {2, %{member: %{email: "invalid-email"}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 2 assert error.field == :email assert error.message =~ "email" end test "returns error for duplicate email" do # Create existing member first {:ok, _existing} = Mv.Membership.create_member(%{email: "duplicate@example.com", first_name: "Existing"}) chunk_rows_with_lines = [ {2, %{member: %{email: "duplicate@example.com", first_name: "New"}, custom: %{}}} ] column_map = %{email: 0, first_name: 1} custom_field_map = %{} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 2 assert error.field == :email assert error.message =~ "email" or error.message =~ "duplicate" or error.message =~ "unique" end test "creates member with custom field values" do # Create custom field first {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Phone", value_type: :string }) |> Ash.create() chunk_rows_with_lines = [ {2, %{ member: %{email: "withcustom@example.com"}, custom: %{to_string(custom_field.id) => "123-456-7890"} }} ] column_map = %{email: 0} custom_field_map = %{to_string(custom_field.id) => 1} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 1 assert chunk_result.failed == 0 # Verify member and custom field value were created members = Mv.Membership.list_members!() member = Enum.find(members, &(&1.email == "withcustom@example.com")) assert member != nil {:ok, member_with_cf} = Ash.load(member, :custom_field_values) assert length(member_with_cf.custom_field_values) == 1 cfv = List.first(member_with_cf.custom_field_values) assert cfv.custom_field_id == custom_field.id assert cfv.value.value == "123-456-7890" end test "handles multiple rows with mixed success and failure" do chunk_rows_with_lines = [ {2, %{member: %{email: "valid1@example.com"}, custom: %{}}}, {3, %{member: %{email: "invalid-email"}, custom: %{}}}, {4, %{member: %{email: "valid2@example.com"}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 2 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 3 end test "preserves CSV line numbers in errors" do chunk_rows_with_lines = [ {5, %{member: %{email: "invalid"}, custom: %{}}}, {10, %{member: %{email: "also-invalid"}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.failed == 2 assert length(chunk_result.errors) == 2 line_numbers = Enum.map(chunk_result.errors, & &1.csv_line_number) assert 5 in line_numbers assert 10 in line_numbers end test "returns {:ok, chunk_result} on success" do chunk_rows_with_lines = [{2, %{member: %{email: "test@example.com"}, custom: %{}}}] column_map = %{email: 0} custom_field_map = %{} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) # Check that chunk_result contains expected fields assert Map.has_key?(chunk_result, :inserted) assert Map.has_key?(chunk_result, :failed) assert Map.has_key?(chunk_result, :errors) assert is_integer(chunk_result.inserted) assert is_integer(chunk_result.failed) assert is_list(chunk_result.errors) end test "returns {:ok, _} with zero counts for empty chunk" do chunk_rows_with_lines = [] column_map = %{} custom_field_map = %{} opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 0 assert chunk_result.errors == [] end test "function has documentation" do # Check that @doc exists by reading the module assert function_exported?(MemberCSV, :process_chunk, 4) end end describe "module documentation" do test "module has @moduledoc" do # Check that the module exists and has documentation assert Code.ensure_loaded?(MemberCSV) # Try to get the module documentation {:docs_v1, _, _, _, %{"en" => moduledoc}, _, _} = Code.fetch_docs(MemberCSV) assert is_binary(moduledoc) assert String.length(moduledoc) > 0 end end end