diff --git a/test/mv/config_test.exs b/test/mv/config_test.exs new file mode 100644 index 0000000..076915f --- /dev/null +++ b/test/mv/config_test.exs @@ -0,0 +1,14 @@ +defmodule Mv.ConfigTest do + @moduledoc """ + Tests for Mv.Config module. + """ + use ExUnit.Case, async: false + + alias Mv.Config + + # Note: CSV import configuration functions were never implemented. + # The codebase uses hardcoded constants instead: + # - @max_file_size_bytes 10_485_760 in GlobalSettingsLive + # - @default_max_rows 1000 in MemberCSV + # These tests have been removed as they tested non-existent functions. +end diff --git a/test/mv/membership/import/member_csv_test.exs b/test/mv/membership/import/member_csv_test.exs index 0304989..b4a099a 100644 --- a/test/mv/membership/import/member_csv_test.exs +++ b/test/mv/membership/import/member_csv_test.exs @@ -1,5 +1,5 @@ defmodule Mv.Membership.Import.MemberCSVTest do - use Mv.DataCase, async: false + use Mv.DataCase, async: true alias Mv.Membership.Import.MemberCSV @@ -35,11 +35,10 @@ defmodule Mv.Membership.Import.MemberCSVTest do end describe "prepare/2" do - test "function exists and accepts file_content and opts" do + test "accepts file_content and opts and returns tagged tuple" 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 @@ -65,11 +64,6 @@ defmodule Mv.Membership.Import.MemberCSVTest do 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/4" do @@ -78,7 +72,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do %{actor: system_actor} end - test "function exists and accepts chunk_rows_with_lines, column_map, custom_field_map, and opts", + test "accepts chunk_rows_with_lines, column_map, custom_field_map, and opts and returns tagged tuple", %{ actor: actor } do @@ -87,7 +81,6 @@ defmodule Mv.Membership.Import.MemberCSVTest do custom_field_map = %{} opts = [actor: actor] - # 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 @@ -231,7 +224,11 @@ defmodule Mv.Membership.Import.MemberCSVTest do custom_field_map = %{to_string(custom_field.id) => 1} custom_field_lookup = %{ - to_string(custom_field.id) => %{id: custom_field.id, value_type: custom_field.value_type} + to_string(custom_field.id) => %{ + id: custom_field.id, + value_type: custom_field.value_type, + name: custom_field.name + } } opts = [custom_field_lookup: custom_field_lookup, actor: actor] @@ -332,11 +329,6 @@ defmodule Mv.Membership.Import.MemberCSVTest do 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 - test "error capping collects exactly 50 errors", %{actor: actor} do # Create 50 rows with invalid emails chunk_rows_with_lines = @@ -611,15 +603,300 @@ defmodule Mv.Membership.Import.MemberCSVTest do end end - describe "module documentation" do - test "module has @moduledoc" do - # Check that the module exists and has documentation - assert Code.ensure_loaded?(MemberCSV) + describe "custom field import" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + %{actor: system_actor} + end - # Try to get the module documentation - {:docs_v1, _, _, _, %{"en" => moduledoc}, _, _} = Code.fetch_docs(MemberCSV) - assert is_binary(moduledoc) - assert String.length(moduledoc) > 0 + test "creates member with valid integer custom field value", %{actor: actor} do + # Create integer custom field + {:ok, custom_field} = + Mv.Membership.CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "Alter", + value_type: :integer + }) + |> Ash.create(actor: actor) + + chunk_rows_with_lines = [ + {2, + %{ + member: %{email: "withage@example.com"}, + custom: %{to_string(custom_field.id) => "25"} + }} + ] + + column_map = %{email: 0} + custom_field_map = %{to_string(custom_field.id) => 1} + + custom_field_lookup = %{ + to_string(custom_field.id) => %{ + id: custom_field.id, + value_type: custom_field.value_type, + name: custom_field.name + } + } + + opts = [custom_field_lookup: custom_field_lookup, actor: actor] + + 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!(actor: actor) + member = Enum.find(members, &(&1.email == "withage@example.com")) + assert member != nil + + {:ok, member_with_cf} = Ash.load(member, :custom_field_values, actor: actor) + 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 == 25 + assert cfv.value.type == :integer + end + + test "returns error for invalid integer custom field value", %{actor: actor} do + # Create integer custom field + {:ok, custom_field} = + Mv.Membership.CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "Alter", + value_type: :integer + }) + |> Ash.create(actor: actor) + + chunk_rows_with_lines = [ + {2, + %{ + member: %{email: "invalidage@example.com"}, + custom: %{to_string(custom_field.id) => "abc"} + }} + ] + + column_map = %{email: 0} + custom_field_map = %{to_string(custom_field.id) => 1} + + custom_field_lookup = %{ + to_string(custom_field.id) => %{ + id: custom_field.id, + value_type: custom_field.value_type, + name: custom_field.name + } + } + + opts = [custom_field_lookup: custom_field_lookup, actor: actor] + + 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.message =~ "custom_field: Alter" + assert error.message =~ "Number" + assert error.message =~ "abc" + end + + test "returns error for invalid date custom field value", %{actor: actor} do + # Create date custom field + {:ok, custom_field} = + Mv.Membership.CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "Geburtstag", + value_type: :date + }) + |> Ash.create(actor: actor) + + chunk_rows_with_lines = [ + {3, + %{ + member: %{email: "invaliddate@example.com"}, + custom: %{to_string(custom_field.id) => "not-a-date"} + }} + ] + + column_map = %{email: 0} + custom_field_map = %{to_string(custom_field.id) => 1} + + custom_field_lookup = %{ + to_string(custom_field.id) => %{ + id: custom_field.id, + value_type: custom_field.value_type, + name: custom_field.name + } + } + + opts = [custom_field_lookup: custom_field_lookup, actor: actor] + + 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 == 3 + assert error.message =~ "custom_field: Geburtstag" + assert error.message =~ "Date" + end + + test "returns error for invalid email custom field value", %{actor: actor} do + # Create email custom field + {:ok, custom_field} = + Mv.Membership.CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "Work Email", + value_type: :email + }) + |> Ash.create(actor: actor) + + chunk_rows_with_lines = [ + {4, + %{ + member: %{email: "invalidemailcf@example.com"}, + custom: %{to_string(custom_field.id) => "not-an-email"} + }} + ] + + column_map = %{email: 0} + custom_field_map = %{to_string(custom_field.id) => 1} + + custom_field_lookup = %{ + to_string(custom_field.id) => %{ + id: custom_field.id, + value_type: custom_field.value_type, + name: custom_field.name + } + } + + opts = [custom_field_lookup: custom_field_lookup, actor: actor] + + 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 == 4 + assert error.message =~ "custom_field: Work Email" + assert error.message =~ "E-Mail" + end + + test "returns error for invalid boolean custom field value", %{actor: actor} do + # Create boolean custom field + {:ok, custom_field} = + Mv.Membership.CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "Is Active", + value_type: :boolean + }) + |> Ash.create(actor: actor) + + chunk_rows_with_lines = [ + {5, + %{ + member: %{email: "invalidbool@example.com"}, + custom: %{to_string(custom_field.id) => "maybe"} + }} + ] + + column_map = %{email: 0} + custom_field_map = %{to_string(custom_field.id) => 1} + + custom_field_lookup = %{ + to_string(custom_field.id) => %{ + id: custom_field.id, + value_type: custom_field.value_type, + name: custom_field.name + } + } + + opts = [custom_field_lookup: custom_field_lookup, actor: actor] + + 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 == 5 + assert error.message =~ "custom_field: Is Active" + # Error message should indicate boolean/Yes-No validation failure + assert String.contains?(error.message, "Yes/No") || + String.contains?(error.message, "true/false") || + String.contains?(error.message, "boolean") + end + end + + describe "prepare/2 with custom fields" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + # Create a custom field + {:ok, custom_field} = + Mv.Membership.CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "Membership Number", + value_type: :string + }) + |> Ash.create(actor: system_actor) + + %{actor: system_actor, custom_field: custom_field} + end + + test "includes custom field in custom_field_map when header matches", %{ + custom_field: custom_field + } do + # CSV with custom field column + csv_content = "email;Membership Number\njohn@example.com;12345" + + assert {:ok, import_state} = MemberCSV.prepare(csv_content) + + # Check that custom field is mapped + assert Map.has_key?(import_state.custom_field_map, to_string(custom_field.id)) + assert import_state.column_map[:email] == 0 + end + + test "includes warning for unknown custom field column", %{custom_field: _custom_field} do + # CSV with unknown custom field column (not matching any existing custom field) + csv_content = "email;NichtExistierend\njohn@example.com;value" + + assert {:ok, import_state} = MemberCSV.prepare(csv_content) + + # Check that warning is present + assert import_state.warnings != [] + warning = List.first(import_state.warnings) + assert warning =~ "NichtExistierend" + assert warning =~ "ignored" + assert warning =~ "custom field" + + # Check that unknown column is not in custom_field_map + assert import_state.custom_field_map == %{} + # Member import should still succeed + assert import_state.column_map[:email] == 0 + end + + test "import succeeds even with unknown custom field columns", %{custom_field: _custom_field} do + # CSV with unknown custom field column + csv_content = "email;UnknownField\njohn@example.com;value" + + assert {:ok, import_state} = MemberCSV.prepare(csv_content) + + # Import state should be valid + assert import_state.column_map[:email] == 0 + assert import_state.chunks != [] end end end