Add actor parameter to all tests requiring authorization
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:
Moritz 2026-01-23 20:00:24 +01:00
parent 4c846f8bba
commit a6cdeaa18d
Signed by: moritz
GPG key ID: 1020A035E5DD0824
75 changed files with 4649 additions and 2865 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 ->

View file

@ -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)

View file

@ -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)

View file

@ -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])

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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