356 lines
10 KiB
Elixir
356 lines
10 KiB
Elixir
defmodule Mv.Membership.MemberExport.BuildTest do
|
|
@moduledoc """
|
|
Tests for MemberExport.Build module.
|
|
|
|
Tests verify that the module correctly:
|
|
- Loads and filters members based on query/selected_ids
|
|
- Builds column specifications (without labels)
|
|
- Generates row data as cell strings
|
|
- Handles member fields, custom fields, and computed fields
|
|
- Applies sorting and filtering consistently
|
|
"""
|
|
use Mv.DataCase, async: true
|
|
|
|
alias Mv.Fixtures
|
|
alias Mv.Membership.{CustomField, Member, MemberExport.Build}
|
|
alias Mv.Constants
|
|
|
|
@custom_field_prefix Constants.custom_field_prefix()
|
|
|
|
setup do
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
|
|
# Create test members
|
|
member1 =
|
|
Fixtures.member_fixture(%{
|
|
first_name: "Alice",
|
|
last_name: "Anderson",
|
|
email: "alice@example.com"
|
|
})
|
|
|
|
member2 =
|
|
Fixtures.member_fixture(%{
|
|
first_name: "Bob",
|
|
last_name: "Brown",
|
|
email: "bob@example.com"
|
|
})
|
|
|
|
%{actor: system_actor, member1: member1, member2: member2}
|
|
end
|
|
|
|
describe "build/3 - standard member fields" do
|
|
test "returns columns and rows for standard member fields", %{
|
|
actor: actor,
|
|
member1: m1,
|
|
member2: m2
|
|
} do
|
|
parsed = %{
|
|
selected_ids: [m1.id, m2.id],
|
|
member_fields: ["first_name", "last_name", "email"],
|
|
selectable_member_fields: ["first_name", "last_name", "email"],
|
|
computed_fields: [],
|
|
custom_field_ids: [],
|
|
query: nil,
|
|
sort_field: nil,
|
|
sort_order: nil,
|
|
show_current_cycle: false
|
|
}
|
|
|
|
result = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
assert {:ok, data} = result
|
|
assert %{columns: columns, rows: rows, meta: meta} = data
|
|
|
|
# Check columns structure
|
|
assert length(columns) == 3
|
|
first_name_col = Enum.find(columns, &(&1.key == "first_name" && &1.kind == :member_field))
|
|
assert first_name_col
|
|
assert first_name_col.label == "Label"
|
|
assert Enum.find(columns, &(&1.key == "last_name" && &1.kind == :member_field))
|
|
assert Enum.find(columns, &(&1.key == "email" && &1.kind == :member_field))
|
|
|
|
# Check rows - should have 2 members
|
|
assert length(rows) == 2
|
|
|
|
# Check first row (member1)
|
|
row1 = Enum.at(rows, 0)
|
|
assert length(row1) == 3
|
|
assert "Alice" in row1
|
|
assert "Anderson" in row1
|
|
assert "alice@example.com" in row1
|
|
|
|
# Check meta
|
|
assert %{generated_at: _timestamp, member_count: 2} = meta
|
|
assert is_binary(meta.generated_at)
|
|
end
|
|
|
|
test "filters members by selected_ids", %{actor: actor, member1: m1, member2: _m2} do
|
|
parsed = %{
|
|
selected_ids: [m1.id],
|
|
member_fields: ["first_name"],
|
|
selectable_member_fields: ["first_name"],
|
|
computed_fields: [],
|
|
custom_field_ids: [],
|
|
query: nil,
|
|
sort_field: nil,
|
|
sort_order: nil,
|
|
show_current_cycle: false
|
|
}
|
|
|
|
{:ok, data} = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
assert length(data.rows) == 1
|
|
row = hd(data.rows)
|
|
assert "Alice" in row
|
|
assert data.meta.member_count == 1
|
|
end
|
|
|
|
test "applies search query when selected_ids is empty", %{
|
|
actor: actor,
|
|
member1: m1,
|
|
member2: _m2
|
|
} do
|
|
parsed = %{
|
|
selected_ids: [],
|
|
member_fields: ["first_name", "last_name"],
|
|
selectable_member_fields: ["first_name", "last_name"],
|
|
computed_fields: [],
|
|
custom_field_ids: [],
|
|
query: "Alice",
|
|
sort_field: nil,
|
|
sort_order: nil,
|
|
show_current_cycle: false
|
|
}
|
|
|
|
{:ok, data} = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
assert length(data.rows) == 1
|
|
row = hd(data.rows)
|
|
assert "Alice" in row
|
|
end
|
|
end
|
|
|
|
describe "build/3 - custom fields" do
|
|
test "includes custom field columns and values", %{
|
|
actor: actor,
|
|
member1: m1
|
|
} do
|
|
# Create custom field
|
|
{:ok, custom_field} =
|
|
CustomField
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
name: "Membership Number",
|
|
value_type: :string
|
|
})
|
|
|> Ash.create(actor: actor)
|
|
|
|
# Create custom field value for member
|
|
{:ok, _cfv} =
|
|
Mv.Membership.CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: m1.id,
|
|
custom_field_id: custom_field.id,
|
|
value: "M12345"
|
|
})
|
|
|> Ash.create(actor: actor)
|
|
|
|
parsed = %{
|
|
selected_ids: [m1.id],
|
|
member_fields: ["first_name"],
|
|
selectable_member_fields: ["first_name"],
|
|
computed_fields: [],
|
|
custom_field_ids: [custom_field.id],
|
|
query: nil,
|
|
sort_field: nil,
|
|
sort_order: nil,
|
|
show_current_cycle: false
|
|
}
|
|
|
|
{:ok, data} = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
# Should have 2 columns: first_name + custom field
|
|
assert length(data.columns) == 2
|
|
|
|
custom_col =
|
|
Enum.find(
|
|
data.columns,
|
|
&(&1.kind == :custom_field && &1.key == to_string(custom_field.id))
|
|
)
|
|
|
|
assert custom_col
|
|
assert custom_col.custom_field.id == custom_field.id
|
|
assert custom_col.label == "Label"
|
|
|
|
# Check row has custom field value
|
|
row = hd(data.rows)
|
|
assert length(row) == 2
|
|
assert "M12345" in row
|
|
end
|
|
|
|
test "handles members without custom field values", %{
|
|
actor: actor,
|
|
member1: m1
|
|
} do
|
|
# Create custom field but no value for member
|
|
{:ok, custom_field} =
|
|
CustomField
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
name: "Optional Field",
|
|
value_type: :string
|
|
})
|
|
|> Ash.create(actor: actor)
|
|
|
|
parsed = %{
|
|
selected_ids: [m1.id],
|
|
member_fields: ["first_name"],
|
|
selectable_member_fields: ["first_name"],
|
|
computed_fields: [],
|
|
custom_field_ids: [custom_field.id],
|
|
query: nil,
|
|
sort_field: nil,
|
|
sort_order: nil,
|
|
show_current_cycle: false
|
|
}
|
|
|
|
{:ok, data} = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
row = hd(data.rows)
|
|
# Custom field value should be empty string
|
|
assert "" in row
|
|
end
|
|
end
|
|
|
|
describe "build/3 - computed fields" do
|
|
test "includes computed field columns and values", %{
|
|
actor: actor,
|
|
member1: m1
|
|
} do
|
|
parsed = %{
|
|
selected_ids: [m1.id],
|
|
member_fields: ["first_name", "membership_fee_status"],
|
|
selectable_member_fields: ["first_name"],
|
|
computed_fields: ["membership_fee_status"],
|
|
custom_field_ids: [],
|
|
query: nil,
|
|
sort_field: nil,
|
|
sort_order: nil,
|
|
show_current_cycle: false
|
|
}
|
|
|
|
{:ok, data} = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
# Should have 2 columns: first_name + computed field
|
|
assert length(data.columns) == 2
|
|
|
|
computed_col =
|
|
Enum.find(data.columns, &(&1.kind == :computed && &1.key == :membership_fee_status))
|
|
|
|
assert computed_col
|
|
assert computed_col.label == "Label"
|
|
|
|
# Check row has computed field value (may be empty if no cycles)
|
|
row = hd(data.rows)
|
|
assert length(row) == 2
|
|
# membership_fee_status should be present (even if empty)
|
|
end
|
|
end
|
|
|
|
describe "build/3 - sorting" do
|
|
test "sorts by member field", %{actor: actor, member1: m1, member2: m2} do
|
|
parsed = %{
|
|
selected_ids: [m1.id, m2.id],
|
|
member_fields: ["first_name"],
|
|
selectable_member_fields: ["first_name"],
|
|
computed_fields: [],
|
|
custom_field_ids: [],
|
|
query: nil,
|
|
sort_field: "first_name",
|
|
sort_order: "asc",
|
|
show_current_cycle: false
|
|
}
|
|
|
|
{:ok, data} = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
# Should be sorted: Alice, Bob
|
|
[row1, row2] = data.rows
|
|
assert "Alice" in row1
|
|
assert "Bob" in row2
|
|
end
|
|
|
|
test "sorts by custom field", %{actor: actor, member1: m1, member2: m2} do
|
|
# Create custom field
|
|
{:ok, custom_field} =
|
|
CustomField
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
name: "Sort Field",
|
|
value_type: :string
|
|
})
|
|
|> Ash.create(actor: actor)
|
|
|
|
# Add values: m1="Zebra", m2="Alpha"
|
|
{:ok, _cfv1} =
|
|
Mv.Membership.CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: m1.id,
|
|
custom_field_id: custom_field.id,
|
|
value: "Zebra"
|
|
})
|
|
|> Ash.create(actor: actor)
|
|
|
|
{:ok, _cfv2} =
|
|
Mv.Membership.CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: m2.id,
|
|
custom_field_id: custom_field.id,
|
|
value: "Alpha"
|
|
})
|
|
|> Ash.create(actor: actor)
|
|
|
|
sort_field = "#{@custom_field_prefix}#{custom_field.id}"
|
|
|
|
parsed = %{
|
|
selected_ids: [m1.id, m2.id],
|
|
member_fields: ["first_name"],
|
|
selectable_member_fields: ["first_name"],
|
|
computed_fields: [],
|
|
custom_field_ids: [custom_field.id],
|
|
query: nil,
|
|
sort_field: sort_field,
|
|
sort_order: "asc",
|
|
show_current_cycle: false
|
|
}
|
|
|
|
{:ok, data} = Build.build(actor, parsed, fn _key -> "Label" end)
|
|
|
|
# Should be sorted by custom field: Alpha (Bob), Zebra (Alice)
|
|
[row1, row2] = data.rows
|
|
# Alpha
|
|
assert "Bob" in row1
|
|
# Zebra
|
|
assert "Alice" in row2
|
|
end
|
|
end
|
|
|
|
describe "build/3 - error handling" do
|
|
test "returns error when actor lacks permission", %{member1: m1} do
|
|
# Create a user with limited permissions
|
|
user = Fixtures.password_user_with_role_fixture(%{permission_set_name: "own_data"})
|
|
|
|
parsed = %{
|
|
selected_ids: [m1.id],
|
|
member_fields: ["first_name"],
|
|
selectable_member_fields: ["first_name"],
|
|
computed_fields: [],
|
|
custom_field_ids: [],
|
|
query: nil,
|
|
sort_field: nil,
|
|
sort_order: nil,
|
|
show_current_cycle: false
|
|
}
|
|
|
|
result = Build.build(user, parsed, fn _key -> "Label" end)
|
|
|
|
assert {:error, :forbidden} = result
|
|
end
|
|
end
|
|
end
|