feat: custom field deletion
This commit is contained in:
parent
21ec86839a
commit
2af23f4042
11 changed files with 938 additions and 16 deletions
254
test/membership/custom_field_deletion_test.exs
Normal file
254
test/membership/custom_field_deletion_test.exs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
defmodule Mv.Membership.CustomFieldDeletionTest do
|
||||
@moduledoc """
|
||||
Tests for CustomField deletion with CASCADE behavior.
|
||||
|
||||
Tests cover:
|
||||
- Deletion of custom fields without assigned values
|
||||
- Deletion of custom fields with assigned values (CASCADE)
|
||||
- assigned_members_count calculation
|
||||
- prepare_deletion action with count loading
|
||||
- CASCADE deletion only affects specific custom field values
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
describe "assigned_members_count calculation" do
|
||||
test "returns 0 for custom field without any values" do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
assert custom_field_with_count.assigned_members_count == 0
|
||||
end
|
||||
|
||||
test "returns correct count for custom field with one member" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, _custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
assert custom_field_with_count.assigned_members_count == 1
|
||||
end
|
||||
|
||||
test "returns correct count for custom field with multiple members" do
|
||||
{:ok, member1} = create_member()
|
||||
{:ok, member2} = create_member()
|
||||
{:ok, member3} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
# Create custom field value for each member
|
||||
for member <- [member1, member2, member3] do
|
||||
{:ok, _} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
end
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
assert custom_field_with_count.assigned_members_count == 3
|
||||
end
|
||||
|
||||
test "counts distinct members (not multiple values per member)" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
# Create custom field value for member
|
||||
{:ok, _} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
|
||||
# Should still be 1, not 2, even if we tried to create multiple (which would fail due to uniqueness)
|
||||
assert custom_field_with_count.assigned_members_count == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "prepare_deletion action" do
|
||||
test "loads assigned_members_count for deletion preparation" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, _} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Use prepare_deletion action
|
||||
[prepared_custom_field] =
|
||||
CustomField
|
||||
|> Ash.Query.for_read(:prepare_deletion, %{id: custom_field.id})
|
||||
|> Ash.read!()
|
||||
|
||||
assert prepared_custom_field.assigned_members_count == 1
|
||||
assert prepared_custom_field.id == custom_field.id
|
||||
end
|
||||
|
||||
test "returns empty list for non-existent custom field" do
|
||||
non_existent_id = Ash.UUID.generate()
|
||||
|
||||
result =
|
||||
CustomField
|
||||
|> Ash.Query.for_read(:prepare_deletion, %{id: non_existent_id})
|
||||
|> Ash.read!()
|
||||
|
||||
assert result == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "destroy_with_values action" do
|
||||
test "deletes custom field without any values" do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
|
||||
# Verify custom field is deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
end
|
||||
|
||||
test "deletes custom field and cascades to all its values" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Delete custom field
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
|
||||
# Verify custom field is deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
|
||||
# Verify custom field value is also deleted (CASCADE)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
|
||||
|
||||
# Verify member still exists
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
end
|
||||
|
||||
test "deletes only values of the specific custom field" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field1} = create_custom_field("field1", :string)
|
||||
{:ok, custom_field2} = create_custom_field("field2", :string)
|
||||
|
||||
# Create value for custom_field1
|
||||
{:ok, value1} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field1.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "value1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create value for custom_field2
|
||||
{:ok, value2} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field2.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "value2"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Delete custom_field1
|
||||
assert :ok = Ash.destroy(custom_field1)
|
||||
|
||||
# Verify custom_field1 and value1 are deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field1.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value1.id)
|
||||
|
||||
# Verify custom_field2 and value2 still exist
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field2.id)
|
||||
assert {:ok, _} = Ash.get(CustomFieldValue, value2.id)
|
||||
end
|
||||
|
||||
test "deletes custom field with values from multiple members" do
|
||||
{:ok, member1} = create_member()
|
||||
{:ok, member2} = create_member()
|
||||
{:ok, member3} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
# Create value for each member
|
||||
values =
|
||||
for member <- [member1, member2, member3] do
|
||||
{:ok, value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
# Delete custom field
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
|
||||
# Verify all values are deleted
|
||||
for value <- values do
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value.id)
|
||||
end
|
||||
|
||||
# Verify all members still exist
|
||||
for member <- [member1, member2, member3] do
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
defp create_member do
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "User#{System.unique_integer([:positive])}",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
end
|
||||
|
||||
defp create_custom_field(name, value_type) do
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "#{name}_#{System.unique_integer([:positive])}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
end
|
||||
end
|
||||
251
test/mv_web/live/custom_field_live/deletion_test.exs
Normal file
251
test/mv_web/live/custom_field_live/deletion_test.exs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||
@moduledoc """
|
||||
Tests for CustomFieldLive.Index deletion modal and slug confirmation.
|
||||
|
||||
Tests cover:
|
||||
- Opening deletion confirmation modal
|
||||
- Displaying correct member count
|
||||
- Slug confirmation input
|
||||
- Successful deletion with correct slug
|
||||
- Failed deletion with incorrect slug
|
||||
- Canceling deletion
|
||||
- Button states (enabled/disabled based on slug match)
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
# Create admin user for testing
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
conn = log_in_user(build_conn(), user)
|
||||
%{conn: conn, user: user}
|
||||
end
|
||||
|
||||
describe "delete button and modal" do
|
||||
test "opens modal with correct member count when delete is clicked", %{conn: conn} do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
# Create custom field value
|
||||
create_custom_field_value(member, custom_field, "test")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
# Click delete button
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Modal should be visible
|
||||
assert has_element?(view, "#delete-custom-field-modal")
|
||||
|
||||
# Should show correct member count (1 member)
|
||||
assert render(view) =~ "1 member has a value assigned for this custom field"
|
||||
|
||||
# Should show the slug
|
||||
assert render(view) =~ custom_field.slug
|
||||
end
|
||||
|
||||
test "shows correct plural form for multiple members", %{conn: conn} do
|
||||
{:ok, member1} = create_member()
|
||||
{:ok, member2} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
# Create values for both members
|
||||
create_custom_field_value(member1, custom_field, "test1")
|
||||
create_custom_field_value(member2, custom_field, "test2")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Should show plural form
|
||||
assert render(view) =~ "2 members have values assigned for this custom field"
|
||||
end
|
||||
|
||||
test "shows 0 members for custom field without values", %{conn: conn} do
|
||||
{:ok, _custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Should show 0 members
|
||||
assert render(view) =~ "0 members have values assigned for this custom field"
|
||||
end
|
||||
end
|
||||
|
||||
describe "slug confirmation input" do
|
||||
test "updates confirmation state when typing", %{conn: conn} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Type in slug input
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug})
|
||||
|
||||
# Confirm button should be enabled now (no disabled attribute)
|
||||
html = render(view)
|
||||
refute html =~ ~r/disabled(?:=""|(?!\w))/
|
||||
end
|
||||
|
||||
test "delete button is disabled when slug doesn't match", %{conn: conn} do
|
||||
{:ok, _custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Type wrong slug
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"})
|
||||
|
||||
# Button should be disabled
|
||||
html = render(view)
|
||||
assert html =~ ~r/disabled(?:=""|(?!\w))/
|
||||
end
|
||||
end
|
||||
|
||||
describe "confirm deletion" do
|
||||
test "successfully deletes custom field with correct slug", %{conn: conn} do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
{:ok, custom_field_value} = create_custom_field_value(member, custom_field, "test")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
# Open modal
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Enter correct slug
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug})
|
||||
|
||||
# Click confirm
|
||||
view
|
||||
|> element("button", "Delete Custom Field and All Values")
|
||||
|> render_click()
|
||||
|
||||
# Should show success message
|
||||
assert render(view) =~ "Custom field deleted successfully"
|
||||
|
||||
# Custom field should be gone from database
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
|
||||
# Custom field value should also be gone (CASCADE)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
|
||||
|
||||
# Member should still exist
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
end
|
||||
|
||||
test "shows error when slug doesn't match", %{conn: conn} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Enter wrong slug
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"})
|
||||
|
||||
# Try to confirm (button should be disabled, but test the handler anyway)
|
||||
view
|
||||
|> render_click("confirm_delete", %{})
|
||||
|
||||
# Should show error message
|
||||
assert render(view) =~ "Slug does not match"
|
||||
|
||||
# Custom field should still exist
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "cancel deletion" do
|
||||
test "closes modal without deleting", %{conn: conn} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Modal should be visible
|
||||
assert has_element?(view, "#delete-custom-field-modal")
|
||||
|
||||
# Click cancel
|
||||
view
|
||||
|> element("button", "Cancel")
|
||||
|> render_click()
|
||||
|
||||
# Modal should be gone
|
||||
refute has_element?(view, "#delete-custom-field-modal")
|
||||
|
||||
# Custom field should still exist
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
defp create_member do
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "User#{System.unique_integer([:positive])}",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
end
|
||||
|
||||
defp create_custom_field(name, value_type) do
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "#{name}_#{System.unique_integer([:positive])}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
end
|
||||
|
||||
defp create_custom_field_value(member, custom_field, value) do
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => value}
|
||||
})
|
||||
|> Ash.create()
|
||||
end
|
||||
|
||||
defp log_in_user(conn, user) do
|
||||
conn
|
||||
|> Phoenix.ConnTest.init_test_session(%{})
|
||||
|> AshAuthentication.Plug.Helpers.store_in_session(user)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue