Implements custom field CSV import closes #338 #395
2 changed files with 315 additions and 24 deletions
14
test/mv/config_test.exs
Normal file
14
test/mv/config_test.exs
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue