feat: add custom boolean field state & URL-Parameter

This commit is contained in:
Simon 2026-01-20 15:55:08 +01:00
parent dbec2d020f
commit 37e1553a02
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
4 changed files with 264 additions and 40 deletions

View file

@ -696,26 +696,26 @@ defmodule MvWeb.MemberLive.IndexTest do
assert state.socket.assigns.boolean_custom_field_filters == %{}
end
test "handle_params parses boolean_filter_<id> values correctly", %{conn: conn} do
test "handle_params parses bf_<id> values correctly", %{conn: conn} do
conn = conn_with_oidc_user(conn)
boolean_field = create_boolean_custom_field()
# Test true value
{:ok, view1, _html} =
live(conn, "/members?boolean_filter_#{boolean_field.id}=true")
live(conn, "/members?bf_#{boolean_field.id}=true")
state1 = :sys.get_state(view1.pid)
filters1 = state1.socket.assigns.boolean_custom_field_filters
assert filters1[boolean_field.id] == :true
assert filters1[boolean_field.id] == true
refute filters1[boolean_field.id] == "true"
# Test false value
{:ok, view2, _html} =
live(conn, "/members?boolean_filter_#{boolean_field.id}=false")
live(conn, "/members?bf_#{boolean_field.id}=false")
state2 = :sys.get_state(view2.pid)
filters2 = state2.socket.assigns.boolean_custom_field_filters
assert filters2[boolean_field.id] == :false
assert filters2[boolean_field.id] == false
refute filters2[boolean_field.id] == "false"
end
@ -724,7 +724,7 @@ defmodule MvWeb.MemberLive.IndexTest do
fake_id = Ecto.UUID.generate()
{:ok, view, _html} =
live(conn, "/members?boolean_filter_#{fake_id}=true")
live(conn, "/members?bf_#{fake_id}=true")
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
@ -739,7 +739,7 @@ defmodule MvWeb.MemberLive.IndexTest do
string_field = create_string_custom_field()
{:ok, view, _html} =
live(conn, "/members?boolean_filter_#{string_field.id}=true")
live(conn, "/members?bf_#{string_field.id}=true")
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
@ -758,7 +758,7 @@ defmodule MvWeb.MemberLive.IndexTest do
for invalid_value <- invalid_values do
{:ok, view, _html} =
live(conn, "/members?boolean_filter_#{boolean_field.id}=#{invalid_value}")
live(conn, "/members?bf_#{boolean_field.id}=#{invalid_value}")
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
@ -777,14 +777,14 @@ defmodule MvWeb.MemberLive.IndexTest do
{:ok, view, _html} =
live(
conn,
"/members?boolean_filter_#{boolean_field1.id}=true&boolean_filter_#{boolean_field2.id}=false"
"/members?bf_#{boolean_field1.id}=true&bf_#{boolean_field2.id}=false"
)
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
assert filters[boolean_field1.id] == :true
assert filters[boolean_field2.id] == :false
assert filters[boolean_field1.id] == true
assert filters[boolean_field2.id] == false
assert map_size(filters) == 2
end
@ -799,7 +799,7 @@ defmodule MvWeb.MemberLive.IndexTest do
{:ok, view1, _html} =
live(
conn,
"/members?boolean_filter_#{boolean_field1.id}=true&boolean_filter_#{boolean_field2.id}=false"
"/members?bf_#{boolean_field1.id}=true&bf_#{boolean_field2.id}=false"
)
# Trigger a search to see if filters are preserved in URL
@ -809,8 +809,8 @@ defmodule MvWeb.MemberLive.IndexTest do
# Check that the patch includes boolean filters
path1 = assert_patch(view1)
assert path1 =~ "boolean_filter_#{boolean_field1.id}=true"
assert path1 =~ "boolean_filter_#{boolean_field2.id}=false"
assert path1 =~ "bf_#{boolean_field1.id}=true"
assert path1 =~ "bf_#{boolean_field2.id}=false"
# Test without filters (nil filters should not appear in URL)
{:ok, view2, _html} = live(conn, "/members")
@ -820,9 +820,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|> element("[data-testid='search-input']")
|> render_change(%{value: "test"})
# Check that no boolean_filter params are in URL
# Check that no bf_ params are in URL
path2 = assert_patch(view2)
refute path2 =~ "boolean_filter_"
refute path2 =~ "bf_"
end
test "boolean filters are preserved during navigation actions", %{conn: conn} do
@ -830,7 +830,7 @@ defmodule MvWeb.MemberLive.IndexTest do
boolean_field = create_boolean_custom_field()
{:ok, view, _html} =
live(conn, "/members?boolean_filter_#{boolean_field.id}=true")
live(conn, "/members?bf_#{boolean_field.id}=true")
# Test sort toggle preserves filter
view
@ -838,7 +838,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|> render_click()
path1 = assert_patch(view)
assert path1 =~ "boolean_filter_#{boolean_field.id}=true"
assert path1 =~ "bf_#{boolean_field.id}=true"
# Test search change preserves filter
view
@ -846,7 +846,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|> render_change(%{value: "test"})
path2 = assert_patch(view)
assert path2 =~ "boolean_filter_#{boolean_field.id}=true"
assert path2 =~ "bf_#{boolean_field.id}=true"
end
test "boolean filters work together with cycle_status_filter", %{conn: conn} do
@ -856,14 +856,14 @@ defmodule MvWeb.MemberLive.IndexTest do
{:ok, view, _html} =
live(
conn,
"/members?cycle_status_filter=paid&boolean_filter_#{boolean_field.id}=true"
"/members?cycle_status_filter=paid&bf_#{boolean_field.id}=true"
)
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
# Both filters should be set
assert filters[boolean_field.id] == :true
assert filters[boolean_field.id] == true
assert state.socket.assigns.cycle_status_filter == :paid
# Both should be in URL when triggering search
@ -873,7 +873,7 @@ defmodule MvWeb.MemberLive.IndexTest do
path = assert_patch(view)
assert path =~ "cycle_status_filter=paid"
assert path =~ "boolean_filter_#{boolean_field.id}=true"
assert path =~ "bf_#{boolean_field.id}=true"
end
test "handle_params removes filter when custom field is deleted", %{conn: conn} do
@ -882,18 +882,18 @@ defmodule MvWeb.MemberLive.IndexTest do
# Set up filter via URL
{:ok, view, _html} =
live(conn, "/members?boolean_filter_#{boolean_field.id}=true")
live(conn, "/members?bf_#{boolean_field.id}=true")
state_before = :sys.get_state(view.pid)
filters_before = state_before.socket.assigns.boolean_custom_field_filters
assert filters_before[boolean_field.id] == :true
assert filters_before[boolean_field.id] == true
# Delete the custom field
Ash.destroy!(boolean_field)
# Navigate again - filter should be removed since custom field no longer exists
{:ok, view2, _html} =
live(conn, "/members?boolean_filter_#{boolean_field.id}=true")
live(conn, "/members?bf_#{boolean_field.id}=true")
state_after = :sys.get_state(view2.pid)
filters_after = state_after.socket.assigns.boolean_custom_field_filters
@ -911,14 +911,77 @@ defmodule MvWeb.MemberLive.IndexTest do
encoded_id = URI.encode(boolean_field.id)
{:ok, view, _html} =
live(conn, "/members?boolean_filter_#{encoded_id}=true")
live(conn, "/members?bf_#{encoded_id}=true")
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
# Filter should work with URL-encoded ID
# Phoenix should decode it automatically, so we check with original ID
assert filters[boolean_field.id] == :true
assert filters[boolean_field.id] == true
end
test "handle_params ignores malformed prefix (bf_bf_<uuid>)", %{conn: conn} do
conn = conn_with_oidc_user(conn)
boolean_field = create_boolean_custom_field()
# Try to send parameter with double prefix
{:ok, view, _html} =
live(conn, "/members?bf_bf_#{boolean_field.id}=true")
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
# Should not parse as valid filter (UUID validation should fail)
refute Map.has_key?(filters, boolean_field.id)
assert filters == %{}
end
test "handle_params limits number of boolean filters to prevent DoS", %{conn: conn} do
conn = conn_with_oidc_user(conn)
# Create 60 boolean custom fields (more than the limit)
boolean_fields = Enum.map(1..60, fn _ -> create_boolean_custom_field() end)
# Build URL with all 60 filters
filter_params =
boolean_fields
|> Enum.map(fn cf -> "bf_#{cf.id}=true" end)
|> Enum.join("&")
{:ok, view, _html} = live(conn, "/members?#{filter_params}")
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
# Should limit to maximum 50 filters
assert map_size(filters) <= 50
# All filters in the result should be valid
Enum.each(filters, fn {id, value} ->
assert value in [true, false]
# Verify the ID corresponds to one of our boolean fields
assert id in Enum.map(boolean_fields, &to_string(&1.id))
end)
end
test "handle_params ignores extremely long custom field IDs", %{conn: conn} do
conn = conn_with_oidc_user(conn)
boolean_field = create_boolean_custom_field()
# Create a fake ID that's way too long (UUIDs are max 36 chars)
fake_long_id = String.duplicate("a", 100)
{:ok, view, _html} =
live(conn, "/members?bf_#{fake_long_id}=true")
state = :sys.get_state(view.pid)
filters = state.socket.assigns.boolean_custom_field_filters
# Should not accept the extremely long ID
refute Map.has_key?(filters, fake_long_id)
# Valid boolean field should still work
refute Map.has_key?(filters, boolean_field.id)
assert filters == %{}
end
end
end