Add actor parameter to all tests requiring authorization
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit adds actor: system_actor to all Ash operations in tests that require authorization.
This commit is contained in:
parent
4c846f8bba
commit
a6cdeaa18d
75 changed files with 4649 additions and 2865 deletions
|
|
@ -13,23 +13,28 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "assigned_members_count calculation" do
|
||||
test "returns 0 for custom field without any values" do
|
||||
test "returns 0 for custom field without any values", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
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)
|
||||
test "returns correct count for custom field with one member", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
{:ok, _custom_field_value} =
|
||||
CustomFieldValue
|
||||
|
|
@ -38,17 +43,17 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
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)
|
||||
test "returns correct count for custom field with multiple members", %{actor: actor} do
|
||||
{:ok, member1} = create_member(actor)
|
||||
{:ok, member2} = create_member(actor)
|
||||
{:ok, member3} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
# Create custom field value for each member
|
||||
for member <- [member1, member2, member3] do
|
||||
|
|
@ -59,16 +64,16 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
end
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
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)
|
||||
test "counts distinct members (not multiple values per member)", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
# Create custom field value for member
|
||||
{:ok, _} =
|
||||
|
|
@ -78,9 +83,9 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
|
||||
# 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
|
||||
|
|
@ -88,9 +93,9 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
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)
|
||||
test "loads assigned_members_count for deletion preparation", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
{:ok, _} =
|
||||
CustomFieldValue
|
||||
|
|
@ -99,43 +104,43 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Use prepare_deletion action
|
||||
[prepared_custom_field] =
|
||||
CustomField
|
||||
|> Ash.Query.for_read(:prepare_deletion, %{id: custom_field.id})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
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
|
||||
test "returns empty list for non-existent custom field", %{actor: actor} do
|
||||
non_existent_id = Ash.UUID.generate()
|
||||
|
||||
result =
|
||||
CustomField
|
||||
|> Ash.Query.for_read(:prepare_deletion, %{id: non_existent_id})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
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)
|
||||
test "deletes custom field without any values", %{actor: actor} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
assert :ok = Ash.destroy(custom_field, actor: actor)
|
||||
|
||||
# Verify custom field is deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: actor)
|
||||
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)
|
||||
test "deletes custom field and cascades to all its values", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
{:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|
|
@ -144,25 +149,25 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Delete custom field
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
assert :ok = Ash.destroy(custom_field, actor: actor)
|
||||
|
||||
# Verify custom field is deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: actor)
|
||||
|
||||
# Verify custom field value is also deleted (CASCADE)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id, actor: actor)
|
||||
|
||||
# Verify member still exists
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
assert {:ok, _} = Ash.get(Member, member.id, actor: actor)
|
||||
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)
|
||||
test "deletes only values of the specific custom field", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field1} = create_custom_field("field1", :string, actor)
|
||||
{:ok, custom_field2} = create_custom_field("field2", :string, actor)
|
||||
|
||||
# Create value for custom_field1
|
||||
{:ok, value1} =
|
||||
|
|
@ -172,7 +177,7 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field1.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "value1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create value for custom_field2
|
||||
{:ok, value2} =
|
||||
|
|
@ -182,25 +187,25 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field2.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "value2"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Delete custom_field1
|
||||
assert :ok = Ash.destroy(custom_field1)
|
||||
assert :ok = Ash.destroy(custom_field1, actor: actor)
|
||||
|
||||
# Verify custom_field1 and value1 are deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field1.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value1.id)
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field1.id, actor: actor)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value1.id, actor: actor)
|
||||
|
||||
# Verify custom_field2 and value2 still exist
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field2.id)
|
||||
assert {:ok, _} = Ash.get(CustomFieldValue, value2.id)
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field2.id, actor: actor)
|
||||
assert {:ok, _} = Ash.get(CustomFieldValue, value2.id, actor: actor)
|
||||
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)
|
||||
test "deletes custom field with values from multiple members", %{actor: actor} do
|
||||
{:ok, member1} = create_member(actor)
|
||||
{:ok, member2} = create_member(actor)
|
||||
{:ok, member3} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
# Create value for each member
|
||||
values =
|
||||
|
|
@ -212,43 +217,43 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
# Delete custom field
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
assert :ok = Ash.destroy(custom_field, actor: actor)
|
||||
|
||||
# Verify all values are deleted
|
||||
for value <- values do
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value.id, actor: actor)
|
||||
end
|
||||
|
||||
# Verify all members still exist
|
||||
for member <- [member1, member2, member3] do
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
assert {:ok, _} = Ash.get(Member, member.id, actor: actor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
defp create_member do
|
||||
defp create_member(actor) 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()
|
||||
|> Ash.create(actor: actor)
|
||||
end
|
||||
|
||||
defp create_custom_field(name, value_type) do
|
||||
defp create_custom_field(name, value_type, actor) do
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "#{name}_#{System.unique_integer([:positive])}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,8 +12,13 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "show_in_overview attribute" do
|
||||
test "creates custom field with show_in_overview: true" do
|
||||
test "creates custom field with show_in_overview: true", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -21,24 +26,24 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "creates custom field with show_in_overview: true (default)" do
|
||||
test "creates custom field with show_in_overview: true (default)", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field_hide",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "updates show_in_overview to true" do
|
||||
test "updates show_in_overview to true", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -46,17 +51,17 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
value_type: :string,
|
||||
show_in_overview: false
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert {:ok, updated_field} =
|
||||
custom_field
|
||||
|> Ash.Changeset.for_update(:update, %{show_in_overview: true})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "updates show_in_overview to false" do
|
||||
test "updates show_in_overview to false", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -64,12 +69,12 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert {:ok, updated_field} =
|
||||
custom_field
|
||||
|> Ash.Changeset.for_update(:update, %{show_in_overview: false})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_field.show_in_overview == false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,94 +13,99 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "automatic slug generation on create" do
|
||||
test "generates slug from name with simple ASCII text" do
|
||||
test "generates slug from name with simple ASCII text", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Mobile Phone",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "mobile-phone"
|
||||
end
|
||||
|
||||
test "generates slug from name with German umlauts" do
|
||||
test "generates slug from name with German umlauts", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Café Müller",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "cafe-muller"
|
||||
end
|
||||
|
||||
test "generates slug with lowercase conversion" do
|
||||
test "generates slug with lowercase conversion", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "TEST NAME",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "test-name"
|
||||
end
|
||||
|
||||
test "generates slug by removing special characters" do
|
||||
test "generates slug by removing special characters", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "E-Mail & Address!",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "e-mail-address"
|
||||
end
|
||||
|
||||
test "generates slug by replacing multiple spaces with single hyphen" do
|
||||
test "generates slug by replacing multiple spaces with single hyphen", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Multiple Spaces",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "multiple-spaces"
|
||||
end
|
||||
|
||||
test "trims leading and trailing hyphens" do
|
||||
test "trims leading and trailing hyphens", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "-Test-",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "test"
|
||||
end
|
||||
|
||||
test "handles unicode characters properly (ß becomes ss)" do
|
||||
test "handles unicode characters properly (ß becomes ss)", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Straße",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "strasse"
|
||||
end
|
||||
end
|
||||
|
||||
describe "slug uniqueness" do
|
||||
test "prevents creating custom field with duplicate slug" do
|
||||
test "prevents creating custom field with duplicate slug", %{actor: actor} do
|
||||
# Create first custom field
|
||||
{:ok, _custom_field} =
|
||||
CustomField
|
||||
|
|
@ -108,7 +113,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Attempt to create second custom field with same slug (different case in name)
|
||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||
|
|
@ -117,19 +122,19 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "test",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Exception.message(error) =~ "has already been taken"
|
||||
end
|
||||
|
||||
test "allows custom fields with different slugs" do
|
||||
test "allows custom fields with different slugs", %{actor: actor} do
|
||||
{:ok, custom_field1} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test One",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
{:ok, custom_field2} =
|
||||
CustomField
|
||||
|
|
@ -137,21 +142,21 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Test Two",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field1.slug == "test-one"
|
||||
assert custom_field2.slug == "test-two"
|
||||
assert custom_field1.slug != custom_field2.slug
|
||||
end
|
||||
|
||||
test "prevents duplicate slugs when names differ only in special characters" do
|
||||
test "prevents duplicate slugs when names differ only in special characters", %{actor: actor} do
|
||||
{:ok, custom_field1} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test!!!",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field1.slug == "test"
|
||||
|
||||
|
|
@ -162,7 +167,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Test???",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail with uniqueness constraint error
|
||||
assert Exception.message(error) =~ "has already been taken"
|
||||
|
|
@ -170,7 +175,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
end
|
||||
|
||||
describe "slug immutability" do
|
||||
test "slug cannot be manually set on create" do
|
||||
test "slug cannot be manually set on create", %{actor: actor} do
|
||||
# Attempting to set slug manually should fail because slug is not writable
|
||||
result =
|
||||
CustomField
|
||||
|
|
@ -179,14 +184,14 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
value_type: :string,
|
||||
slug: "custom-slug"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail because slug is not an accepted input
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
assert Exception.message(elem(result, 1)) =~ "No such input"
|
||||
end
|
||||
|
||||
test "slug does not change when name is updated" do
|
||||
test "slug does not change when name is updated", %{actor: actor} do
|
||||
# Create custom field
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|
|
@ -194,7 +199,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Original Name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
original_slug = custom_field.slug
|
||||
assert original_slug == "original-name"
|
||||
|
|
@ -205,7 +210,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|> Ash.Changeset.for_update(:update, %{
|
||||
name: "New Different Name"
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Slug should remain unchanged
|
||||
assert updated_custom_field.slug == original_slug
|
||||
|
|
@ -213,14 +218,14 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
assert updated_custom_field.name == "New Different Name"
|
||||
end
|
||||
|
||||
test "slug cannot be manually updated" do
|
||||
test "slug cannot be manually updated", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
original_slug = custom_field.slug
|
||||
assert original_slug == "test"
|
||||
|
|
@ -231,20 +236,20 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|> Ash.Changeset.for_update(:update, %{
|
||||
slug: "new-slug"
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Should fail because slug is not an accepted input
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
assert Exception.message(elem(result, 1)) =~ "No such input"
|
||||
|
||||
# Reload to verify slug hasn't changed
|
||||
reloaded = Ash.get!(CustomField, custom_field.id)
|
||||
reloaded = Ash.get!(CustomField, custom_field.id, actor: actor)
|
||||
assert reloaded.slug == "test"
|
||||
end
|
||||
end
|
||||
|
||||
describe "slug edge cases" do
|
||||
test "handles very long names by truncating slug" do
|
||||
test "handles very long names by truncating slug", %{actor: actor} do
|
||||
# Create a name at the maximum length (100 chars)
|
||||
long_name = String.duplicate("abcdefghij", 10)
|
||||
# 100 characters exactly
|
||||
|
|
@ -255,7 +260,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: long_name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Slug should be truncated to maximum 100 characters
|
||||
assert String.length(custom_field.slug) <= 100
|
||||
|
|
@ -263,7 +268,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
assert custom_field.slug == long_name
|
||||
end
|
||||
|
||||
test "rejects name with only special characters" do
|
||||
test "rejects name with only special characters", %{actor: actor} do
|
||||
# When name contains only special characters, slug would be empty
|
||||
# This should fail validation
|
||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||
|
|
@ -272,59 +277,59 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "!!!",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail because slug would be empty
|
||||
error_message = Exception.message(error)
|
||||
assert error_message =~ "Slug cannot be empty" or error_message =~ "is required"
|
||||
end
|
||||
|
||||
test "handles mixed special characters and text" do
|
||||
test "handles mixed special characters and text", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test@#$%Name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# slugify keeps the hyphen between words
|
||||
assert custom_field.slug == "test-name"
|
||||
end
|
||||
|
||||
test "handles numbers in name" do
|
||||
test "handles numbers in name", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Field 123 Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "field-123-test"
|
||||
end
|
||||
|
||||
test "handles consecutive hyphens in name" do
|
||||
test "handles consecutive hyphens in name", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test---Name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should reduce multiple hyphens to single hyphen
|
||||
assert custom_field.slug == "test-name"
|
||||
end
|
||||
|
||||
test "handles name with dots and underscores" do
|
||||
test "handles name with dots and underscores", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test.field_name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Dots and underscores should be handled (either kept or converted)
|
||||
assert custom_field.slug =~ ~r/^[a-z0-9-]+$/
|
||||
|
|
@ -332,45 +337,45 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
end
|
||||
|
||||
describe "slug in queries and responses" do
|
||||
test "slug is included in struct after create" do
|
||||
test "slug is included in struct after create", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Slug should be present in the struct
|
||||
assert Map.has_key?(custom_field, :slug)
|
||||
assert custom_field.slug != nil
|
||||
end
|
||||
|
||||
test "can load custom field and slug is present" do
|
||||
test "can load custom field and slug is present", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Load it back
|
||||
loaded_custom_field = Ash.get!(CustomField, custom_field.id)
|
||||
loaded_custom_field = Ash.get!(CustomField, custom_field.id, actor: actor)
|
||||
|
||||
assert loaded_custom_field.slug == "test"
|
||||
end
|
||||
|
||||
test "slug is returned in list queries" do
|
||||
test "slug is returned in list queries", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_fields = Ash.read!(CustomField)
|
||||
custom_fields = Ash.read!(CustomField, actor: actor)
|
||||
|
||||
found = Enum.find(custom_fields, &(&1.id == custom_field.id))
|
||||
assert found.slug == "test"
|
||||
|
|
@ -379,18 +384,18 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|
||||
describe "slug-based lookup (future feature)" do
|
||||
@tag :skip
|
||||
test "can find custom field by slug" do
|
||||
test "can find custom field by slug", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test Field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# This test is for future implementation
|
||||
# We might add a custom action like :by_slug
|
||||
found = Ash.get!(CustomField, custom_field.slug, load: [:slug])
|
||||
found = Ash.get!(CustomField, custom_field.slug, load: [:slug], actor: actor)
|
||||
assert found.id == custom_field.id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,8 +13,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "name validation" do
|
||||
test "accepts name with exactly 100 characters" do
|
||||
test "accepts name with exactly 100 characters", %{actor: actor} do
|
||||
name = String.duplicate("a", 100)
|
||||
|
||||
assert {:ok, custom_field} =
|
||||
|
|
@ -23,13 +28,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.name == name
|
||||
assert String.length(custom_field.name) == 100
|
||||
end
|
||||
|
||||
test "rejects name with 101 characters" do
|
||||
test "rejects name with 101 characters", %{actor: actor} do
|
||||
name = String.duplicate("a", 101)
|
||||
|
||||
assert {:error, changeset} =
|
||||
|
|
@ -38,50 +43,50 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert [%{field: :name, message: message}] = changeset.errors
|
||||
assert message =~ "max" or message =~ "length" or message =~ "100"
|
||||
end
|
||||
|
||||
test "trims whitespace from name" do
|
||||
test "trims whitespace from name", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: " test_field ",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.name == "test_field"
|
||||
end
|
||||
|
||||
test "rejects empty name" do
|
||||
test "rejects empty name", %{actor: actor} do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
|
||||
test "rejects nil name" do
|
||||
test "rejects nil name", %{actor: actor} do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "description validation" do
|
||||
test "accepts description with exactly 500 characters" do
|
||||
test "accepts description with exactly 500 characters", %{actor: actor} do
|
||||
description = String.duplicate("a", 500)
|
||||
|
||||
assert {:ok, custom_field} =
|
||||
|
|
@ -91,13 +96,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: description
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.description == description
|
||||
assert String.length(custom_field.description) == 500
|
||||
end
|
||||
|
||||
test "rejects description with 501 characters" do
|
||||
test "rejects description with 501 characters", %{actor: actor} do
|
||||
description = String.duplicate("a", 501)
|
||||
|
||||
assert {:error, changeset} =
|
||||
|
|
@ -107,13 +112,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: description
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert [%{field: :description, message: message}] = changeset.errors
|
||||
assert message =~ "max" or message =~ "length" or message =~ "500"
|
||||
end
|
||||
|
||||
test "trims whitespace from description" do
|
||||
test "trims whitespace from description", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -121,24 +126,24 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: " A nice description "
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.description == "A nice description"
|
||||
end
|
||||
|
||||
test "accepts nil description (optional field)" do
|
||||
test "accepts nil description (optional field)", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.description == nil
|
||||
end
|
||||
|
||||
test "accepts empty description after trimming" do
|
||||
test "accepts empty description after trimming", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -146,7 +151,7 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: " "
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# After trimming whitespace, becomes nil (empty strings are converted to nil)
|
||||
assert custom_field.description == nil
|
||||
|
|
@ -154,14 +159,14 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
end
|
||||
|
||||
describe "name uniqueness" do
|
||||
test "rejects duplicate names" do
|
||||
test "rejects duplicate names", %{actor: actor} do
|
||||
assert {:ok, _} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "unique_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|
|
@ -169,14 +174,14 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: "unique_field",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "value_type validation" do
|
||||
test "accepts all valid value types" do
|
||||
test "accepts all valid value types", %{actor: actor} do
|
||||
for value_type <- [:string, :integer, :boolean, :date, :email] do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|
|
@ -184,20 +189,20 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: "field_#{value_type}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.value_type == value_type
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects invalid value type" do
|
||||
test "rejects invalid value type", %{actor: actor} do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "invalid_field",
|
||||
value_type: :invalid_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert [%{field: :value_type}] = changeset.errors
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create a test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -21,7 +23,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
last_name: "User",
|
||||
email: "test.validation@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom fields for different types
|
||||
{:ok, string_field} =
|
||||
|
|
@ -30,7 +32,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
name: "string_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, integer_field} =
|
||||
CustomField
|
||||
|
|
@ -38,7 +40,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
name: "integer_field",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, email_field} =
|
||||
CustomField
|
||||
|
|
@ -46,9 +48,10 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
name: "email_field",
|
||||
value_type: :email
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field,
|
||||
integer_field: integer_field,
|
||||
|
|
@ -58,6 +61,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
|
||||
describe "string value length validation" do
|
||||
test "accepts string value with exactly 10,000 characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -73,13 +77,14 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
"_union_value" => value_string
|
||||
}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == value_string
|
||||
assert String.length(custom_field_value.value.value) == 10_000
|
||||
end
|
||||
|
||||
test "rejects string value with 10,001 characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -92,14 +97,18 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => value_string}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error ->
|
||||
error.field == :value and (error.message =~ "max" or error.message =~ "length")
|
||||
end)
|
||||
end
|
||||
|
||||
test "trims whitespace from string value", %{member: member, string_field: string_field} do
|
||||
test "trims whitespace from string value", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -107,12 +116,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => " test value "}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == "test value"
|
||||
end
|
||||
|
||||
test "accepts empty string value", %{member: member, string_field: string_field} do
|
||||
test "accepts empty string value", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -120,13 +133,17 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Empty strings after trimming become nil
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts string with special characters", %{member: member, string_field: string_field} do
|
||||
test "accepts string with special characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
special_string = "Hello 世界! 🎉 @#$%^&*()"
|
||||
|
||||
assert {:ok, custom_field_value} =
|
||||
|
|
@ -136,14 +153,18 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => special_string}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == special_string
|
||||
end
|
||||
end
|
||||
|
||||
describe "integer value validation" do
|
||||
test "accepts valid integer value", %{member: member, integer_field: integer_field} do
|
||||
test "accepts valid integer value", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
integer_field: integer_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -151,12 +172,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 42}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == 42
|
||||
end
|
||||
|
||||
test "accepts negative integer", %{member: member, integer_field: integer_field} do
|
||||
test "accepts negative integer", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
integer_field: integer_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -164,12 +189,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => -100}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == -100
|
||||
end
|
||||
|
||||
test "accepts zero", %{member: member, integer_field: integer_field} do
|
||||
test "accepts zero", %{actor: system_actor, member: member, integer_field: integer_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -177,14 +202,18 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 0}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "email value validation" do
|
||||
test "accepts nil value (optional field)", %{member: member, email_field: email_field} do
|
||||
test "accepts nil value (optional field)", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -192,12 +221,13 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => nil}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts empty string (becomes nil after trim)", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
|
|
@ -208,13 +238,13 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Empty string after trim should become nil
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts valid email", %{member: member, email_field: email_field} do
|
||||
test "accepts valid email", %{actor: system_actor, member: member, email_field: email_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -222,12 +252,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "test@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == "test@example.com"
|
||||
end
|
||||
|
||||
test "rejects invalid email format", %{member: member, email_field: email_field} do
|
||||
test "rejects invalid email format", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
assert {:error, changeset} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -235,12 +269,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "not-an-email"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
|
||||
end
|
||||
|
||||
test "rejects email longer than 254 characters", %{member: member, email_field: email_field} do
|
||||
test "rejects email longer than 254 characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
# Create an email with >254 chars (243 + 12 = 255)
|
||||
long_email = String.duplicate("a", 243) <> "@example.com"
|
||||
|
||||
|
|
@ -251,12 +289,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => long_email}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
|
||||
end
|
||||
|
||||
test "trims whitespace from email", %{member: member, email_field: email_field} do
|
||||
test "trims whitespace from email", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -264,7 +306,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => " test@example.com "}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == "test@example.com"
|
||||
end
|
||||
|
|
@ -272,6 +314,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
|
||||
describe "uniqueness constraint" do
|
||||
test "rejects duplicate custom_field_id per member", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -283,7 +326,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "first value"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Try to create second custom field value with same custom_field_id for same member
|
||||
assert {:error, changeset} =
|
||||
|
|
@ -293,7 +336,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "second value"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Should have uniqueness error
|
||||
assert Enum.any?(changeset.errors, fn error ->
|
||||
|
|
|
|||
|
|
@ -1,70 +1,93 @@
|
|||
defmodule Mv.Membership.FuzzySearchTest do
|
||||
use Mv.DataCase, async: false
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
test "fuzzy_search/2 function exists" do
|
||||
assert function_exported?(Mv.Membership.Member, :fuzzy_search, 2)
|
||||
end
|
||||
|
||||
test "fuzzy_search returns only John Doe by fuzzy query 'john'" do
|
||||
test "fuzzy_search returns only John Doe by fuzzy query 'john'", %{actor: actor} do
|
||||
{:ok, john} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _jane} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Adriana",
|
||||
last_name: "Smith",
|
||||
email: "adriana.smith@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Adriana",
|
||||
last_name: "Smith",
|
||||
email: "adriana.smith@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, alice} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice.johnson@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice.johnson@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{
|
||||
query: "john"
|
||||
})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
assert Enum.map(result, & &1.id) == [john.id, alice.id]
|
||||
end
|
||||
|
||||
test "fuzzy_search finds 'Thomas' when searching misspelled 'tomas'" do
|
||||
test "fuzzy_search finds 'Thomas' when searching misspelled 'tomas'", %{actor: actor} do
|
||||
{:ok, thomas} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Thomas",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Thomas",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, jane} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane.smith@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane.smith@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _alice} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice.johnson@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice.johnson@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{
|
||||
query: "tomas"
|
||||
})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert thomas.id in ids
|
||||
|
|
@ -72,17 +95,21 @@ defmodule Mv.Membership.FuzzySearchTest do
|
|||
assert not Enum.empty?(ids)
|
||||
end
|
||||
|
||||
test "empty query returns all members" do
|
||||
test "empty query returns all members", %{actor: actor} do
|
||||
{:ok, a} =
|
||||
Mv.Membership.create_member(%{first_name: "A", last_name: "One", email: "a1@example.com"})
|
||||
Mv.Membership.create_member(%{first_name: "A", last_name: "One", email: "a1@example.com"},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, b} =
|
||||
Mv.Membership.create_member(%{first_name: "B", last_name: "Two", email: "b2@example.com"})
|
||||
Mv.Membership.create_member(%{first_name: "B", last_name: "Two", email: "b2@example.com"},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: ""})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
assert Enum.sort(Enum.map(result, & &1.id))
|
||||
|> Enum.uniq()
|
||||
|
|
@ -90,352 +117,435 @@ defmodule Mv.Membership.FuzzySearchTest do
|
|||
|> Enum.all?(fn id -> id in [a.id, b.id] end)
|
||||
end
|
||||
|
||||
test "substring numeric search matches postal_code mid-string" do
|
||||
test "substring numeric search matches postal_code mid-string", %{actor: actor} do
|
||||
{:ok, m1} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Num",
|
||||
last_name: "One",
|
||||
email: "n1@example.com",
|
||||
postal_code: "12345"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Num",
|
||||
last_name: "One",
|
||||
email: "n1@example.com",
|
||||
postal_code: "12345"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _m2} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Num",
|
||||
last_name: "Two",
|
||||
email: "n2@example.com",
|
||||
postal_code: "67890"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Num",
|
||||
last_name: "Two",
|
||||
email: "n2@example.com",
|
||||
postal_code: "67890"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "345"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert m1.id in ids
|
||||
end
|
||||
|
||||
test "substring numeric search matches house_number mid-string" do
|
||||
test "substring numeric search matches house_number mid-string", %{actor: actor} do
|
||||
{:ok, m1} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Home",
|
||||
last_name: "One",
|
||||
email: "h1@example.com",
|
||||
house_number: "A345B"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Home",
|
||||
last_name: "One",
|
||||
email: "h1@example.com",
|
||||
house_number: "A345B"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _m2} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Home",
|
||||
last_name: "Two",
|
||||
email: "h2@example.com",
|
||||
house_number: "77"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Home",
|
||||
last_name: "Two",
|
||||
email: "h2@example.com",
|
||||
house_number: "77"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "345"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert m1.id in ids
|
||||
end
|
||||
|
||||
test "fuzzy matches street misspelling" do
|
||||
test "fuzzy matches street misspelling", %{actor: actor} do
|
||||
{:ok, s1} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Road",
|
||||
last_name: "Test",
|
||||
email: "s1@example.com",
|
||||
street: "Main Street"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Road",
|
||||
last_name: "Test",
|
||||
email: "s1@example.com",
|
||||
street: "Main Street"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _s2} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Road",
|
||||
last_name: "Other",
|
||||
email: "s2@example.com",
|
||||
street: "Second Avenue"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Road",
|
||||
last_name: "Other",
|
||||
email: "s2@example.com",
|
||||
street: "Second Avenue"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "mainn"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert s1.id in ids
|
||||
end
|
||||
|
||||
test "substring in city matches mid-string" do
|
||||
test "substring in city matches mid-string", %{actor: actor} do
|
||||
{:ok, b} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "City",
|
||||
last_name: "One",
|
||||
email: "city1@example.com",
|
||||
city: "Berlin"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "City",
|
||||
last_name: "One",
|
||||
email: "city1@example.com",
|
||||
city: "Berlin"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _m} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "City",
|
||||
last_name: "Two",
|
||||
email: "city2@example.com",
|
||||
city: "München"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "City",
|
||||
last_name: "Two",
|
||||
email: "city2@example.com",
|
||||
city: "München"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "erl"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert b.id in ids
|
||||
end
|
||||
|
||||
test "blank character handling: query with spaces matches full name" do
|
||||
test "blank character handling: query with spaces matches full name", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane.smith@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane.smith@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "john doe"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "blank character handling: query with multiple spaces is handled" do
|
||||
test "blank character handling: query with multiple spaces is handled", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Mary",
|
||||
last_name: "Jane",
|
||||
email: "mary.jane@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Mary",
|
||||
last_name: "Jane",
|
||||
email: "mary.jane@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "mary jane"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "special character handling: @ symbol in query matches email" do
|
||||
test "special character handling: @ symbol in query matches email", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test.user@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test.user@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Other",
|
||||
last_name: "Person",
|
||||
email: "other.person@different.org"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Other",
|
||||
last_name: "Person",
|
||||
email: "other.person@different.org"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "example"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "special character handling: dot in query matches email" do
|
||||
test "special character handling: dot in query matches email", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Dot",
|
||||
last_name: "Test",
|
||||
email: "dot.test@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Dot",
|
||||
last_name: "Test",
|
||||
email: "dot.test@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "No",
|
||||
last_name: "Dot",
|
||||
email: "nodot@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "No",
|
||||
last_name: "Dot",
|
||||
email: "nodot@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "dot.test"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "special character handling: hyphen in query matches data" do
|
||||
test "special character handling: hyphen in query matches data", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Mary-Jane",
|
||||
last_name: "Watson",
|
||||
email: "mary.jane@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Mary-Jane",
|
||||
last_name: "Watson",
|
||||
email: "mary.jane@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Mary",
|
||||
last_name: "Smith",
|
||||
email: "mary.smith@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Mary",
|
||||
last_name: "Smith",
|
||||
email: "mary.smith@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "mary-jane"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "unicode character handling: umlaut ö in query matches data" do
|
||||
test "unicode character handling: umlaut ö in query matches data", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Jörg",
|
||||
last_name: "Schmidt",
|
||||
email: "joerg.schmidt@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Jörg",
|
||||
last_name: "Schmidt",
|
||||
email: "joerg.schmidt@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Smith",
|
||||
email: "john.smith@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Smith",
|
||||
email: "john.smith@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "jörg"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "unicode character handling: umlaut ä in query matches data" do
|
||||
test "unicode character handling: umlaut ä in query matches data", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Märta",
|
||||
last_name: "Andersson",
|
||||
email: "maerta.andersson@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Märta",
|
||||
last_name: "Andersson",
|
||||
email: "maerta.andersson@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Marta",
|
||||
last_name: "Johnson",
|
||||
email: "marta.johnson@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Marta",
|
||||
last_name: "Johnson",
|
||||
email: "marta.johnson@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "märta"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "unicode character handling: umlaut ü in query matches data" do
|
||||
test "unicode character handling: umlaut ü in query matches data", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Günther",
|
||||
last_name: "Müller",
|
||||
email: "guenther.mueller@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Günther",
|
||||
last_name: "Müller",
|
||||
email: "guenther.mueller@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Gunter",
|
||||
last_name: "Miller",
|
||||
email: "gunter.miller@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Gunter",
|
||||
last_name: "Miller",
|
||||
email: "gunter.miller@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "müller"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "unicode character handling: query without umlaut matches data with umlaut" do
|
||||
test "unicode character handling: query without umlaut matches data with umlaut", %{
|
||||
actor: actor
|
||||
} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Müller",
|
||||
last_name: "Schmidt",
|
||||
email: "mueller.schmidt@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Müller",
|
||||
last_name: "Schmidt",
|
||||
email: "mueller.schmidt@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Miller",
|
||||
last_name: "Smith",
|
||||
email: "miller.smith@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Miller",
|
||||
last_name: "Smith",
|
||||
email: "miller.smith@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: "muller"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
ids = Enum.map(result, & &1.id)
|
||||
assert member.id in ids
|
||||
end
|
||||
|
||||
test "very long search strings: handles long query without error" do
|
||||
test "very long search strings: handles long query without error", %{actor: actor} do
|
||||
{:ok, _member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
long_query = String.duplicate("a", 1000)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: long_query})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
# Should not crash, may return empty or some results
|
||||
assert is_list(result)
|
||||
end
|
||||
|
||||
test "very long search strings: handles extremely long query" do
|
||||
test "very long search strings: handles extremely long query", %{actor: actor} do
|
||||
{:ok, _member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test@example.com"
|
||||
})
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
very_long_query = String.duplicate("test query ", 1000)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: very_long_query})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
# Should not crash, may return empty or some results
|
||||
assert is_list(result)
|
||||
|
|
|
|||
|
|
@ -13,64 +13,87 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
|
||||
describe "available_for_linking/2" do
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create 5 unlinked members with distinct names
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Bob",
|
||||
last_name: "Williams",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Bob",
|
||||
last_name: "Williams",
|
||||
email: "bob@example.com"
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member3} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Davis",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Davis",
|
||||
email: "charlie@example.com"
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member4} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Diana",
|
||||
last_name: "Martinez",
|
||||
email: "diana@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Diana",
|
||||
last_name: "Martinez",
|
||||
email: "diana@example.com"
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member5} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Emma",
|
||||
last_name: "Taylor",
|
||||
email: "emma@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Emma",
|
||||
last_name: "Taylor",
|
||||
email: "emma@example.com"
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
unlinked_members = [member1, member2, member3, member4, member5]
|
||||
|
||||
# Create 2 linked members (with users)
|
||||
{:ok, user1} = Mv.Accounts.create_user(%{email: "user1@example.com"})
|
||||
{:ok, user1} = Mv.Accounts.create_user(%{email: "user1@example.com"}, actor: system_actor)
|
||||
|
||||
{:ok, linked_member1} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member1",
|
||||
email: "linked1@example.com",
|
||||
user: %{id: user1.id}
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member1",
|
||||
email: "linked1@example.com",
|
||||
user: %{id: user1.id}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, user2} = Mv.Accounts.create_user(%{email: "user2@example.com"})
|
||||
{:ok, user2} = Mv.Accounts.create_user(%{email: "user2@example.com"}, actor: system_actor)
|
||||
|
||||
{:ok, linked_member2} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member2",
|
||||
email: "linked2@example.com",
|
||||
user: %{id: user2.id}
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member2",
|
||||
email: "linked2@example.com",
|
||||
user: %{id: user2.id}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
%{
|
||||
unlinked_members: unlinked_members,
|
||||
|
|
@ -82,11 +105,13 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
unlinked_members: unlinked_members,
|
||||
linked_members: _linked_members
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Call the action without any arguments
|
||||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Should return only the 5 unlinked members, not the 2 linked ones
|
||||
assert length(members) == 5
|
||||
|
|
@ -98,25 +123,32 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
|
||||
# Verify none of the returned members have a user_id
|
||||
Enum.each(members, fn member ->
|
||||
member_with_user = Ash.get!(Mv.Membership.Member, member.id, load: [:user])
|
||||
member_with_user =
|
||||
Ash.get!(Mv.Membership.Member, member.id, actor: system_actor, load: [:user])
|
||||
|
||||
assert is_nil(member_with_user.user)
|
||||
end)
|
||||
end
|
||||
|
||||
test "limits results to 10 members even when more exist" do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create 15 additional unlinked members (total 20 unlinked)
|
||||
for i <- 6..20 do
|
||||
Membership.create_member(%{
|
||||
first_name: "Extra#{i}",
|
||||
last_name: "Member#{i}",
|
||||
email: "extra#{i}@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Extra#{i}",
|
||||
last_name: "Member#{i}",
|
||||
email: "extra#{i}@example.com"
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
end
|
||||
|
||||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Should be limited to 10
|
||||
assert length(members) == 10
|
||||
|
|
@ -125,6 +157,8 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
test "email match: returns only member with matching email when exists", %{
|
||||
unlinked_members: unlinked_members
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Get one of the unlinked members' email
|
||||
target_member = List.first(unlinked_members)
|
||||
user_email = target_member.email
|
||||
|
|
@ -132,7 +166,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
raw_members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{user_email: user_email})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Apply email match filtering (sorted results come from query)
|
||||
# When user_email matches, only that member should be returned
|
||||
|
|
@ -145,13 +179,15 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
end
|
||||
|
||||
test "email match: returns all unlinked members when no email match" do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Use an email that doesn't match any member
|
||||
non_matching_email = "nonexistent@example.com"
|
||||
|
||||
raw_members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{user_email: non_matching_email})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Apply email match filtering
|
||||
members = Mv.Membership.Member.filter_by_email_match(raw_members, non_matching_email)
|
||||
|
|
@ -163,11 +199,13 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
test "search query: filters by first_name, last_name, and email", %{
|
||||
unlinked_members: _unlinked_members
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Search by first name
|
||||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "Alice"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(members) == 1
|
||||
assert List.first(members).first_name == "Alice"
|
||||
|
|
@ -176,7 +214,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "Williams"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(members) == 1
|
||||
assert List.first(members).last_name == "Williams"
|
||||
|
|
@ -185,7 +223,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "charlie@"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(members) == 1
|
||||
assert List.first(members).email == "charlie@example.com"
|
||||
|
|
@ -194,12 +232,13 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "NonExistent"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.empty?(members)
|
||||
end
|
||||
|
||||
test "user_email takes precedence over search_query", %{unlinked_members: unlinked_members} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
target_member = List.first(unlinked_members)
|
||||
|
||||
# Pass both email match and search query that would match different members
|
||||
|
|
@ -209,7 +248,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
user_email: target_member.email,
|
||||
search_query: "Bob"
|
||||
})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Apply email-match filter (as LiveView does)
|
||||
members = Mv.Membership.Member.filter_by_email_match(raw_members, target_member.email)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,13 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
defp create_fee_type(attrs, actor) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -21,11 +26,11 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
defp create_member(attrs, actor) do
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -36,11 +41,11 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
defp create_cycle(member, fee_type, attrs, actor) do
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2024-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -53,62 +58,77 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
describe "current_cycle_status" do
|
||||
test "returns status of current cycle for member with active cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns status of current cycle for member with active cycle", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Create a cycle that is active today (2024-01-01 to 2024-12-31)
|
||||
# Assuming today is in 2024
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: cycle_start,
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: cycle_start,
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :current_cycle_status)
|
||||
assert member.current_cycle_status == :paid
|
||||
end
|
||||
|
||||
test "returns nil for member without current cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns nil for member without current cycle", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Create a cycle in the past (not current)
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2020-01-01],
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2020-01-01],
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :current_cycle_status)
|
||||
assert member.current_cycle_status == nil
|
||||
end
|
||||
|
||||
test "returns nil for member without cycles" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns nil for member without cycles", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
member = Ash.load!(member, :current_cycle_status)
|
||||
assert member.current_cycle_status == nil
|
||||
end
|
||||
|
||||
test "returns status of current cycle for monthly interval" do
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns status of current cycle for monthly interval", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Create a cycle that is active today (current month)
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: cycle_start,
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: cycle_start,
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :current_cycle_status)
|
||||
assert member.current_cycle_status == :unpaid
|
||||
|
|
@ -116,79 +136,109 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
end
|
||||
|
||||
describe "last_cycle_status" do
|
||||
test "returns status of last completed cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns status of last completed cycle", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Create cycles: 2022 (completed), 2023 (completed), 2024 (current)
|
||||
today = Date.utc_today()
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Current cycle
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: cycle_start,
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: cycle_start,
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :last_cycle_status)
|
||||
# Should return status of 2023 (last completed)
|
||||
assert member.last_cycle_status == :unpaid
|
||||
end
|
||||
|
||||
test "returns nil for member without completed cycles" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns nil for member without completed cycles", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Only create current cycle (not completed yet)
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: cycle_start,
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: cycle_start,
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :last_cycle_status)
|
||||
assert member.last_cycle_status == nil
|
||||
end
|
||||
|
||||
test "returns nil for member without cycles" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns nil for member without cycles", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
member = Ash.load!(member, :last_cycle_status)
|
||||
assert member.last_cycle_status == nil
|
||||
end
|
||||
|
||||
test "returns status of last completed cycle for monthly interval" do
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns status of last completed cycle for monthly interval", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
today = Date.utc_today()
|
||||
# Create cycles: last month (completed), current month (not completed)
|
||||
last_month_start = Date.add(today, -32) |> CalendarCycles.calculate_cycle_start(:monthly)
|
||||
current_month_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: last_month_start,
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: last_month_start,
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: current_month_start,
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: current_month_start,
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :last_cycle_status)
|
||||
# Should return status of last month (last completed)
|
||||
|
|
@ -197,9 +247,9 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
end
|
||||
|
||||
describe "overdue_count" do
|
||||
test "counts only unpaid cycles that have ended" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "counts only unpaid cycles that have ended", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
today = Date.utc_today()
|
||||
|
||||
|
|
@ -209,23 +259,38 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
# 2024: unpaid, current (not overdue)
|
||||
# 2025: unpaid, future (not overdue)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Current cycle
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: cycle_start,
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: cycle_start,
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Future cycle (if we're not at the end of the year)
|
||||
next_year = today.year + 1
|
||||
|
|
@ -233,10 +298,15 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
if today.month < 12 or today.day < 31 do
|
||||
next_year_start = Date.new!(next_year, 1, 1)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: next_year_start,
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: next_year_start,
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
end
|
||||
|
||||
member = Ash.load!(member, :overdue_count)
|
||||
|
|
@ -244,31 +314,36 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
assert member.overdue_count == 1
|
||||
end
|
||||
|
||||
test "returns 0 when no overdue cycles" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns 0 when no overdue cycles", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Create only paid cycles
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :overdue_count)
|
||||
assert member.overdue_count == 0
|
||||
end
|
||||
|
||||
test "returns 0 for member without cycles" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns 0 for member without cycles", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
member = Ash.load!(member, :overdue_count)
|
||||
assert member.overdue_count == 0
|
||||
end
|
||||
|
||||
test "counts overdue cycles for monthly interval" do
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "counts overdue cycles for monthly interval", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
today = Date.utc_today()
|
||||
|
||||
|
|
@ -279,45 +354,75 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
last_month_start = Date.add(today, -32) |> CalendarCycles.calculate_cycle_start(:monthly)
|
||||
current_month_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: two_months_ago_start,
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: two_months_ago_start,
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: last_month_start,
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: last_month_start,
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: current_month_start,
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: current_month_start,
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :overdue_count)
|
||||
# Should only count two_months_ago (unpaid and ended)
|
||||
assert member.overdue_count == 1
|
||||
end
|
||||
|
||||
test "counts multiple overdue cycles" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "counts multiple overdue cycles", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Create multiple unpaid, ended cycles
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2020-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2020-01-01],
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2021-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2021-01-01],
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = Ash.load!(member, :overdue_count)
|
||||
assert member.overdue_count == 3
|
||||
|
|
@ -325,29 +430,44 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
end
|
||||
|
||||
describe "calculations with multiple cycles" do
|
||||
test "all calculations work correctly with multiple cycles" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "all calculations work correctly with multiple cycles", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
today = Date.utc_today()
|
||||
|
||||
# Create cycles: 2022 (unpaid, ended), 2023 (paid, ended), 2024 (unpaid, current)
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
status: :paid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
status: :paid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
cycle_start: cycle_start,
|
||||
status: :unpaid
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: cycle_start,
|
||||
status: :unpaid
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member =
|
||||
Ash.load!(member, [:current_cycle_status, :last_cycle_status, :overdue_count])
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ defmodule Mv.Membership.MemberEmailSyncTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Member email synchronization to linked User" do
|
||||
@valid_user_attrs %{
|
||||
email: "user@example.com"
|
||||
|
|
@ -19,108 +24,119 @@ defmodule Mv.Membership.MemberEmailSyncTest do
|
|||
email: "member@example.com"
|
||||
}
|
||||
|
||||
test "updating member email syncs to linked user" do
|
||||
test "updating member email syncs to linked user", %{actor: actor} do
|
||||
# Create a user
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
assert to_string(user.email) == "user@example.com"
|
||||
|
||||
# Create a member linked to the user
|
||||
{:ok, member} =
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}))
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}),
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify initial state - member email should be overridden by user email
|
||||
{:ok, member_after_create} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, member_after_create} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert member_after_create.email == "user@example.com"
|
||||
|
||||
# Update member email
|
||||
{:ok, updated_member} =
|
||||
Membership.update_member(member, %{email: "newmember@example.com"})
|
||||
Membership.update_member(member, %{email: "newmember@example.com"}, actor: actor)
|
||||
|
||||
assert updated_member.email == "newmember@example.com"
|
||||
|
||||
# Verify user email was also updated
|
||||
{:ok, synced_user} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, synced_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert to_string(synced_user.email) == "newmember@example.com"
|
||||
end
|
||||
|
||||
test "creating member linked to user syncs user email to member" do
|
||||
test "creating member linked to user syncs user email to member", %{actor: actor} do
|
||||
# Create a user with their own email
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
assert to_string(user.email) == "user@example.com"
|
||||
|
||||
# Create a member linked to this user
|
||||
{:ok, member} =
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}))
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}),
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Member should have been created with user's email (user is source of truth)
|
||||
assert member.email == "user@example.com"
|
||||
|
||||
# Verify the link
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor)
|
||||
assert loaded_member.user.id == user.id
|
||||
end
|
||||
|
||||
test "linking member to existing user syncs user email to member" do
|
||||
test "linking member to existing user syncs user email to member", %{actor: actor} do
|
||||
# Create a standalone user
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
assert to_string(user.email) == "user@example.com"
|
||||
|
||||
# Create a standalone member
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
assert member.email == "member@example.com"
|
||||
|
||||
# Link the member to the user
|
||||
{:ok, linked_member} = Membership.update_member(member, %{user: %{id: user.id}})
|
||||
{:ok, linked_member} =
|
||||
Membership.update_member(member, %{user: %{id: user.id}}, actor: actor)
|
||||
|
||||
# Verify the link
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, linked_member.id, load: [:user])
|
||||
{:ok, loaded_member} =
|
||||
Ash.get(Mv.Membership.Member, linked_member.id, load: [:user], actor: actor)
|
||||
|
||||
assert loaded_member.user.id == user.id
|
||||
|
||||
# Verify member email was overridden with user email
|
||||
assert loaded_member.email == "user@example.com"
|
||||
end
|
||||
|
||||
test "updating member email when no user linked does not error" do
|
||||
test "updating member email when no user linked does not error", %{actor: actor} do
|
||||
# Create a standalone member without user link
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
assert member.email == "member@example.com"
|
||||
|
||||
# Load to verify no user link
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor)
|
||||
assert loaded_member.user == nil
|
||||
|
||||
# Update member email - should work fine without error
|
||||
{:ok, updated_member} =
|
||||
Membership.update_member(member, %{email: "newemail@example.com"})
|
||||
Membership.update_member(member, %{email: "newemail@example.com"}, actor: actor)
|
||||
|
||||
assert updated_member.email == "newemail@example.com"
|
||||
end
|
||||
|
||||
test "unlinking member from user does not sync email" do
|
||||
test "unlinking member from user does not sync email", %{actor: actor} do
|
||||
# Create user
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
|
||||
# Create member linked to user
|
||||
{:ok, member} =
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}))
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}),
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify member email was synced to user email
|
||||
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert synced_member.email == "user@example.com"
|
||||
|
||||
# Verify link exists
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor)
|
||||
assert loaded_member.user != nil
|
||||
|
||||
# Unlink member from user
|
||||
{:ok, unlinked_member} = Membership.update_member(member, %{user: nil})
|
||||
{:ok, unlinked_member} = Membership.update_member(member, %{user: nil}, actor: actor)
|
||||
|
||||
# Verify unlink
|
||||
{:ok, loaded_unlinked} = Ash.get(Mv.Membership.Member, unlinked_member.id, load: [:user])
|
||||
{:ok, loaded_unlinked} =
|
||||
Ash.get(Mv.Membership.Member, unlinked_member.id, load: [:user], actor: actor)
|
||||
|
||||
assert loaded_unlinked.user == nil
|
||||
|
||||
# User email should remain unchanged after unlinking
|
||||
{:ok, user_after_unlink} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, user_after_unlink} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert to_string(user_after_unlink.email) == "user@example.com"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,15 +9,23 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "available_for_linking with fuzzy search" do
|
||||
test "finds member despite typo" do
|
||||
test "finds member despite typo", %{actor: actor} do
|
||||
# Create member with specific name
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Jonathan",
|
||||
last_name: "Smith",
|
||||
email: "jonathan@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jonathan",
|
||||
last_name: "Smith",
|
||||
email: "jonathan@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Search with typo
|
||||
query =
|
||||
|
|
@ -27,21 +35,24 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
search_query: "Jonatan"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor)
|
||||
|
||||
# Should find Jonathan despite typo
|
||||
assert length(members) == 1
|
||||
assert hd(members).id == member.id
|
||||
end
|
||||
|
||||
test "finds member with partial match" do
|
||||
test "finds member with partial match", %{actor: actor} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Alexander",
|
||||
last_name: "Williams",
|
||||
email: "alex@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Alexander",
|
||||
last_name: "Williams",
|
||||
email: "alex@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Search with partial
|
||||
query =
|
||||
|
|
@ -51,28 +62,34 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
search_query: "Alex"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor)
|
||||
|
||||
# Should find Alexander
|
||||
assert length(members) == 1
|
||||
assert hd(members).id == member.id
|
||||
end
|
||||
|
||||
test "email match overrides fuzzy search" do
|
||||
test "email match overrides fuzzy search", %{actor: actor} do
|
||||
# Create two members
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _member2} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Search with user_email that matches member1, but search_query that would match member2
|
||||
query =
|
||||
|
|
@ -82,7 +99,7 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
search_query: "Jane"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor)
|
||||
|
||||
# Apply email filter
|
||||
filtered_members = Mv.Membership.Member.filter_by_email_match(members, "john@example.com")
|
||||
|
|
@ -92,14 +109,17 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
assert hd(filtered_members).id == member1.id
|
||||
end
|
||||
|
||||
test "limits to 10 results" do
|
||||
test "limits to 10 results", %{actor: actor} do
|
||||
# Create 15 members with similar names
|
||||
for i <- 1..15 do
|
||||
Membership.create_member(%{
|
||||
first_name: "Test#{i}",
|
||||
last_name: "Member",
|
||||
email: "test#{i}@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Test#{i}",
|
||||
last_name: "Member",
|
||||
email: "test#{i}@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
end
|
||||
|
||||
# Search for "Test"
|
||||
|
|
@ -110,34 +130,43 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
search_query: "Test"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor)
|
||||
|
||||
# Should return max 10 members
|
||||
assert length(members) == 10
|
||||
end
|
||||
|
||||
test "excludes linked members" do
|
||||
test "excludes linked members", %{actor: actor} do
|
||||
# Create member and link to user
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member",
|
||||
email: "linked@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member",
|
||||
email: "linked@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _user} =
|
||||
Accounts.create_user(%{
|
||||
email: "user@example.com",
|
||||
member: %{id: member1.id}
|
||||
})
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com",
|
||||
member: %{id: member1.id}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create unlinked member
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Unlinked",
|
||||
last_name: "Member",
|
||||
email: "unlinked@example.com"
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Unlinked",
|
||||
last_name: "Member",
|
||||
email: "unlinked@example.com"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Search for "Member"
|
||||
query =
|
||||
|
|
@ -147,7 +176,7 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
search_query: "Member"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor)
|
||||
|
||||
# Should only return unlinked member
|
||||
member_ids = Enum.map(members, & &1.id)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create required custom fields for different types
|
||||
{:ok, required_string_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -22,7 +24,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :string,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_integer_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -31,7 +33,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :integer,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_boolean_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -40,7 +42,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :boolean,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_date_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -49,7 +51,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :date,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_email_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -58,7 +60,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :email,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, optional_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -67,7 +69,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :string,
|
||||
required: false
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
required_string_field: required_string_field,
|
||||
|
|
@ -75,7 +77,8 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
required_boolean_field: required_boolean_field,
|
||||
required_date_field: required_date_field,
|
||||
required_email_field: required_email_field,
|
||||
optional_field: optional_field
|
||||
optional_field: optional_field,
|
||||
actor: system_actor
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -118,17 +121,23 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
email: "john@example.com"
|
||||
}
|
||||
|
||||
test "fails when required custom field is missing", %{required_string_field: field} do
|
||||
test "fails when required custom field is missing", %{
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} do
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, [])
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required string custom field has nil value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values
|
||||
custom_field_values =
|
||||
|
|
@ -143,14 +152,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required string custom field has empty string value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values
|
||||
custom_field_values =
|
||||
|
|
@ -165,14 +177,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required string custom field has whitespace-only value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values
|
||||
custom_field_values =
|
||||
|
|
@ -187,14 +202,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required string custom field has valid value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values, then update the string field
|
||||
custom_field_values =
|
||||
|
|
@ -209,12 +227,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required integer custom field has nil value",
|
||||
%{
|
||||
required_integer_field: field
|
||||
required_integer_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -228,14 +247,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required integer custom field has empty string value",
|
||||
%{
|
||||
required_integer_field: field
|
||||
required_integer_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -249,25 +271,29 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required integer custom field has zero value",
|
||||
%{
|
||||
required_integer_field: _field
|
||||
required_integer_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when required integer custom field has positive value",
|
||||
%{
|
||||
required_integer_field: field
|
||||
required_integer_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -281,12 +307,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required boolean custom field has nil value",
|
||||
%{
|
||||
required_boolean_field: field
|
||||
required_boolean_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -300,25 +327,29 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required boolean custom field has false value",
|
||||
%{
|
||||
required_boolean_field: _field
|
||||
required_boolean_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when required boolean custom field has true value",
|
||||
%{
|
||||
required_boolean_field: field
|
||||
required_boolean_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -332,12 +363,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required date custom field has nil value",
|
||||
%{
|
||||
required_date_field: field
|
||||
required_date_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -351,14 +383,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required date custom field has empty string value",
|
||||
%{
|
||||
required_date_field: field
|
||||
required_date_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -372,25 +407,29 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required date custom field has valid date value",
|
||||
%{
|
||||
required_date_field: _field
|
||||
required_date_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required email custom field has nil value",
|
||||
%{
|
||||
required_email_field: field
|
||||
required_email_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -404,14 +443,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required email custom field has empty string value",
|
||||
%{
|
||||
required_email_field: field
|
||||
required_email_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -425,27 +467,31 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required email custom field has valid email value",
|
||||
%{
|
||||
required_email_field: _field
|
||||
required_email_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when multiple required custom fields are provided",
|
||||
%{
|
||||
required_string_field: string_field,
|
||||
required_integer_field: integer_field,
|
||||
required_boolean_field: boolean_field
|
||||
required_boolean_field: boolean_field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -467,13 +513,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when one of multiple required custom fields is missing",
|
||||
%{
|
||||
required_string_field: string_field,
|
||||
required_integer_field: integer_field
|
||||
required_integer_field: integer_field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Provide only string field, missing integer, boolean, and date
|
||||
custom_field_values =
|
||||
|
|
@ -487,22 +534,24 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ integer_field.name
|
||||
end
|
||||
|
||||
test "succeeds when optional custom field is missing", %{} = context do
|
||||
test "succeeds when optional custom field is missing", %{actor: actor} = context do
|
||||
# Provide all required fields, but no optional field
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when optional custom field has nil value",
|
||||
%{optional_field: field} = context do
|
||||
%{optional_field: field, actor: actor} = context do
|
||||
# Provide all required fields plus optional field with nil
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context) ++
|
||||
|
|
@ -515,29 +564,33 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_member with required custom fields" do
|
||||
test "fails when removing a required custom field value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Create member with all required custom fields
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com",
|
||||
custom_field_values: custom_field_values
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com",
|
||||
custom_field_values: custom_field_values
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to update without the required custom field
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.update_member(member, %{custom_field_values: []})
|
||||
Membership.update_member(member, %{custom_field_values: []}, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
|
|
@ -545,18 +598,22 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
test "fails when setting required custom field value to empty",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Create member with all required custom fields
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com",
|
||||
custom_field_values: custom_field_values
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com",
|
||||
custom_field_values: custom_field_values
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to update with empty value for the string field
|
||||
updated_custom_field_values =
|
||||
|
|
@ -570,9 +627,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
end)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.update_member(member, %{
|
||||
custom_field_values: updated_custom_field_values
|
||||
})
|
||||
Membership.update_member(
|
||||
member,
|
||||
%{
|
||||
custom_field_values: updated_custom_field_values
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
|
|
@ -580,21 +641,25 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
test "succeeds when updating required custom field to valid value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Create member with all required custom fields
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com",
|
||||
custom_field_values: custom_field_values
|
||||
})
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com",
|
||||
custom_field_values: custom_field_values
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Load existing custom field values to get their IDs
|
||||
{:ok, member_with_cfvs} = Ash.load(member, :custom_field_values)
|
||||
{:ok, member_with_cfvs} = Ash.load(member, :custom_field_values, actor: actor)
|
||||
|
||||
# Update with new valid value for the string field, using existing IDs
|
||||
updated_custom_field_values =
|
||||
|
|
@ -620,9 +685,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
end)
|
||||
|
||||
assert {:ok, _updated_member} =
|
||||
Membership.update_member(member, %{
|
||||
custom_field_values: updated_custom_field_values
|
||||
})
|
||||
Membership.update_member(
|
||||
member,
|
||||
%{
|
||||
custom_field_values: updated_custom_field_values
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|
|
@ -18,7 +20,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|
|
@ -27,7 +29,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member3} =
|
||||
Member
|
||||
|
|
@ -36,7 +38,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
last_name: "Clark",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom fields for different types
|
||||
{:ok, string_field} =
|
||||
|
|
@ -45,7 +47,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "membership_number",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, integer_field} =
|
||||
CustomField
|
||||
|
|
@ -53,7 +55,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "member_id_number",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, email_field} =
|
||||
CustomField
|
||||
|
|
@ -61,7 +63,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "secondary_email",
|
||||
value_type: :email
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, date_field} =
|
||||
CustomField
|
||||
|
|
@ -69,7 +71,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "birthday",
|
||||
value_type: :date
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, boolean_field} =
|
||||
CustomField
|
||||
|
|
@ -77,7 +79,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "newsletter",
|
||||
value_type: :boolean
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
|
|
@ -87,12 +89,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
integer_field: integer_field,
|
||||
email_field: email_field,
|
||||
date_field: date_field,
|
||||
boolean_field: boolean_field
|
||||
boolean_field: boolean_field,
|
||||
system_actor: system_actor
|
||||
}
|
||||
end
|
||||
|
||||
describe "search with custom field values" do
|
||||
test "finds member by string custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -104,25 +108,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MEMBER12345"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update by reloading member
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MEMBER12345"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by integer custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
integer_field: integer_field
|
||||
} do
|
||||
|
|
@ -134,25 +139,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 42_424}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "42424"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by email custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
email_field: email_field
|
||||
} do
|
||||
|
|
@ -164,19 +170,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "alice.secondary@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for partial custom field value (should work via FTS or custom field filter)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "alice.secondary"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
|
@ -185,7 +191,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "alice.secondary@example.com"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_full) == 1
|
||||
assert List.first(results_full).id == member1.id
|
||||
|
|
@ -195,7 +201,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_domain =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "example.com"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Verify that member1 is in the results (may have other members too)
|
||||
ids = Enum.map(results_domain, & &1.id)
|
||||
|
|
@ -203,6 +209,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds member by date custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
date_field: date_field
|
||||
} do
|
||||
|
|
@ -214,25 +221,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: date_field.id,
|
||||
value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value (date is stored as text in search_vector)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "1990-05-15"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by boolean custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
boolean_field: boolean_field
|
||||
} do
|
||||
|
|
@ -244,25 +252,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: boolean_field.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value (boolean is stored as "true" or "false" text)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "true"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Note: "true" might match other things, so we check that member1 is in results
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value update triggers search_vector update", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -274,13 +283,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "OLDVALUE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Update custom field value
|
||||
{:ok, _updated_cfv} =
|
||||
|
|
@ -288,13 +297,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
|> Ash.Changeset.for_update(:update, %{
|
||||
value: %{"_union_type" => "string", "_union_value" => "NEWVALUE123"}
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the new value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "NEWVALUE123"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
|
@ -303,12 +312,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
old_results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "OLDVALUE"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
refute Enum.any?(old_results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value delete triggers search_vector update", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -320,19 +330,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "TOBEDELETED"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Verify it's searchable
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "TOBEDELETED"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
|
@ -344,12 +354,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
deleted_results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "TOBEDELETED"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
refute Enum.any?(deleted_results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value create triggers search_vector update", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -361,19 +372,20 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "AUTOUPDATE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Search should find it immediately (trigger should have updated search_vector)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "AUTOUPDATE"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "member update includes custom field values in search_vector", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -385,25 +397,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MEMBERUPDATE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Update member (should trigger search_vector update including custom fields)
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{notes: "Updated notes"})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search should find the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MEMBERUPDATE"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "multiple custom field values are all searchable", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field,
|
||||
integer_field: integer_field,
|
||||
|
|
@ -417,7 +430,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MULTI1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -426,7 +439,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 99_999}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|
|
@ -435,38 +448,39 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "multi@test.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# All values should be searchable
|
||||
results1 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MULTI1"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results1, fn m -> m.id == member1.id end)
|
||||
|
||||
results2 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "99999"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results2, fn m -> m.id == member1.id end)
|
||||
|
||||
results3 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "multi@test.com"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results3, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "finds member by custom field value with numbers in text field (e.g. phone number)", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -478,19 +492,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "M-123-456"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for full value (should work via search_vector)
|
||||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "M-123-456"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results_full, fn m -> m.id == member1.id end),
|
||||
"Full value search should find member via search_vector"
|
||||
|
|
@ -501,6 +515,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds member by phone number in Emergency Contact custom field", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1
|
||||
} do
|
||||
# Create Emergency Contact custom field
|
||||
|
|
@ -510,7 +525,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "Emergency Contact",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field value with phone number
|
||||
phone_number = "+49 123 456789"
|
||||
|
|
@ -522,19 +537,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: emergency_contact_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => phone_number}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for full phone number (should work via search_vector)
|
||||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: phone_number})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results_full, fn m -> m.id == member1.id end),
|
||||
"Full phone number search should find member via search_vector"
|
||||
|
|
@ -547,6 +562,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
|
||||
describe "custom field substring search (ILIKE)" do
|
||||
test "finds member by prefix of custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -558,14 +574,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Premium"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Test prefix searches - should all find the member
|
||||
for prefix <- ["Premium", "Premiu", "Premi", "Prem", "Pre"] do
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: prefix})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end),
|
||||
"Prefix '#{prefix}' should find member with custom field 'Premium'"
|
||||
|
|
@ -573,6 +589,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "custom field search is case-insensitive", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -584,7 +601,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "GoldMember"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Test case variations - should all find the member
|
||||
for variant <- [
|
||||
|
|
@ -599,7 +616,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: variant})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end),
|
||||
"Case variant '#{variant}' should find member with custom field 'GoldMember'"
|
||||
|
|
@ -607,6 +624,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds member by suffix/middle of custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -618,14 +636,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "ActiveMember"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Test suffix and middle substring searches
|
||||
for substring <- ["Member", "ember", "tiveMem", "ctive"] do
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: substring})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end),
|
||||
"Substring '#{substring}' should find member with custom field 'ActiveMember'"
|
||||
|
|
@ -633,6 +651,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds correct member among multiple with different custom field values", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
member3: member3,
|
||||
|
|
@ -646,7 +665,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Beginner"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -655,7 +674,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Advanced"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|
|
@ -664,13 +683,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Expert"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Search for "Begin" - should only find member1
|
||||
results_begin =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "Begin"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_begin) == 1
|
||||
assert List.first(results_begin).id == member1.id
|
||||
|
|
@ -679,7 +698,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_advan =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "Advan"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_advan) == 1
|
||||
assert List.first(results_advan).id == member2.id
|
||||
|
|
@ -688,7 +707,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_exper =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "Exper"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_exper) == 1
|
||||
assert List.first(results_exper).id == member3.id
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ defmodule Mv.Membership.MemberTest do
|
|||
use Mv.DataCase, async: false
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Fields and Validations" do
|
||||
@valid_attrs %{
|
||||
first_name: "John",
|
||||
|
|
@ -16,60 +21,74 @@ defmodule Mv.Membership.MemberTest do
|
|||
postal_code: "12345"
|
||||
}
|
||||
|
||||
test "First name is optional" do
|
||||
test "First name is optional", %{actor: actor} do
|
||||
attrs = Map.delete(@valid_attrs, :first_name)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Last name is optional" do
|
||||
test "Last name is optional", %{actor: actor} do
|
||||
attrs = Map.delete(@valid_attrs, :last_name)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Email is required" do
|
||||
test "Email is required", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :email, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :email) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Email must be valid" do
|
||||
test "Email must be valid", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :email, "test@")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :email) =~ "is not a valid email"
|
||||
end
|
||||
|
||||
test "Join date cannot be in the future" do
|
||||
test "Join date cannot be in the future", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1))
|
||||
|
||||
assert {:error,
|
||||
%Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidAttribute{field: :join_date}]}} =
|
||||
Membership.create_member(attrs)
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Exit date is optional but must not be before join date if both are specified" do
|
||||
test "Exit date is optional but must not be before join date if both are specified", %{
|
||||
actor: actor
|
||||
} do
|
||||
attrs = Map.put(@valid_attrs, :exit_date, ~D[2010-01-01])
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :exit_date) =~ "cannot be before join date"
|
||||
attrs2 = Map.delete(@valid_attrs, :exit_date)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2, actor: actor)
|
||||
end
|
||||
|
||||
test "Notes is optional" do
|
||||
test "Notes is optional", %{actor: actor} do
|
||||
attrs = Map.delete(@valid_attrs, :notes)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "City, street, house number are optional" do
|
||||
test "City, street, house number are optional", %{actor: actor} do
|
||||
attrs = @valid_attrs |> Map.drop([:city, :street, :house_number])
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Postal code is optional but must have 5 digits if specified" do
|
||||
test "Postal code is optional but must have 5 digits if specified", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :postal_code, "1234")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :postal_code) =~ "must consist of 5 digits"
|
||||
attrs2 = Map.delete(@valid_attrs, :postal_code)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2, actor: actor)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,13 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
defp create_fee_type(attrs, actor) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -23,11 +28,11 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
defp create_member(attrs, actor) do
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -39,11 +44,11 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
defp create_cycle(member, fee_type, attrs, actor) do
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2024-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -56,17 +61,17 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
describe "type change cycle regeneration" do
|
||||
test "future unpaid cycles are regenerated with new amount" do
|
||||
test "future unpaid cycles are regenerated with new amount", %{actor: actor} do
|
||||
today = Date.utc_today()
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")})
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)
|
||||
|
||||
# Create member without fee type first to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
member = create_member(%{}, actor)
|
||||
|
||||
# Manually assign fee type (this will trigger cycle generation)
|
||||
member =
|
||||
|
|
@ -74,7 +79,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type1.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Cycle generation runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
@ -89,26 +94,31 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
# Check if it already exists (from auto-generation), if not create it
|
||||
case MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start)
|
||||
|> Ash.read_one() do
|
||||
|> Ash.read_one(actor: actor) do
|
||||
{:ok, existing_cycle} when not is_nil(existing_cycle) ->
|
||||
# Update to paid
|
||||
existing_cycle
|
||||
|> Ash.Changeset.for_update(:update, %{status: :paid})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
_ ->
|
||||
create_cycle(member, yearly_type1, %{
|
||||
cycle_start: past_cycle_start,
|
||||
status: :paid,
|
||||
amount: Decimal.new("100.00")
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
yearly_type1,
|
||||
%{
|
||||
cycle_start: past_cycle_start,
|
||||
status: :paid,
|
||||
amount: Decimal.new("100.00")
|
||||
},
|
||||
actor
|
||||
)
|
||||
end
|
||||
|
||||
# Current cycle (unpaid) - should be regenerated
|
||||
# Delete if exists (from auto-generation), then create with old amount
|
||||
case MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
|
||||
|> Ash.read_one() do
|
||||
|> Ash.read_one(actor: actor) do
|
||||
{:ok, existing_cycle} when not is_nil(existing_cycle) ->
|
||||
Ash.destroy!(existing_cycle)
|
||||
|
||||
|
|
@ -117,11 +127,16 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
end
|
||||
|
||||
_current_cycle =
|
||||
create_cycle(member, yearly_type1, %{
|
||||
cycle_start: current_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
yearly_type1,
|
||||
%{
|
||||
cycle_start: current_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Change membership fee type (same interval, different amount)
|
||||
assert {:ok, _updated_member} =
|
||||
|
|
@ -129,7 +144,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type2.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Cycle regeneration runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
@ -138,7 +153,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
past_cycle_after =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|
||||
assert past_cycle_after.status == :paid
|
||||
assert Decimal.equal?(past_cycle_after.amount, Decimal.new("100.00"))
|
||||
|
|
@ -149,7 +164,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
new_current_cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|
||||
# Verify it has the new type and amount
|
||||
assert new_current_cycle.membership_fee_type_id == yearly_type2.id
|
||||
|
|
@ -163,18 +178,18 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
member_id == ^member.id and cycle_start == ^current_cycle_start and
|
||||
membership_fee_type_id == ^yearly_type1.id
|
||||
)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
assert Enum.empty?(old_current_cycles)
|
||||
end
|
||||
|
||||
test "paid cycles remain unchanged" do
|
||||
test "paid cycles remain unchanged", %{actor: actor} do
|
||||
today = Date.utc_today()
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")})
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)
|
||||
|
||||
# Create member without fee type first to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
member = create_member(%{}, actor)
|
||||
|
||||
# Manually assign fee type (this will trigger cycle generation)
|
||||
member =
|
||||
|
|
@ -182,7 +197,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type1.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Cycle generation runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
@ -194,9 +209,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
paid_cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:mark_as_paid)
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Change membership fee type
|
||||
assert {:ok, _updated_member} =
|
||||
|
|
@ -204,25 +219,25 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type2.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Cycle regeneration runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
||||
# Verify paid cycle is unchanged (not deleted and regenerated)
|
||||
{:ok, cycle_after} = Ash.get(MembershipFeeCycle, paid_cycle.id)
|
||||
{:ok, cycle_after} = Ash.get(MembershipFeeCycle, paid_cycle.id, actor: actor)
|
||||
assert cycle_after.status == :paid
|
||||
assert Decimal.equal?(cycle_after.amount, Decimal.new("100.00"))
|
||||
assert cycle_after.membership_fee_type_id == yearly_type1.id
|
||||
end
|
||||
|
||||
test "suspended cycles remain unchanged" do
|
||||
test "suspended cycles remain unchanged", %{actor: actor} do
|
||||
today = Date.utc_today()
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")})
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)
|
||||
|
||||
# Create member without fee type first to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
member = create_member(%{}, actor)
|
||||
|
||||
# Manually assign fee type (this will trigger cycle generation)
|
||||
member =
|
||||
|
|
@ -230,7 +245,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type1.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Cycle generation runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
@ -242,9 +257,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
suspended_cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:mark_as_suspended)
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Change membership fee type
|
||||
assert {:ok, _updated_member} =
|
||||
|
|
@ -252,25 +267,25 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type2.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Cycle regeneration runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
||||
# Verify suspended cycle is unchanged (not deleted and regenerated)
|
||||
{:ok, cycle_after} = Ash.get(MembershipFeeCycle, suspended_cycle.id)
|
||||
{:ok, cycle_after} = Ash.get(MembershipFeeCycle, suspended_cycle.id, actor: actor)
|
||||
assert cycle_after.status == :suspended
|
||||
assert Decimal.equal?(cycle_after.amount, Decimal.new("100.00"))
|
||||
assert cycle_after.membership_fee_type_id == yearly_type1.id
|
||||
end
|
||||
|
||||
test "only cycles that haven't ended yet are deleted" do
|
||||
test "only cycles that haven't ended yet are deleted", %{actor: actor} do
|
||||
today = Date.utc_today()
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")})
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)
|
||||
|
||||
# Create member without fee type first to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
member = create_member(%{}, actor)
|
||||
|
||||
# Manually assign fee type (this will trigger cycle generation)
|
||||
member =
|
||||
|
|
@ -278,7 +293,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type1.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Cycle generation runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
@ -296,7 +311,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
# Delete existing cycle if it exists (from auto-generation)
|
||||
case MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start)
|
||||
|> Ash.read_one() do
|
||||
|> Ash.read_one(actor: actor) do
|
||||
{:ok, existing_cycle} when not is_nil(existing_cycle) ->
|
||||
Ash.destroy!(existing_cycle)
|
||||
|
||||
|
|
@ -305,17 +320,22 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
end
|
||||
|
||||
past_cycle =
|
||||
create_cycle(member, yearly_type1, %{
|
||||
cycle_start: past_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
yearly_type1,
|
||||
%{
|
||||
cycle_start: past_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Current cycle (unpaid) - should be regenerated (cycle_start >= today)
|
||||
# Delete existing cycle if it exists (from auto-generation)
|
||||
case MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
|
||||
|> Ash.read_one() do
|
||||
|> Ash.read_one(actor: actor) do
|
||||
{:ok, existing_cycle} when not is_nil(existing_cycle) ->
|
||||
Ash.destroy!(existing_cycle)
|
||||
|
||||
|
|
@ -324,11 +344,16 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
end
|
||||
|
||||
_current_cycle =
|
||||
create_cycle(member, yearly_type1, %{
|
||||
cycle_start: current_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
yearly_type1,
|
||||
%{
|
||||
cycle_start: current_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Change membership fee type
|
||||
assert {:ok, _updated_member} =
|
||||
|
|
@ -336,13 +361,13 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type2.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Cycle regeneration runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
||||
# Verify past cycle is unchanged
|
||||
{:ok, past_cycle_after} = Ash.get(MembershipFeeCycle, past_cycle.id)
|
||||
{:ok, past_cycle_after} = Ash.get(MembershipFeeCycle, past_cycle.id, actor: actor)
|
||||
assert past_cycle_after.status == :unpaid
|
||||
assert Decimal.equal?(past_cycle_after.amount, Decimal.new("100.00"))
|
||||
assert past_cycle_after.membership_fee_type_id == yearly_type1.id
|
||||
|
|
@ -352,7 +377,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
new_current_cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|
||||
assert new_current_cycle.membership_fee_type_id == yearly_type2.id
|
||||
assert Decimal.equal?(new_current_cycle.amount, Decimal.new("150.00"))
|
||||
|
|
@ -364,19 +389,19 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
member_id == ^member.id and cycle_start == ^current_cycle_start and
|
||||
membership_fee_type_id == ^yearly_type1.id
|
||||
)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
assert Enum.empty?(old_current_cycles)
|
||||
end
|
||||
|
||||
test "member calculations update after type change" do
|
||||
test "member calculations update after type change", %{actor: actor} do
|
||||
today = Date.utc_today()
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")})
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)
|
||||
|
||||
# Create member with join_date = today to avoid past cycles
|
||||
# This ensures no overdue cycles exist
|
||||
member = create_member(%{join_date: today})
|
||||
member = create_member(%{join_date: today}, actor)
|
||||
|
||||
# Manually assign fee type (this will trigger cycle generation)
|
||||
member =
|
||||
|
|
@ -384,7 +409,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type1.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Cycle generation runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
@ -397,7 +422,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
existing_cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
Enum.each(existing_cycles, fn cycle ->
|
||||
if cycle.cycle_start != current_cycle_start do
|
||||
|
|
@ -408,22 +433,27 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
# Ensure current cycle exists and is unpaid
|
||||
case MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
|
||||
|> Ash.read_one() do
|
||||
|> Ash.read_one(actor: actor) do
|
||||
{:ok, existing_cycle} when not is_nil(existing_cycle) ->
|
||||
# Update to unpaid if it's not
|
||||
if existing_cycle.status != :unpaid do
|
||||
existing_cycle
|
||||
|> Ash.Changeset.for_update(:mark_as_unpaid)
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Create if it doesn't exist
|
||||
create_cycle(member, yearly_type1, %{
|
||||
cycle_start: current_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
})
|
||||
create_cycle(
|
||||
member,
|
||||
yearly_type1,
|
||||
%{
|
||||
cycle_start: current_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
},
|
||||
actor
|
||||
)
|
||||
end
|
||||
|
||||
# Load calculations before change
|
||||
|
|
@ -437,7 +467,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type2.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Cycle regeneration runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
alias Mv.Membership.Setting
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "membership fee settings" do
|
||||
test "default values are correct" do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
|
@ -18,7 +23,7 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
assert %Setting{} = settings
|
||||
end
|
||||
|
||||
test "settings can be written via update_membership_fee_settings" do
|
||||
test "settings can be written via update_membership_fee_settings", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
{:ok, updated} =
|
||||
|
|
@ -26,12 +31,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
include_joining_cycle: false
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated.include_joining_cycle == false
|
||||
end
|
||||
|
||||
test "default_membership_fee_type_id can be nil (optional)" do
|
||||
test "default_membership_fee_type_id can be nil (optional)", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
{:ok, updated} =
|
||||
|
|
@ -39,12 +44,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: nil
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated.default_membership_fee_type_id == nil
|
||||
end
|
||||
|
||||
test "default_membership_fee_type_id validation: must exist if set" do
|
||||
test "default_membership_fee_type_id validation: must exist if set", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
# Create a valid fee type
|
||||
|
|
@ -61,12 +66,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated.default_membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
||||
test "default_membership_fee_type_id validation: fails if not found" do
|
||||
test "default_membership_fee_type_id validation: fails if not found", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
# Use a non-existent UUID
|
||||
|
|
@ -77,7 +82,7 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fake_uuid
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert error_on_field?(error, :default_membership_fee_type_id)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue