test: add tdd tests for custom boolean field filter logic
Some checks reported errors
continuous-integration/drone/push Build was killed
Some checks reported errors
continuous-integration/drone/push Build was killed
This commit is contained in:
parent
37e1553a02
commit
d65da2f498
1 changed files with 469 additions and 43 deletions
|
|
@ -3,6 +3,49 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
|
||||
# Helper to create a membership fee type (shared across all tests)
|
||||
defp create_fee_type(attrs) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
# Helper to create a cycle (shared across all tests)
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
# Delete any auto-generated cycles first to avoid conflicts
|
||||
existing_cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
test "shows translated title in German", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||||
|
|
@ -457,24 +500,6 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
end
|
||||
|
||||
describe "cycle status filter" do
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
default_attrs = %{
|
||||
|
|
@ -490,31 +515,6 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|> Ash.create!()
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
# Delete any auto-generated cycles first to avoid conflicts
|
||||
existing_cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
test "filter shows only members with paid status in last cycle", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
|
@ -983,5 +983,431 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
refute Map.has_key?(filters, boolean_field.id)
|
||||
assert filters == %{}
|
||||
end
|
||||
|
||||
# Helper to create a member with a boolean custom field value
|
||||
defp create_member_with_boolean_value(member_attrs \\ %{}, custom_field, value) do
|
||||
{:ok, member} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||
} |> Map.merge(member_attrs))
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => value}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Reload member with custom field values
|
||||
member
|
||||
|> Ash.load!(:custom_field_values)
|
||||
end
|
||||
|
||||
# Tests for get_boolean_custom_field_value/2
|
||||
test "get_boolean_custom_field_value extracts true from Ash.Union format", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
member = create_member_with_boolean_value(%{}, boolean_field, true)
|
||||
|
||||
# Test the function (will fail until implemented)
|
||||
result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field)
|
||||
|
||||
assert result == true
|
||||
end
|
||||
|
||||
test "get_boolean_custom_field_value extracts false from Ash.Union format", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
member = create_member_with_boolean_value(%{}, boolean_field, false)
|
||||
|
||||
result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field)
|
||||
|
||||
assert result == false
|
||||
end
|
||||
|
||||
test "get_boolean_custom_field_value extracts true from map format with type and value keys", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
{:ok, member} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create CustomFieldValue with map format
|
||||
{:ok, _cfv} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: boolean_field.id,
|
||||
value: %{"type" => "boolean", "value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Reload member with custom field values
|
||||
member = member |> Ash.load!(:custom_field_values)
|
||||
|
||||
result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field)
|
||||
|
||||
assert result == true
|
||||
end
|
||||
|
||||
test "get_boolean_custom_field_value returns nil when no CustomFieldValue exists", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
{:ok, member} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Member has no custom field value for this field
|
||||
member = member |> Ash.load!(:custom_field_values)
|
||||
|
||||
result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field)
|
||||
|
||||
assert result == nil
|
||||
end
|
||||
|
||||
test "get_boolean_custom_field_value returns nil when CustomFieldValue has nil value", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
{:ok, member} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create CustomFieldValue with nil value (edge case)
|
||||
{:ok, _cfv} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: boolean_field.id,
|
||||
value: nil
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
member = member |> Ash.load!(:custom_field_values)
|
||||
|
||||
result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field)
|
||||
|
||||
assert result == nil
|
||||
end
|
||||
|
||||
test "get_boolean_custom_field_value returns nil for non-boolean CustomFieldValue", %{conn: _conn} do
|
||||
string_field = create_string_custom_field()
|
||||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
{:ok, member} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create string custom field value (not boolean)
|
||||
{:ok, _cfv} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
member = member |> Ash.load!(:custom_field_values)
|
||||
|
||||
# Try to get boolean value from string field - should return nil
|
||||
result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field)
|
||||
|
||||
assert result == nil
|
||||
end
|
||||
|
||||
# Tests for apply_boolean_custom_field_filters/2
|
||||
test "apply_boolean_custom_field_filters filters members with true value and excludes false/without values", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
member_with_true = create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true)
|
||||
member_with_false = create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false)
|
||||
{:ok, member_without_value} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "NoValue",
|
||||
last_name: "Member",
|
||||
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
member_without_value = member_without_value |> Ash.load!(:custom_field_values)
|
||||
|
||||
members = [member_with_true, member_with_false, member_without_value]
|
||||
filters = %{to_string(boolean_field.id) => true}
|
||||
|
||||
result = MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(members, filters)
|
||||
|
||||
assert length(result) == 1
|
||||
assert List.first(result).id == member_with_true.id
|
||||
refute Enum.any?(result, &(&1.id == member_with_false.id))
|
||||
refute Enum.any?(result, &(&1.id == member_without_value.id))
|
||||
end
|
||||
|
||||
test "apply_boolean_custom_field_filters filters members with false value and excludes true/without values", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
member_with_true = create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true)
|
||||
member_with_false = create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false)
|
||||
{:ok, member_without_value} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "NoValue",
|
||||
last_name: "Member",
|
||||
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
member_without_value = member_without_value |> Ash.load!(:custom_field_values)
|
||||
|
||||
members = [member_with_true, member_with_false, member_without_value]
|
||||
filters = %{to_string(boolean_field.id) => false}
|
||||
|
||||
result = MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(members, filters)
|
||||
|
||||
assert length(result) == 1
|
||||
assert List.first(result).id == member_with_false.id
|
||||
refute Enum.any?(result, &(&1.id == member_with_true.id))
|
||||
refute Enum.any?(result, &(&1.id == member_without_value.id))
|
||||
end
|
||||
|
||||
test "apply_boolean_custom_field_filters returns all members when filter map is empty", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
member1 = create_member_with_boolean_value(%{first_name: "Member1"}, boolean_field, true)
|
||||
member2 = create_member_with_boolean_value(%{first_name: "Member2"}, boolean_field, false)
|
||||
|
||||
members = [member1, member2]
|
||||
filters = %{}
|
||||
|
||||
result = MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(members, filters)
|
||||
|
||||
assert length(result) == 2
|
||||
assert Enum.all?([member1.id, member2.id], fn id ->
|
||||
Enum.any?(result, &(&1.id == id))
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
test "apply_boolean_custom_field_filters applies multiple filters with AND logic", %{conn: _conn} do
|
||||
boolean_field1 = create_boolean_custom_field(%{name: "Field1"})
|
||||
boolean_field2 = create_boolean_custom_field(%{name: "Field2"})
|
||||
|
||||
# Member with both fields = true
|
||||
{:ok, member_both_true} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "BothTrue",
|
||||
last_name: "Member",
|
||||
email: "bothtrue.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv1} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_both_true.id,
|
||||
custom_field_id: boolean_field1.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_both_true.id,
|
||||
custom_field_id: boolean_field2.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
member_both_true = member_both_true |> Ash.load!(:custom_field_values)
|
||||
|
||||
# Member with field1 = true, field2 = false
|
||||
{:ok, member_mixed} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Mixed",
|
||||
last_name: "Member",
|
||||
email: "mixed.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv3} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_mixed.id,
|
||||
custom_field_id: boolean_field1.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv4} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_mixed.id,
|
||||
custom_field_id: boolean_field2.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => false}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
member_mixed = member_mixed |> Ash.load!(:custom_field_values)
|
||||
|
||||
members = [member_both_true, member_mixed]
|
||||
filters = %{
|
||||
to_string(boolean_field1.id) => true,
|
||||
to_string(boolean_field2.id) => true
|
||||
}
|
||||
|
||||
result = MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(members, filters)
|
||||
|
||||
# Only member_both_true should match (both fields = true)
|
||||
assert length(result) == 1
|
||||
assert List.first(result).id == member_both_true.id
|
||||
end
|
||||
|
||||
test "apply_boolean_custom_field_filters ignores filter with non-existent custom field ID", %{conn: _conn} do
|
||||
boolean_field = create_boolean_custom_field()
|
||||
fake_id = Ecto.UUID.generate()
|
||||
|
||||
member = create_member_with_boolean_value(%{first_name: "Member"}, boolean_field, true)
|
||||
|
||||
members = [member]
|
||||
filters = %{fake_id => true}
|
||||
|
||||
result = MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(members, filters)
|
||||
|
||||
# Should return all members since fake_id doesn't match any custom field
|
||||
assert length(result) == 1
|
||||
end
|
||||
|
||||
# Integration tests for boolean custom field filters in load_members
|
||||
test "boolean filter integration filters members by boolean custom field value via URL parameter", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
member_with_true = create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true)
|
||||
member_with_false = create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false)
|
||||
{:ok, member_without_value} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "NoValue",
|
||||
last_name: "Member",
|
||||
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Test true filter
|
||||
{:ok, _view, html_true} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
|
||||
assert html_true =~ "TrueMember"
|
||||
refute html_true =~ "FalseMember"
|
||||
refute html_true =~ "NoValue"
|
||||
|
||||
# Test false filter
|
||||
{:ok, _view, html_false} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=false")
|
||||
|
||||
assert html_false =~ "FalseMember"
|
||||
refute html_false =~ "TrueMember"
|
||||
refute html_false =~ "NoValue"
|
||||
end
|
||||
|
||||
test "boolean filter integration works together with cycle_status_filter", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field()
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||
|
||||
# Member with true boolean value and paid status
|
||||
{:ok, member_paid_true} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "PaidTrue",
|
||||
last_name: "Member",
|
||||
email: "paidtrue.member.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_paid_true.id,
|
||||
custom_field_id: boolean_field.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
create_cycle(member_paid_true, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||
|
||||
# Member with true boolean value but unpaid status
|
||||
{:ok, member_unpaid_true} =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "UnpaidTrue",
|
||||
last_name: "Member",
|
||||
email: "unpaidtrue.member.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_unpaid_true.id,
|
||||
custom_field_id: boolean_field.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
create_cycle(member_unpaid_true, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||
|
||||
# Test both filters together
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?cycle_status_filter=paid&bf_#{boolean_field.id}=true")
|
||||
|
||||
# Only member_paid_true should match both filters
|
||||
assert html =~ "PaidTrue"
|
||||
refute html =~ "UnpaidTrue"
|
||||
end
|
||||
|
||||
test "boolean filter integration works together with search query", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
member_with_true = create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true)
|
||||
member_with_false = create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false)
|
||||
|
||||
# Test search + boolean filter
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?query=TrueMember&bf_#{boolean_field.id}=true")
|
||||
|
||||
# Only member_with_true should match both search and filter
|
||||
assert html =~ "TrueMember"
|
||||
refute html =~ "FalseMember"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue