[Refactor] Remove NoActor bypass #367
75 changed files with 4686 additions and 2859 deletions
|
|
@ -85,10 +85,13 @@ defmodule Mv.MembershipFees.MembershipFeeType do
|
|||
if changeset.action_type == :destroy do
|
||||
require Ash.Query
|
||||
|
||||
# Use system_actor for validation queries (systemic operation)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
member_count =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id)
|
||||
|> Ash.count!()
|
||||
|> Ash.count!(actor: system_actor)
|
||||
|
||||
if member_count > 0 do
|
||||
{:error,
|
||||
|
|
@ -108,10 +111,13 @@ defmodule Mv.MembershipFees.MembershipFeeType do
|
|||
if changeset.action_type == :destroy do
|
||||
require Ash.Query
|
||||
|
||||
# Use system_actor for validation queries (systemic operation)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
cycle_count =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id)
|
||||
|> Ash.count!()
|
||||
|> Ash.count!(actor: system_actor)
|
||||
|
||||
if cycle_count > 0 do
|
||||
{:error,
|
||||
|
|
@ -131,10 +137,13 @@ defmodule Mv.MembershipFees.MembershipFeeType do
|
|||
if changeset.action_type == :destroy do
|
||||
require Ash.Query
|
||||
|
||||
# Use system_actor for validation queries (systemic operation)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
setting_count =
|
||||
Mv.Membership.Setting
|
||||
|> Ash.Query.filter(default_membership_fee_type_id == ^changeset.data.id)
|
||||
|> Ash.count!()
|
||||
|> Ash.count!(actor: system_actor)
|
||||
|
||||
if setting_count > 0 do
|
||||
{:error,
|
||||
|
|
|
|||
|
|
@ -512,7 +512,10 @@ defmodule Mv.Membership.Import.MemberCSV do
|
|||
member_attrs_with_cf
|
||||
end
|
||||
|
||||
case Mv.Membership.create_member(final_attrs) do
|
||||
# Use system_actor for CSV imports (systemic operation)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
case Mv.Membership.create_member(final_attrs, actor: system_actor) do
|
||||
{:ok, member} ->
|
||||
{:ok, member}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Email sync edge cases" do
|
||||
@valid_user_attrs %{
|
||||
email: "user@example.com"
|
||||
|
|
@ -18,15 +23,15 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do
|
|||
email: "member@example.com"
|
||||
}
|
||||
|
||||
test "simultaneous email updates use user email as source of truth" do
|
||||
test "simultaneous email updates use user email as source of truth", %{actor: actor} do
|
||||
# Create linked user and member
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
|
||||
|
||||
# Verify link and initial sync
|
||||
{: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"
|
||||
|
||||
# Scenario: Both emails are updated "simultaneously"
|
||||
|
|
@ -35,58 +40,60 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do
|
|||
|
||||
# Update member email first
|
||||
{:ok, _updated_member} =
|
||||
Membership.update_member(member, %{email: "member-new@example.com"})
|
||||
Membership.update_member(member, %{email: "member-new@example.com"}, actor: actor)
|
||||
|
||||
# Verify it synced to user
|
||||
{:ok, user_after_member_update} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, user_after_member_update} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert to_string(user_after_member_update.email) == "member-new@example.com"
|
||||
|
||||
# Now update user email - this should override
|
||||
{:ok, _updated_user} =
|
||||
Accounts.update_user(user_after_member_update, %{email: "user-final@example.com"})
|
||||
Accounts.update_user(user_after_member_update, %{email: "user-final@example.com"},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Reload both
|
||||
{:ok, final_user} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, final_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, final_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
{:ok, final_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
|
||||
# User email should be the final truth
|
||||
assert to_string(final_user.email) == "user-final@example.com"
|
||||
assert final_member.email == "user-final@example.com"
|
||||
end
|
||||
|
||||
test "email validation works for both user and member" do
|
||||
test "email validation works for both user and member", %{actor: actor} do
|
||||
# Test that invalid emails are rejected for both resources
|
||||
|
||||
# Invalid email for user
|
||||
invalid_user_result = Accounts.create_user(%{email: "not-an-email"})
|
||||
invalid_user_result = Accounts.create_user(%{email: "not-an-email"}, actor: actor)
|
||||
assert {:error, %Ash.Error.Invalid{}} = invalid_user_result
|
||||
|
||||
# Invalid email for member
|
||||
invalid_member_attrs = Map.put(@valid_member_attrs, :email, "also-not-an-email")
|
||||
invalid_member_result = Membership.create_member(invalid_member_attrs)
|
||||
invalid_member_result = Membership.create_member(invalid_member_attrs, actor: actor)
|
||||
assert {:error, %Ash.Error.Invalid{}} = invalid_member_result
|
||||
|
||||
# Valid emails should work
|
||||
{:ok, _user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, _member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, _user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
{:ok, _member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "identity constraints prevent duplicate emails" do
|
||||
test "identity constraints prevent duplicate emails", %{actor: actor} do
|
||||
# Create first user with an email
|
||||
{:ok, user1} = Accounts.create_user(%{email: "duplicate@example.com"})
|
||||
{:ok, user1} = Accounts.create_user(%{email: "duplicate@example.com"}, actor: actor)
|
||||
assert to_string(user1.email) == "duplicate@example.com"
|
||||
|
||||
# Try to create second user with same email - should fail due to unique constraint
|
||||
result = Accounts.create_user(%{email: "duplicate@example.com"})
|
||||
result = Accounts.create_user(%{email: "duplicate@example.com"}, actor: actor)
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
||||
# Same for members
|
||||
member_attrs = Map.put(@valid_member_attrs, :email, "member-dup@example.com")
|
||||
{:ok, member1} = Membership.create_member(member_attrs)
|
||||
{:ok, member1} = Membership.create_member(member_attrs, actor: actor)
|
||||
assert member1.email == "member-dup@example.com"
|
||||
|
||||
# Try to create second member with same email - should fail
|
||||
result2 = Membership.create_member(member_attrs)
|
||||
result2 = Membership.create_member(member_attrs, actor: actor)
|
||||
assert {:error, %Ash.Error.Invalid{}} = result2
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,121 +4,177 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Email uniqueness validation - Creation" do
|
||||
test "CAN create member with existing unlinked user email" do
|
||||
test "CAN create member with existing unlinked user email", %{actor: actor} do
|
||||
# Create a user with email
|
||||
{:ok, _user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "existing@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create member with same email - should succeed
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "existing@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert to_string(member.email) == "existing@example.com"
|
||||
end
|
||||
|
||||
test "CAN create user with existing unlinked member email" do
|
||||
test "CAN create user with existing unlinked member email", %{actor: actor} do
|
||||
# Create a member with email
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "existing@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create user with same email - should succeed
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "existing@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert to_string(user.email) == "existing@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Email uniqueness validation - Updating unlinked entities" do
|
||||
test "unlinked member email CAN be changed to an existing unlinked user email" do
|
||||
test "unlinked member email CAN be changed to an existing unlinked user email", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create a user with email
|
||||
{:ok, _user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "existing_user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create an unlinked member with different email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "member@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Change member email to existing user email - should succeed (member is unlinked)
|
||||
{:ok, updated_member} =
|
||||
Membership.update_member(member, %{
|
||||
Membership.update_member(
|
||||
member,
|
||||
%{
|
||||
email: "existing_user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert to_string(updated_member.email) == "existing_user@example.com"
|
||||
end
|
||||
|
||||
test "unlinked user email CAN be changed to an existing unlinked member email" do
|
||||
test "unlinked user email CAN be changed to an existing unlinked member email", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create a member with email
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "existing_member@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create an unlinked user with different email
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Change user email to existing member email - should succeed (user is unlinked)
|
||||
{:ok, updated_user} =
|
||||
Accounts.update_user(user, %{
|
||||
Accounts.update_user(
|
||||
user,
|
||||
%{
|
||||
email: "existing_member@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert to_string(updated_user.email) == "existing_member@example.com"
|
||||
end
|
||||
|
||||
test "unlinked member email CANNOT be changed to an existing linked user email" do
|
||||
test "unlinked member email CANNOT be changed to an existing linked user email", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create a user and link it to a member - this makes the user "linked"
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "linked_user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _member_a} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Member",
|
||||
last_name: "A",
|
||||
email: "temp@example.com",
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create an unlinked member with different email
|
||||
{:ok, member_b} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Member",
|
||||
last_name: "B",
|
||||
email: "member_b@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to change unlinked member's email to linked user's email - should fail
|
||||
result =
|
||||
Membership.update_member(member_b, %{
|
||||
Membership.update_member(
|
||||
member_b,
|
||||
%{
|
||||
email: "linked_user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} = result
|
||||
|
||||
|
|
@ -129,37 +185,52 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "unlinked user email CANNOT be changed to an existing linked member email" do
|
||||
test "unlinked user email CANNOT be changed to an existing linked member email", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create a user and link it to a member - this makes the member "linked"
|
||||
{:ok, user_a} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user_a@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _member_a} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Member",
|
||||
last_name: "A",
|
||||
email: "temp@example.com",
|
||||
user: %{id: user_a.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Reload user to get updated member_id and linked member email
|
||||
{:ok, user_a_reloaded} = Ash.get(Mv.Accounts.User, user_a.id)
|
||||
{:ok, user_a_with_member} = Ash.load(user_a_reloaded, :member)
|
||||
{:ok, user_a_reloaded} = Ash.get(Mv.Accounts.User, user_a.id, actor: actor)
|
||||
{:ok, user_a_with_member} = Ash.load(user_a_reloaded, :member, actor: actor)
|
||||
linked_member_email = to_string(user_a_with_member.member.email)
|
||||
|
||||
# Create an unlinked user with different email
|
||||
{:ok, user_b} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user_b@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to change unlinked user's email to linked member's email - should fail
|
||||
result =
|
||||
Accounts.update_user(user_b, %{
|
||||
Accounts.update_user(
|
||||
user_b,
|
||||
%{
|
||||
email: linked_member_email
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} = result
|
||||
|
||||
|
|
@ -172,28 +243,37 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
end
|
||||
|
||||
describe "Email uniqueness validation - Creating with linked emails" do
|
||||
test "CANNOT create member with existing linked user email" do
|
||||
test "CANNOT create member with existing linked user email", %{actor: actor} do
|
||||
# Create a user and link it to a member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "linked@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "First",
|
||||
last_name: "Member",
|
||||
email: "temp@example.com",
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to create a new member with the linked user's email - should fail
|
||||
result =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Second",
|
||||
last_name: "Member",
|
||||
email: "linked@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} = result
|
||||
|
||||
|
|
@ -204,31 +284,40 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "CANNOT create user with existing linked member email" do
|
||||
test "CANNOT create user with existing linked member email", %{actor: actor} do
|
||||
# Create a user and link it to a member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Member",
|
||||
last_name: "One",
|
||||
email: "temp@example.com",
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Reload user to get the linked member's email
|
||||
{:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, user_with_member} = Ash.load(user_reloaded, :member)
|
||||
{:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
{:ok, user_with_member} = Ash.load(user_reloaded, :member, actor: actor)
|
||||
linked_member_email = to_string(user_with_member.member.email)
|
||||
|
||||
# Try to create a new user with the linked member's email - should fail
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: linked_member_email
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} = result
|
||||
|
||||
|
|
@ -241,32 +330,45 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
end
|
||||
|
||||
describe "Email uniqueness validation - Updating linked entities" do
|
||||
test "linked member email CANNOT be changed to an existing user email" do
|
||||
test "linked member email CANNOT be changed to an existing user email", %{actor: actor} do
|
||||
# Create a user with email
|
||||
{:ok, _other_user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "other_user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a user and link it to a member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "temp@example.com",
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to change linked member's email to other user's email - should fail
|
||||
result =
|
||||
Membership.update_member(member, %{
|
||||
Membership.update_member(
|
||||
member,
|
||||
%{
|
||||
email: "other_user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} = result
|
||||
|
||||
|
|
@ -277,37 +379,50 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "linked user email CANNOT be changed to an existing member email" do
|
||||
test "linked user email CANNOT be changed to an existing member email", %{actor: actor} do
|
||||
# Create a member with email
|
||||
{:ok, _other_member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Doe",
|
||||
email: "other_member@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a user and link it to a member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "temp@example.com",
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
|
||||
# Try to change linked user's email to other member's email - should fail
|
||||
result =
|
||||
Accounts.update_user(user_reloaded, %{
|
||||
Accounts.update_user(
|
||||
user_reloaded,
|
||||
%{
|
||||
email: "other_member@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} = result
|
||||
|
||||
|
|
@ -320,34 +435,49 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
end
|
||||
|
||||
describe "Email uniqueness validation - Linking" do
|
||||
test "CANNOT link user to member if user email is already used by another unlinked member" do
|
||||
test "CANNOT link user to member if user email is already used by another unlinked member", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create a member with email
|
||||
{:ok, _other_member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Doe",
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a user with same email
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a member to link with the user
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Smith",
|
||||
email: "john@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to link user to member - should fail because user.email is already used by other_member
|
||||
result =
|
||||
Accounts.update_user(user, %{
|
||||
Accounts.update_user(
|
||||
user,
|
||||
%{
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} = result
|
||||
|
||||
|
|
@ -358,120 +488,160 @@ defmodule Mv.Accounts.EmailUniquenessTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "CAN link member to user even if member email is used by another user (member email gets overridden)" do
|
||||
test "CAN link member to user even if member email is used by another user (member email gets overridden)",
|
||||
%{actor: actor} do
|
||||
# Create a user with email
|
||||
{:ok, _other_user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a member with same email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a user to link with the member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Link member to user - should succeed because member.email will be overridden
|
||||
{:ok, updated_member} =
|
||||
Membership.update_member(member, %{
|
||||
Membership.update_member(
|
||||
member,
|
||||
%{
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Member email should now be the same as user email
|
||||
{:ok, member_reloaded} = Ash.get(Mv.Membership.Member, updated_member.id)
|
||||
{:ok, member_reloaded} = Ash.get(Mv.Membership.Member, updated_member.id, actor: actor)
|
||||
assert to_string(member_reloaded.email) == "user@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Email syncing" do
|
||||
test "member email syncs to linked user email without validation error" do
|
||||
test "member email syncs to linked user email without validation error", %{actor: actor} do
|
||||
# Create a user
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a member linked to this user
|
||||
# The override change will set member.email = user.email automatically
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "member@example.com",
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Member email should have been overridden to user email
|
||||
# This happens through our sync mechanism, which should NOT trigger
|
||||
# the "email already used" validation because it's the same user
|
||||
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert member_after_link.email == "user@example.com"
|
||||
end
|
||||
|
||||
test "user email syncs to linked member without validation error" do
|
||||
test "user email syncs to linked member without validation error", %{actor: actor} do
|
||||
# Create a member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "member@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a user linked to this member
|
||||
# The override change will set member.email = user.email automatically
|
||||
{:ok, _user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Member email should have been overridden to user email
|
||||
# This happens through our sync mechanism, which should NOT trigger
|
||||
# the "email already used" validation because it's the same member
|
||||
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert member_after_link.email == "user@example.com"
|
||||
end
|
||||
|
||||
test "two unlinked users cannot have the same email" do
|
||||
test "two unlinked users cannot have the same email", %{actor: actor} do
|
||||
# Create first user
|
||||
{:ok, _user1} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to create second user with same email
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
end
|
||||
|
||||
test "two unlinked members cannot have the same email (members have unique constraint)" do
|
||||
test "two unlinked members cannot have the same email (members have unique constraint)", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create first member
|
||||
{:ok, _member1} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to create second member with same email - should fail
|
||||
result =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
# Members DO have a unique email constraint at database level
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ defmodule Mv.Accounts.UserAuthenticationTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Password authentication user identification" do
|
||||
@tag :test_proposal
|
||||
test "password login uses email as identifier" do
|
||||
|
|
@ -27,7 +32,7 @@ defmodule Mv.Accounts.UserAuthenticationTest do
|
|||
{:ok, users} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(email == ^email_to_find)
|
||||
|> Ash.read()
|
||||
|> Ash.read(actor: user)
|
||||
|
||||
assert length(users) == 1
|
||||
found_user = List.first(users)
|
||||
|
|
@ -113,11 +118,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
|
|||
# Use sign_in_with_rauthy to find user by oidc_id
|
||||
# Note: This test will FAIL until we implement the security fix
|
||||
# that changes the filter from email to oidc_id
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
case result do
|
||||
{:ok, [found_user]} ->
|
||||
|
|
@ -141,11 +151,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
|
|||
}
|
||||
|
||||
# Should create via register_with_rauthy
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, new_user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert to_string(new_user.email) == "newuser@example.com"
|
||||
assert new_user.oidc_id == "brand_new_oidc_789"
|
||||
|
|
@ -170,12 +185,12 @@ defmodule Mv.Accounts.UserAuthenticationTest do
|
|||
{:ok, users1} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(oidc_id == "oidc_unique_1")
|
||||
|> Ash.read()
|
||||
|> Ash.read(actor: user1)
|
||||
|
||||
{:ok, users2} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(oidc_id == "oidc_unique_2")
|
||||
|> Ash.read()
|
||||
|> Ash.read(actor: user2)
|
||||
|
||||
assert length(users1) == 1
|
||||
assert length(users2) == 1
|
||||
|
|
@ -205,11 +220,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
|
|||
}
|
||||
|
||||
# Should NOT find the user (security requirement)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
@ -241,11 +261,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
|
|||
}
|
||||
|
||||
# Should NOT find the user because oidc_id is nil
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ defmodule Mv.Accounts.UserEmailSyncTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "User email synchronization to linked Member" do
|
||||
@valid_user_attrs %{
|
||||
email: "user@example.com"
|
||||
|
|
@ -19,96 +24,100 @@ defmodule Mv.Accounts.UserEmailSyncTest do
|
|||
email: "member@example.com"
|
||||
}
|
||||
|
||||
test "updating user email syncs to linked member" do
|
||||
test "updating user email syncs to linked member", %{actor: actor} do
|
||||
# Create a 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"
|
||||
|
||||
# Create a user linked to the member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
|
||||
|
||||
# Verify initial state - member email should be overridden by user email
|
||||
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert member_after_link.email == "user@example.com"
|
||||
|
||||
# Update user email
|
||||
{:ok, updated_user} = Accounts.update_user(user, %{email: "newemail@example.com"})
|
||||
{:ok, updated_user} =
|
||||
Accounts.update_user(user, %{email: "newemail@example.com"}, actor: actor)
|
||||
|
||||
assert to_string(updated_user.email) == "newemail@example.com"
|
||||
|
||||
# Verify member email was also updated
|
||||
{: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 == "newemail@example.com"
|
||||
end
|
||||
|
||||
test "creating user linked to member overrides member email" do
|
||||
test "creating user linked to member overrides member email", %{actor: actor} do
|
||||
# Create a member with their own email
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
assert member.email == "member@example.com"
|
||||
|
||||
# Create a user linked to this member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
|
||||
|
||||
assert to_string(user.email) == "user@example.com"
|
||||
assert user.member_id == member.id
|
||||
|
||||
# Verify member email was overridden with user email
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert updated_member.email == "user@example.com"
|
||||
end
|
||||
|
||||
test "linking user to existing member syncs user email to member" do
|
||||
test "linking user to existing member syncs user email to member", %{actor: actor} do
|
||||
# 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"
|
||||
|
||||
# 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"
|
||||
assert user.member_id == nil
|
||||
|
||||
# Link the user to the member
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
|
||||
assert linked_user.member_id == member.id
|
||||
|
||||
# Verify member email was overridden with 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"
|
||||
end
|
||||
|
||||
test "updating user email when no member linked does not error" do
|
||||
test "updating user email when no member linked does not error", %{actor: actor} do
|
||||
# Create a standalone user without member link
|
||||
{: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"
|
||||
assert user.member_id == nil
|
||||
|
||||
# Update user email - should work fine without error
|
||||
{:ok, updated_user} = Accounts.update_user(user, %{email: "newemail@example.com"})
|
||||
{:ok, updated_user} =
|
||||
Accounts.update_user(user, %{email: "newemail@example.com"}, actor: actor)
|
||||
|
||||
assert to_string(updated_user.email) == "newemail@example.com"
|
||||
assert updated_user.member_id == nil
|
||||
end
|
||||
|
||||
test "unlinking user from member does not sync email" do
|
||||
test "unlinking user from member does not sync email", %{actor: actor} do
|
||||
# Create member
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
# Create user linked to member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
|
||||
|
||||
assert user.member_id == member.id
|
||||
|
||||
# Verify member email was synced
|
||||
{: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"
|
||||
|
||||
# Unlink user from member
|
||||
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil})
|
||||
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}, actor: actor)
|
||||
assert unlinked_user.member_id == nil
|
||||
|
||||
# Member email should remain unchanged after unlinking
|
||||
{:ok, member_after_unlink} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, member_after_unlink} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert member_after_unlink.email == "user@example.com"
|
||||
end
|
||||
end
|
||||
|
|
@ -119,6 +128,8 @@ defmodule Mv.Accounts.UserEmailSyncTest do
|
|||
email = "test@example.com"
|
||||
password = "securepassword123"
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create user with password strategy (simulating registration)
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -126,7 +137,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do
|
|||
email: email,
|
||||
password: password
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert to_string(user.email) == email
|
||||
assert user.hashed_password != nil
|
||||
|
|
@ -138,7 +149,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do
|
|||
email: email,
|
||||
password: password
|
||||
})
|
||||
|> Ash.read_one()
|
||||
|> Ash.read_one(actor: system_actor)
|
||||
|
||||
assert signed_in_user.id == user.id
|
||||
assert to_string(signed_in_user.email) == email
|
||||
|
|
@ -153,6 +164,8 @@ defmodule Mv.Accounts.UserEmailSyncTest do
|
|||
|
||||
oauth_tokens = %{"access_token" => "mock_token"}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Simulate OIDC registration
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -160,7 +173,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do
|
|||
user_info: user_info,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert to_string(user.email) == "oidc@example.com"
|
||||
assert user.oidc_id == "oidc-user-123"
|
||||
|
|
|
|||
|
|
@ -18,71 +18,86 @@ defmodule Mv.Accounts.UserMemberDeletionTest do
|
|||
email: "john@example.com"
|
||||
}
|
||||
|
||||
test "deleting a member sets the user's member_id to NULL" do
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
test "deleting a member sets the user's member_id to NULL", %{actor: actor} do
|
||||
# Create a member
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
# Create a user linked to the member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
|
||||
|
||||
# Verify the relationship is established
|
||||
{:ok, user_before_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
|
||||
{:ok, user_before_delete} =
|
||||
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
|
||||
|
||||
assert user_before_delete.member_id == member.id
|
||||
assert user_before_delete.member.id == member.id
|
||||
|
||||
# Delete the member
|
||||
:ok = Membership.destroy_member(member)
|
||||
:ok = Membership.destroy_member(member, actor: actor)
|
||||
|
||||
# Verify the user still exists but member_id is NULL
|
||||
{:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
|
||||
{:ok, user_after_delete} =
|
||||
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
|
||||
|
||||
assert user_after_delete.id == user.id
|
||||
assert user_after_delete.member_id == nil
|
||||
assert user_after_delete.member == nil
|
||||
end
|
||||
|
||||
test "user can be linked to a new member after old member is deleted" do
|
||||
test "user can be linked to a new member after old member is deleted", %{actor: actor} do
|
||||
# Create first member
|
||||
{:ok, member1} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member1} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
# Create user linked to first member
|
||||
{:ok, user} =
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member1.id}))
|
||||
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member1.id}), actor: actor)
|
||||
|
||||
assert user.member_id == member1.id
|
||||
|
||||
# Delete first member
|
||||
:ok = Membership.destroy_member(member1)
|
||||
:ok = Membership.destroy_member(member1, actor: actor)
|
||||
|
||||
# Reload user from database to get updated member_id (should be NULL)
|
||||
{:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert user_after_delete.member_id == nil
|
||||
|
||||
# Create second member
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Link user to second member (use reloaded user)
|
||||
{:ok, updated_user} = Accounts.update_user(user_after_delete, %{member: %{id: member2.id}})
|
||||
{:ok, updated_user} =
|
||||
Accounts.update_user(user_after_delete, %{member: %{id: member2.id}}, actor: actor)
|
||||
|
||||
# Verify new relationship
|
||||
{:ok, final_user} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member])
|
||||
{:ok, final_user} =
|
||||
Ash.get(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member])
|
||||
|
||||
assert final_user.member_id == member2.id
|
||||
assert final_user.member.id == member2.id
|
||||
end
|
||||
|
||||
test "member without linked user can be deleted normally" do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "member without linked user can be deleted normally", %{actor: actor} do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
# Delete member (no users linked)
|
||||
assert :ok = Membership.destroy_member(member)
|
||||
assert :ok = Membership.destroy_member(member, actor: actor)
|
||||
|
||||
# Verify member is deleted
|
||||
assert {:error, _} = Ash.get(Mv.Membership.Member, member.id)
|
||||
assert {:error, _} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,51 +10,70 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "link with same email" do
|
||||
test "succeeds when user.email == member.email" do
|
||||
test "succeeds when user.email == member.email", %{actor: actor} do
|
||||
# Create member with specific email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create user with same email and link to member
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "alice@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should succeed without errors
|
||||
assert {:ok, user} = result
|
||||
assert to_string(user.email) == "alice@example.com"
|
||||
|
||||
# Reload to verify link
|
||||
user = Ash.load!(user, [:member], domain: Mv.Accounts)
|
||||
user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor)
|
||||
assert user.member.id == member.id
|
||||
assert user.member.email == "alice@example.com"
|
||||
end
|
||||
|
||||
test "no validation error triggered when updating linked pair with same email" do
|
||||
test "no validation error triggered when updating linked pair with same email", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Bob",
|
||||
last_name: "Smith",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create user and link
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "bob@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Update user (should not trigger email validation error)
|
||||
result = Accounts.update_user(user, %{email: "bob@example.com"})
|
||||
result = Accounts.update_user(user, %{email: "bob@example.com"}, actor: actor)
|
||||
|
||||
assert {:ok, updated_user} = result
|
||||
assert to_string(updated_user.email) == "bob@example.com"
|
||||
|
|
@ -62,70 +81,88 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do
|
|||
end
|
||||
|
||||
describe "link with different emails" do
|
||||
test "fails if member.email is used by a DIFFERENT linked user" do
|
||||
test "fails if member.email is used by a DIFFERENT linked user", %{actor: actor} do
|
||||
# Create first user and link to a different member
|
||||
{:ok, other_member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Other",
|
||||
last_name: "Member",
|
||||
email: "other@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _user1} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user1@example.com",
|
||||
member: %{id: other_member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Reload to ensure email sync happened
|
||||
_other_member = Ash.reload!(other_member)
|
||||
_other_member = Ash.reload!(other_member, actor: actor)
|
||||
|
||||
# Create a NEW member with different email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Brown",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to create user2 with email that matches the linked other_member
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user1@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail because user1@example.com is already used by other_member (which is linked to user1)
|
||||
assert {:error, _error} = result
|
||||
end
|
||||
|
||||
test "succeeds for unique emails" do
|
||||
test "succeeds for unique emails", %{actor: actor} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "David",
|
||||
last_name: "Wilson",
|
||||
email: "david@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create user with different but unique email
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should succeed
|
||||
assert {:ok, user} = result
|
||||
|
||||
# Email sync should update member's email to match user's
|
||||
user = Ash.load!(user, [:member], domain: Mv.Accounts)
|
||||
user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor)
|
||||
assert user.member.email == "user@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "unlinking and relinking with same email works (Problem #4)" do
|
||||
test "unlinking and relinking with same email works (Problem #4)", %{actor: actor} do
|
||||
# This is the exact scenario from Problem #4:
|
||||
# 1. Link user and member (both have same email)
|
||||
# 2. Unlink them (member keeps the email)
|
||||
|
|
@ -133,34 +170,40 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do
|
|||
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Emma",
|
||||
last_name: "Davis",
|
||||
email: "emma@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create user and link
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "emma@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify they are linked
|
||||
user = Ash.load!(user, [:member], domain: Mv.Accounts)
|
||||
user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor)
|
||||
assert user.member.id == member.id
|
||||
assert user.member.email == "emma@example.com"
|
||||
|
||||
# Unlink
|
||||
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil})
|
||||
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}, actor: actor)
|
||||
assert is_nil(unlinked_user.member_id)
|
||||
|
||||
# Member still has the email after unlink
|
||||
member = Ash.reload!(member)
|
||||
member = Ash.reload!(member, actor: actor)
|
||||
assert member.email == "emma@example.com"
|
||||
|
||||
# Relink (should work - this is Problem #4)
|
||||
result = Accounts.update_user(unlinked_user, %{member: %{id: member.id}})
|
||||
result = Accounts.update_user(unlinked_user, %{member: %{id: member.id}}, actor: actor)
|
||||
|
||||
assert {:ok, relinked_user} = result
|
||||
assert relinked_user.member_id == member.id
|
||||
|
|
|
|||
|
|
@ -9,121 +9,150 @@ defmodule Mv.Accounts.UserMemberLinkingTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "User-Member Linking with Email Sync" do
|
||||
test "link user to member with different email syncs member email" do
|
||||
test "link user to member with different email syncs member email", %{actor: actor} do
|
||||
# Create user with one email
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor)
|
||||
|
||||
# Create member with different email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "member@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Link user to member
|
||||
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
|
||||
|
||||
# Verify link exists
|
||||
user_with_member = Ash.get!(Mv.Accounts.User, updated_user.id, load: [:member])
|
||||
user_with_member =
|
||||
Ash.get!(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member])
|
||||
|
||||
assert user_with_member.member.id == member.id
|
||||
|
||||
# Verify member email was synced to match user email
|
||||
synced_member = Ash.get!(Mv.Membership.Member, member.id)
|
||||
synced_member = Ash.get!(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert synced_member.email == "user@example.com"
|
||||
end
|
||||
|
||||
test "unlink member from user sets member to nil" do
|
||||
test "unlink member from user sets member to nil", %{actor: actor} do
|
||||
# Create and link user and member
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
|
||||
|
||||
# Verify link exists
|
||||
user_with_member = Ash.get!(Mv.Accounts.User, linked_user.id, load: [:member])
|
||||
user_with_member = Ash.get!(Mv.Accounts.User, linked_user.id, actor: actor, load: [:member])
|
||||
assert user_with_member.member.id == member.id
|
||||
|
||||
# Unlink by setting member to nil
|
||||
{:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil})
|
||||
{:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}, actor: actor)
|
||||
|
||||
# Verify link is removed
|
||||
user_without_member = Ash.get!(Mv.Accounts.User, unlinked_user.id, load: [:member])
|
||||
user_without_member =
|
||||
Ash.get!(Mv.Accounts.User, unlinked_user.id, actor: actor, load: [:member])
|
||||
|
||||
assert is_nil(user_without_member.member)
|
||||
|
||||
# Verify member still exists independently
|
||||
member_still_exists = Ash.get!(Mv.Membership.Member, member.id)
|
||||
member_still_exists = Ash.get!(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert member_still_exists.id == member.id
|
||||
end
|
||||
|
||||
test "cannot link member already linked to another user" do
|
||||
test "cannot link member already linked to another user", %{actor: actor} do
|
||||
# Create first user and link to member
|
||||
{:ok, user1} = Accounts.create_user(%{email: "user1@example.com"})
|
||||
{:ok, user1} = Accounts.create_user(%{email: "user1@example.com"}, actor: actor)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Bob",
|
||||
last_name: "Wilson",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _linked_user1} = Accounts.update_user(user1, %{member: %{id: member.id}})
|
||||
{:ok, _linked_user1} =
|
||||
Accounts.update_user(user1, %{member: %{id: member.id}}, actor: actor)
|
||||
|
||||
# Create second user and try to link to same member
|
||||
{:ok, user2} = Accounts.create_user(%{email: "user2@example.com"})
|
||||
{:ok, user2} = Accounts.create_user(%{email: "user2@example.com"}, actor: actor)
|
||||
|
||||
# Should fail because member is already linked
|
||||
assert {:error, %Ash.Error.Invalid{}} =
|
||||
Accounts.update_user(user2, %{member: %{id: member.id}})
|
||||
Accounts.update_user(user2, %{member: %{id: member.id}}, actor: actor)
|
||||
end
|
||||
|
||||
test "cannot change member link directly, must unlink first" do
|
||||
test "cannot change member link directly, must unlink first", %{actor: actor} do
|
||||
# Create user and link to first member
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor)
|
||||
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member1.id}})
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member1.id}}, actor: actor)
|
||||
|
||||
# Create second member
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Brown",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to directly change member link (should fail)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Accounts.update_user(linked_user, %{member: %{id: member2.id}})
|
||||
Accounts.update_user(linked_user, %{member: %{id: member2.id}}, actor: actor)
|
||||
|
||||
# Verify error message mentions "Remove existing member first"
|
||||
error_messages = Enum.map(errors, & &1.message)
|
||||
assert Enum.any?(error_messages, &String.contains?(&1, "Remove existing member first"))
|
||||
|
||||
# Two-step process: first unlink, then link new member
|
||||
{:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil})
|
||||
{:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}, actor: actor)
|
||||
|
||||
# After unlinking, member1 still has the user's email
|
||||
# Change member1's email to avoid conflict when relinking to member2
|
||||
{:ok, _} = Membership.update_member(member1, %{email: "alice_changed@example.com"})
|
||||
{:ok, _} =
|
||||
Membership.update_member(member1, %{email: "alice_changed@example.com"}, actor: actor)
|
||||
|
||||
{:ok, relinked_user} = Accounts.update_user(unlinked_user, %{member: %{id: member2.id}})
|
||||
{:ok, relinked_user} =
|
||||
Accounts.update_user(unlinked_user, %{member: %{id: member2.id}}, actor: actor)
|
||||
|
||||
# Verify new link is established
|
||||
user_with_new_member = Ash.get!(Mv.Accounts.User, relinked_user.id, load: [:member])
|
||||
user_with_new_member =
|
||||
Ash.get!(Mv.Accounts.User, relinked_user.id, actor: actor, load: [:member])
|
||||
|
||||
assert user_with_new_member.member.id == member2.id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "User-Member Relationship - Basic Tests" do
|
||||
@valid_user_attrs %{
|
||||
email: "test@example.com"
|
||||
|
|
@ -16,22 +21,26 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
|||
email: "john@example.com"
|
||||
}
|
||||
|
||||
test "user can exist without member" do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
test "user can exist without member", %{actor: actor} do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
assert user.member_id == nil
|
||||
|
||||
# Load the relationship to test it
|
||||
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
|
||||
{:ok, user_with_member} =
|
||||
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
|
||||
|
||||
assert user_with_member.member == nil
|
||||
end
|
||||
|
||||
test "member can exist without user" do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "member can exist without user", %{actor: actor} do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
assert member.id != nil
|
||||
assert member.first_name == "John"
|
||||
|
||||
# Load the relationship to test it
|
||||
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, member_with_user} =
|
||||
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
|
||||
|
||||
assert member_with_user.user == nil
|
||||
end
|
||||
end
|
||||
|
|
@ -47,47 +56,58 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
|||
email: "alice@example.com"
|
||||
}
|
||||
|
||||
test "user can be linked to member during user creation" do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "user can be linked to member during user creation", %{actor: actor} do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
|
||||
{:ok, user} = Accounts.create_user(user_attrs)
|
||||
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
|
||||
|
||||
# Load the relationship to test it
|
||||
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
|
||||
{:ok, user_with_member} =
|
||||
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
|
||||
|
||||
assert user_with_member.member.id == member.id
|
||||
end
|
||||
|
||||
test "member can be linked to user during member creation using manage_relationship" do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
test "member can be linked to user during member creation using manage_relationship", %{
|
||||
actor: actor
|
||||
} do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
|
||||
member_attrs = Map.put(@valid_member_attrs, :user, %{id: user.id})
|
||||
{:ok, member} = Membership.create_member(member_attrs)
|
||||
{:ok, member} = Membership.create_member(member_attrs, actor: actor)
|
||||
|
||||
# Load the relationship to test it
|
||||
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, member_with_user} =
|
||||
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
|
||||
|
||||
assert member_with_user.user.id == user.id
|
||||
end
|
||||
|
||||
test "user can be linked to member during update" do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "user can be linked to member during update", %{actor: actor} do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
|
||||
|
||||
# Load the relationship to test it
|
||||
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member])
|
||||
{:ok, user_with_member} =
|
||||
Ash.get(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member], actor: actor)
|
||||
|
||||
assert user_with_member.member.id == member.id
|
||||
end
|
||||
|
||||
test "member can be linked to user during update using manage_relationship" do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "member can be linked to user during update using manage_relationship", %{actor: actor} do
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
{:ok, _updated_member} = Membership.update_member(member, %{user: %{id: user.id}})
|
||||
{:ok, _updated_member} =
|
||||
Membership.update_member(member, %{user: %{id: user.id}}, actor: actor)
|
||||
|
||||
# Load the relationship to test it
|
||||
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, member_with_user} =
|
||||
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
|
||||
|
||||
assert member_with_user.user.id == user.id
|
||||
end
|
||||
end
|
||||
|
|
@ -103,25 +123,39 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
|||
email: "bob@example.com"
|
||||
}
|
||||
|
||||
test "ash resolves inverse relationship automatically" do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
test "ash resolves inverse relationship automatically", %{actor: actor} do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
|
||||
{:ok, user} = Accounts.create_user(user_attrs)
|
||||
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
|
||||
|
||||
# Load relationships
|
||||
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
|
||||
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, user_with_member} =
|
||||
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
|
||||
|
||||
{:ok, member_with_user} =
|
||||
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
|
||||
|
||||
assert user_with_member.member.id == member.id
|
||||
assert member_with_user.user.id == user.id
|
||||
end
|
||||
|
||||
test "member can find associated user" do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "member can find associated user", %{actor: actor} do
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, member_with_user} =
|
||||
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
|
||||
|
||||
{:ok, user} = Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}})
|
||||
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
assert member_with_user.user.id == user.id
|
||||
end
|
||||
end
|
||||
|
|
@ -137,61 +171,77 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
|||
email: "charlie@example.com"
|
||||
}
|
||||
|
||||
test "prevents overwriting a member of already linked user on update" do
|
||||
{:ok, existing_member} = Membership.create_member(@valid_member_attrs)
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
test "prevents overwriting a member of already linked user on update", %{actor: actor} do
|
||||
{:ok, existing_member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
|
||||
{:ok, user} = Accounts.create_user(user_attrs)
|
||||
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
|
||||
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Dave",
|
||||
last_name: "Wilson",
|
||||
email: "dave@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} =
|
||||
Accounts.update_user(user, %{member: %{id: member2.id}})
|
||||
Accounts.update_user(user, %{member: %{id: member2.id}}, actor: actor)
|
||||
end
|
||||
|
||||
test "prevents linking user to already linked member on update" do
|
||||
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "prevents linking user to already linked member on update", %{actor: actor} do
|
||||
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
|
||||
{:ok, _updated_user} =
|
||||
Accounts.update_user(existing_user, %{member: %{id: member.id}}, actor: actor)
|
||||
|
||||
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
|
||||
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"}, actor: actor)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} =
|
||||
Accounts.update_user(user2, %{member: %{id: member.id}})
|
||||
Accounts.update_user(user2, %{member: %{id: member.id}}, actor: actor)
|
||||
end
|
||||
|
||||
test "prevents linking member to already linked user on creation" do
|
||||
{:ok, existing_member} = Membership.create_member(@valid_member_attrs)
|
||||
test "prevents linking member to already linked user on creation", %{actor: actor} do
|
||||
{:ok, existing_member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
|
||||
{:ok, user} = Accounts.create_user(user_attrs)
|
||||
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Dave",
|
||||
last_name: "Wilson",
|
||||
email: "dave@example.com",
|
||||
user: %{id: user.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
end
|
||||
|
||||
test "prevents linking user to already linked member on creation" do
|
||||
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
test "prevents linking user to already linked member on creation", %{actor: actor} do
|
||||
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
|
||||
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
|
||||
{:ok, _updated_user} =
|
||||
Accounts.update_user(existing_user, %{member: %{id: member.id}}, actor: actor)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "test5@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,23 +13,28 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "assigned_members_count calculation" do
|
||||
test "returns 0 for custom field without any values" do
|
||||
test "returns 0 for custom field without any values", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
assert custom_field_with_count.assigned_members_count == 0
|
||||
end
|
||||
|
||||
test "returns correct count for custom field with one member" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
test "returns correct count for custom field with one member", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
{:ok, _custom_field_value} =
|
||||
CustomFieldValue
|
||||
|
|
@ -38,17 +43,17 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
assert custom_field_with_count.assigned_members_count == 1
|
||||
end
|
||||
|
||||
test "returns correct count for custom field with multiple members" do
|
||||
{:ok, member1} = create_member()
|
||||
{:ok, member2} = create_member()
|
||||
{:ok, member3} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
test "returns correct count for custom field with multiple members", %{actor: actor} do
|
||||
{:ok, member1} = create_member(actor)
|
||||
{:ok, member2} = create_member(actor)
|
||||
{:ok, member3} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
# Create custom field value for each member
|
||||
for member <- [member1, member2, member3] do
|
||||
|
|
@ -59,16 +64,16 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
end
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
assert custom_field_with_count.assigned_members_count == 3
|
||||
end
|
||||
|
||||
test "counts distinct members (not multiple values per member)" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
test "counts distinct members (not multiple values per member)", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
# Create custom field value for member
|
||||
{:ok, _} =
|
||||
|
|
@ -78,9 +83,9 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor)
|
||||
|
||||
# Should still be 1, not 2, even if we tried to create multiple (which would fail due to uniqueness)
|
||||
assert custom_field_with_count.assigned_members_count == 1
|
||||
|
|
@ -88,9 +93,9 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
end
|
||||
|
||||
describe "prepare_deletion action" do
|
||||
test "loads assigned_members_count for deletion preparation" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
test "loads assigned_members_count for deletion preparation", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
{:ok, _} =
|
||||
CustomFieldValue
|
||||
|
|
@ -99,43 +104,43 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Use prepare_deletion action
|
||||
[prepared_custom_field] =
|
||||
CustomField
|
||||
|> Ash.Query.for_read(:prepare_deletion, %{id: custom_field.id})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
assert prepared_custom_field.assigned_members_count == 1
|
||||
assert prepared_custom_field.id == custom_field.id
|
||||
end
|
||||
|
||||
test "returns empty list for non-existent custom field" do
|
||||
test "returns empty list for non-existent custom field", %{actor: actor} do
|
||||
non_existent_id = Ash.UUID.generate()
|
||||
|
||||
result =
|
||||
CustomField
|
||||
|> Ash.Query.for_read(:prepare_deletion, %{id: non_existent_id})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
assert result == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "destroy_with_values action" do
|
||||
test "deletes custom field without any values" do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
test "deletes custom field without any values", %{actor: actor} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
assert :ok = Ash.destroy(custom_field, actor: actor)
|
||||
|
||||
# Verify custom field is deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: actor)
|
||||
end
|
||||
|
||||
test "deletes custom field and cascades to all its values" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
test "deletes custom field and cascades to all its values", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
{:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|
|
@ -144,25 +149,25 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Delete custom field
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
assert :ok = Ash.destroy(custom_field, actor: actor)
|
||||
|
||||
# Verify custom field is deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: actor)
|
||||
|
||||
# Verify custom field value is also deleted (CASCADE)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id, actor: actor)
|
||||
|
||||
# Verify member still exists
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
assert {:ok, _} = Ash.get(Member, member.id, actor: actor)
|
||||
end
|
||||
|
||||
test "deletes only values of the specific custom field" do
|
||||
{:ok, member} = create_member()
|
||||
{:ok, custom_field1} = create_custom_field("field1", :string)
|
||||
{:ok, custom_field2} = create_custom_field("field2", :string)
|
||||
test "deletes only values of the specific custom field", %{actor: actor} do
|
||||
{:ok, member} = create_member(actor)
|
||||
{:ok, custom_field1} = create_custom_field("field1", :string, actor)
|
||||
{:ok, custom_field2} = create_custom_field("field2", :string, actor)
|
||||
|
||||
# Create value for custom_field1
|
||||
{:ok, value1} =
|
||||
|
|
@ -172,7 +177,7 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field1.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "value1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create value for custom_field2
|
||||
{:ok, value2} =
|
||||
|
|
@ -182,25 +187,25 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field2.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "value2"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Delete custom_field1
|
||||
assert :ok = Ash.destroy(custom_field1)
|
||||
assert :ok = Ash.destroy(custom_field1, actor: actor)
|
||||
|
||||
# Verify custom_field1 and value1 are deleted
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field1.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value1.id)
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field1.id, actor: actor)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value1.id, actor: actor)
|
||||
|
||||
# Verify custom_field2 and value2 still exist
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field2.id)
|
||||
assert {:ok, _} = Ash.get(CustomFieldValue, value2.id)
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field2.id, actor: actor)
|
||||
assert {:ok, _} = Ash.get(CustomFieldValue, value2.id, actor: actor)
|
||||
end
|
||||
|
||||
test "deletes custom field with values from multiple members" do
|
||||
{:ok, member1} = create_member()
|
||||
{:ok, member2} = create_member()
|
||||
{:ok, member3} = create_member()
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
test "deletes custom field with values from multiple members", %{actor: actor} do
|
||||
{:ok, member1} = create_member(actor)
|
||||
{:ok, member2} = create_member(actor)
|
||||
{:ok, member3} = create_member(actor)
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string, actor)
|
||||
|
||||
# Create value for each member
|
||||
values =
|
||||
|
|
@ -212,43 +217,43 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
# Delete custom field
|
||||
assert :ok = Ash.destroy(custom_field)
|
||||
assert :ok = Ash.destroy(custom_field, actor: actor)
|
||||
|
||||
# Verify all values are deleted
|
||||
for value <- values do
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, value.id, actor: actor)
|
||||
end
|
||||
|
||||
# Verify all members still exist
|
||||
for member <- [member1, member2, member3] do
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
assert {:ok, _} = Ash.get(Member, member.id, actor: actor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
defp create_member do
|
||||
defp create_member(actor) do
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "User#{System.unique_integer([:positive])}",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
end
|
||||
|
||||
defp create_custom_field(name, value_type) do
|
||||
defp create_custom_field(name, value_type, actor) do
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "#{name}_#{System.unique_integer([:positive])}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,8 +12,13 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "show_in_overview attribute" do
|
||||
test "creates custom field with show_in_overview: true" do
|
||||
test "creates custom field with show_in_overview: true", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -21,24 +26,24 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "creates custom field with show_in_overview: true (default)" do
|
||||
test "creates custom field with show_in_overview: true (default)", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field_hide",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "updates show_in_overview to true" do
|
||||
test "updates show_in_overview to true", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -46,17 +51,17 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
value_type: :string,
|
||||
show_in_overview: false
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert {:ok, updated_field} =
|
||||
custom_field
|
||||
|> Ash.Changeset.for_update(:update, %{show_in_overview: true})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "updates show_in_overview to false" do
|
||||
test "updates show_in_overview to false", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -64,12 +69,12 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert {:ok, updated_field} =
|
||||
custom_field
|
||||
|> Ash.Changeset.for_update(:update, %{show_in_overview: false})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_field.show_in_overview == false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,94 +13,99 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "automatic slug generation on create" do
|
||||
test "generates slug from name with simple ASCII text" do
|
||||
test "generates slug from name with simple ASCII text", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Mobile Phone",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "mobile-phone"
|
||||
end
|
||||
|
||||
test "generates slug from name with German umlauts" do
|
||||
test "generates slug from name with German umlauts", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Café Müller",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "cafe-muller"
|
||||
end
|
||||
|
||||
test "generates slug with lowercase conversion" do
|
||||
test "generates slug with lowercase conversion", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "TEST NAME",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "test-name"
|
||||
end
|
||||
|
||||
test "generates slug by removing special characters" do
|
||||
test "generates slug by removing special characters", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "E-Mail & Address!",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "e-mail-address"
|
||||
end
|
||||
|
||||
test "generates slug by replacing multiple spaces with single hyphen" do
|
||||
test "generates slug by replacing multiple spaces with single hyphen", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Multiple Spaces",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "multiple-spaces"
|
||||
end
|
||||
|
||||
test "trims leading and trailing hyphens" do
|
||||
test "trims leading and trailing hyphens", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "-Test-",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "test"
|
||||
end
|
||||
|
||||
test "handles unicode characters properly (ß becomes ss)" do
|
||||
test "handles unicode characters properly (ß becomes ss)", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Straße",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "strasse"
|
||||
end
|
||||
end
|
||||
|
||||
describe "slug uniqueness" do
|
||||
test "prevents creating custom field with duplicate slug" do
|
||||
test "prevents creating custom field with duplicate slug", %{actor: actor} do
|
||||
# Create first custom field
|
||||
{:ok, _custom_field} =
|
||||
CustomField
|
||||
|
|
@ -108,7 +113,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Attempt to create second custom field with same slug (different case in name)
|
||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||
|
|
@ -117,19 +122,19 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "test",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Exception.message(error) =~ "has already been taken"
|
||||
end
|
||||
|
||||
test "allows custom fields with different slugs" do
|
||||
test "allows custom fields with different slugs", %{actor: actor} do
|
||||
{:ok, custom_field1} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test One",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
{:ok, custom_field2} =
|
||||
CustomField
|
||||
|
|
@ -137,21 +142,21 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Test Two",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field1.slug == "test-one"
|
||||
assert custom_field2.slug == "test-two"
|
||||
assert custom_field1.slug != custom_field2.slug
|
||||
end
|
||||
|
||||
test "prevents duplicate slugs when names differ only in special characters" do
|
||||
test "prevents duplicate slugs when names differ only in special characters", %{actor: actor} do
|
||||
{:ok, custom_field1} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test!!!",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field1.slug == "test"
|
||||
|
||||
|
|
@ -162,7 +167,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Test???",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail with uniqueness constraint error
|
||||
assert Exception.message(error) =~ "has already been taken"
|
||||
|
|
@ -170,7 +175,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
end
|
||||
|
||||
describe "slug immutability" do
|
||||
test "slug cannot be manually set on create" do
|
||||
test "slug cannot be manually set on create", %{actor: actor} do
|
||||
# Attempting to set slug manually should fail because slug is not writable
|
||||
result =
|
||||
CustomField
|
||||
|
|
@ -179,14 +184,14 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
value_type: :string,
|
||||
slug: "custom-slug"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail because slug is not an accepted input
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
assert Exception.message(elem(result, 1)) =~ "No such input"
|
||||
end
|
||||
|
||||
test "slug does not change when name is updated" do
|
||||
test "slug does not change when name is updated", %{actor: actor} do
|
||||
# Create custom field
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|
|
@ -194,7 +199,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "Original Name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
original_slug = custom_field.slug
|
||||
assert original_slug == "original-name"
|
||||
|
|
@ -205,7 +210,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|> Ash.Changeset.for_update(:update, %{
|
||||
name: "New Different Name"
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Slug should remain unchanged
|
||||
assert updated_custom_field.slug == original_slug
|
||||
|
|
@ -213,14 +218,14 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
assert updated_custom_field.name == "New Different Name"
|
||||
end
|
||||
|
||||
test "slug cannot be manually updated" do
|
||||
test "slug cannot be manually updated", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
original_slug = custom_field.slug
|
||||
assert original_slug == "test"
|
||||
|
|
@ -231,20 +236,20 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|> Ash.Changeset.for_update(:update, %{
|
||||
slug: "new-slug"
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Should fail because slug is not an accepted input
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
assert Exception.message(elem(result, 1)) =~ "No such input"
|
||||
|
||||
# Reload to verify slug hasn't changed
|
||||
reloaded = Ash.get!(CustomField, custom_field.id)
|
||||
reloaded = Ash.get!(CustomField, custom_field.id, actor: actor)
|
||||
assert reloaded.slug == "test"
|
||||
end
|
||||
end
|
||||
|
||||
describe "slug edge cases" do
|
||||
test "handles very long names by truncating slug" do
|
||||
test "handles very long names by truncating slug", %{actor: actor} do
|
||||
# Create a name at the maximum length (100 chars)
|
||||
long_name = String.duplicate("abcdefghij", 10)
|
||||
# 100 characters exactly
|
||||
|
|
@ -255,7 +260,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: long_name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Slug should be truncated to maximum 100 characters
|
||||
assert String.length(custom_field.slug) <= 100
|
||||
|
|
@ -263,7 +268,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
assert custom_field.slug == long_name
|
||||
end
|
||||
|
||||
test "rejects name with only special characters" do
|
||||
test "rejects name with only special characters", %{actor: actor} do
|
||||
# When name contains only special characters, slug would be empty
|
||||
# This should fail validation
|
||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||
|
|
@ -272,59 +277,59 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
name: "!!!",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail because slug would be empty
|
||||
error_message = Exception.message(error)
|
||||
assert error_message =~ "Slug cannot be empty" or error_message =~ "is required"
|
||||
end
|
||||
|
||||
test "handles mixed special characters and text" do
|
||||
test "handles mixed special characters and text", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test@#$%Name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# slugify keeps the hyphen between words
|
||||
assert custom_field.slug == "test-name"
|
||||
end
|
||||
|
||||
test "handles numbers in name" do
|
||||
test "handles numbers in name", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Field 123 Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.slug == "field-123-test"
|
||||
end
|
||||
|
||||
test "handles consecutive hyphens in name" do
|
||||
test "handles consecutive hyphens in name", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test---Name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should reduce multiple hyphens to single hyphen
|
||||
assert custom_field.slug == "test-name"
|
||||
end
|
||||
|
||||
test "handles name with dots and underscores" do
|
||||
test "handles name with dots and underscores", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test.field_name",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Dots and underscores should be handled (either kept or converted)
|
||||
assert custom_field.slug =~ ~r/^[a-z0-9-]+$/
|
||||
|
|
@ -332,45 +337,45 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
end
|
||||
|
||||
describe "slug in queries and responses" do
|
||||
test "slug is included in struct after create" do
|
||||
test "slug is included in struct after create", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Slug should be present in the struct
|
||||
assert Map.has_key?(custom_field, :slug)
|
||||
assert custom_field.slug != nil
|
||||
end
|
||||
|
||||
test "can load custom field and slug is present" do
|
||||
test "can load custom field and slug is present", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Load it back
|
||||
loaded_custom_field = Ash.get!(CustomField, custom_field.id)
|
||||
loaded_custom_field = Ash.get!(CustomField, custom_field.id, actor: actor)
|
||||
|
||||
assert loaded_custom_field.slug == "test"
|
||||
end
|
||||
|
||||
test "slug is returned in list queries" do
|
||||
test "slug is returned in list queries", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
custom_fields = Ash.read!(CustomField)
|
||||
custom_fields = Ash.read!(CustomField, actor: actor)
|
||||
|
||||
found = Enum.find(custom_fields, &(&1.id == custom_field.id))
|
||||
assert found.slug == "test"
|
||||
|
|
@ -379,18 +384,18 @@ defmodule Mv.Membership.CustomFieldSlugTest do
|
|||
|
||||
describe "slug-based lookup (future feature)" do
|
||||
@tag :skip
|
||||
test "can find custom field by slug" do
|
||||
test "can find custom field by slug", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test Field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# This test is for future implementation
|
||||
# We might add a custom action like :by_slug
|
||||
found = Ash.get!(CustomField, custom_field.slug, load: [:slug])
|
||||
found = Ash.get!(CustomField, custom_field.slug, load: [:slug], actor: actor)
|
||||
assert found.id == custom_field.id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,8 +13,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "name validation" do
|
||||
test "accepts name with exactly 100 characters" do
|
||||
test "accepts name with exactly 100 characters", %{actor: actor} do
|
||||
name = String.duplicate("a", 100)
|
||||
|
||||
assert {:ok, custom_field} =
|
||||
|
|
@ -23,13 +28,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.name == name
|
||||
assert String.length(custom_field.name) == 100
|
||||
end
|
||||
|
||||
test "rejects name with 101 characters" do
|
||||
test "rejects name with 101 characters", %{actor: actor} do
|
||||
name = String.duplicate("a", 101)
|
||||
|
||||
assert {:error, changeset} =
|
||||
|
|
@ -38,50 +43,50 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert [%{field: :name, message: message}] = changeset.errors
|
||||
assert message =~ "max" or message =~ "length" or message =~ "100"
|
||||
end
|
||||
|
||||
test "trims whitespace from name" do
|
||||
test "trims whitespace from name", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: " test_field ",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.name == "test_field"
|
||||
end
|
||||
|
||||
test "rejects empty name" do
|
||||
test "rejects empty name", %{actor: actor} do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
|
||||
test "rejects nil name" do
|
||||
test "rejects nil name", %{actor: actor} do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "description validation" do
|
||||
test "accepts description with exactly 500 characters" do
|
||||
test "accepts description with exactly 500 characters", %{actor: actor} do
|
||||
description = String.duplicate("a", 500)
|
||||
|
||||
assert {:ok, custom_field} =
|
||||
|
|
@ -91,13 +96,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: description
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.description == description
|
||||
assert String.length(custom_field.description) == 500
|
||||
end
|
||||
|
||||
test "rejects description with 501 characters" do
|
||||
test "rejects description with 501 characters", %{actor: actor} do
|
||||
description = String.duplicate("a", 501)
|
||||
|
||||
assert {:error, changeset} =
|
||||
|
|
@ -107,13 +112,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: description
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert [%{field: :description, message: message}] = changeset.errors
|
||||
assert message =~ "max" or message =~ "length" or message =~ "500"
|
||||
end
|
||||
|
||||
test "trims whitespace from description" do
|
||||
test "trims whitespace from description", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -121,24 +126,24 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: " A nice description "
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.description == "A nice description"
|
||||
end
|
||||
|
||||
test "accepts nil description (optional field)" do
|
||||
test "accepts nil description (optional field)", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.description == nil
|
||||
end
|
||||
|
||||
test "accepts empty description after trimming" do
|
||||
test "accepts empty description after trimming", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -146,7 +151,7 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
value_type: :string,
|
||||
description: " "
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# After trimming whitespace, becomes nil (empty strings are converted to nil)
|
||||
assert custom_field.description == nil
|
||||
|
|
@ -154,14 +159,14 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
end
|
||||
|
||||
describe "name uniqueness" do
|
||||
test "rejects duplicate names" do
|
||||
test "rejects duplicate names", %{actor: actor} do
|
||||
assert {:ok, _} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "unique_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|
|
@ -169,14 +174,14 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: "unique_field",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "value_type validation" do
|
||||
test "accepts all valid value types" do
|
||||
test "accepts all valid value types", %{actor: actor} do
|
||||
for value_type <- [:string, :integer, :boolean, :date, :email] do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|
|
@ -184,20 +189,20 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
name: "field_#{value_type}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.value_type == value_type
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects invalid value type" do
|
||||
test "rejects invalid value type", %{actor: actor} do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "invalid_field",
|
||||
value_type: :invalid_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert [%{field: :value_type}] = changeset.errors
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create a test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -21,7 +23,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
last_name: "User",
|
||||
email: "test.validation@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom fields for different types
|
||||
{:ok, string_field} =
|
||||
|
|
@ -30,7 +32,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
name: "string_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, integer_field} =
|
||||
CustomField
|
||||
|
|
@ -38,7 +40,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
name: "integer_field",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, email_field} =
|
||||
CustomField
|
||||
|
|
@ -46,9 +48,10 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
name: "email_field",
|
||||
value_type: :email
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field,
|
||||
integer_field: integer_field,
|
||||
|
|
@ -58,6 +61,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
|
||||
describe "string value length validation" do
|
||||
test "accepts string value with exactly 10,000 characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -73,13 +77,14 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
"_union_value" => value_string
|
||||
}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == value_string
|
||||
assert String.length(custom_field_value.value.value) == 10_000
|
||||
end
|
||||
|
||||
test "rejects string value with 10,001 characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -92,14 +97,18 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => value_string}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error ->
|
||||
error.field == :value and (error.message =~ "max" or error.message =~ "length")
|
||||
end)
|
||||
end
|
||||
|
||||
test "trims whitespace from string value", %{member: member, string_field: string_field} do
|
||||
test "trims whitespace from string value", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -107,12 +116,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => " test value "}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == "test value"
|
||||
end
|
||||
|
||||
test "accepts empty string value", %{member: member, string_field: string_field} do
|
||||
test "accepts empty string value", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -120,13 +133,17 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Empty strings after trimming become nil
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts string with special characters", %{member: member, string_field: string_field} do
|
||||
test "accepts string with special characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
special_string = "Hello 世界! 🎉 @#$%^&*()"
|
||||
|
||||
assert {:ok, custom_field_value} =
|
||||
|
|
@ -136,14 +153,18 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => special_string}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == special_string
|
||||
end
|
||||
end
|
||||
|
||||
describe "integer value validation" do
|
||||
test "accepts valid integer value", %{member: member, integer_field: integer_field} do
|
||||
test "accepts valid integer value", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
integer_field: integer_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -151,12 +172,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 42}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == 42
|
||||
end
|
||||
|
||||
test "accepts negative integer", %{member: member, integer_field: integer_field} do
|
||||
test "accepts negative integer", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
integer_field: integer_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -164,12 +189,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => -100}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == -100
|
||||
end
|
||||
|
||||
test "accepts zero", %{member: member, integer_field: integer_field} do
|
||||
test "accepts zero", %{actor: system_actor, member: member, integer_field: integer_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -177,14 +202,18 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 0}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "email value validation" do
|
||||
test "accepts nil value (optional field)", %{member: member, email_field: email_field} do
|
||||
test "accepts nil value (optional field)", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -192,12 +221,13 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => nil}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts empty string (becomes nil after trim)", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
|
|
@ -208,13 +238,13 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Empty string after trim should become nil
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts valid email", %{member: member, email_field: email_field} do
|
||||
test "accepts valid email", %{actor: system_actor, member: member, email_field: email_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -222,12 +252,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "test@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == "test@example.com"
|
||||
end
|
||||
|
||||
test "rejects invalid email format", %{member: member, email_field: email_field} do
|
||||
test "rejects invalid email format", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
assert {:error, changeset} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -235,12 +269,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "not-an-email"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
|
||||
end
|
||||
|
||||
test "rejects email longer than 254 characters", %{member: member, email_field: email_field} do
|
||||
test "rejects email longer than 254 characters", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
# Create an email with >254 chars (243 + 12 = 255)
|
||||
long_email = String.duplicate("a", 243) <> "@example.com"
|
||||
|
||||
|
|
@ -251,12 +289,16 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => long_email}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
|
||||
end
|
||||
|
||||
test "trims whitespace from email", %{member: member, email_field: email_field} do
|
||||
test "trims whitespace from email", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -264,7 +306,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => " test@example.com "}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert custom_field_value.value.value == "test@example.com"
|
||||
end
|
||||
|
|
@ -272,6 +314,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
|
||||
describe "uniqueness constraint" do
|
||||
test "rejects duplicate custom_field_id per member", %{
|
||||
actor: system_actor,
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -283,7 +326,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "first value"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Try to create second custom field value with same custom_field_id for same member
|
||||
assert {:error, changeset} =
|
||||
|
|
@ -293,7 +336,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "second value"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Should have uniqueness error
|
||||
assert Enum.any?(changeset.errors, fn error ->
|
||||
|
|
|
|||
|
|
@ -1,70 +1,93 @@
|
|||
defmodule Mv.Membership.FuzzySearchTest do
|
||||
use Mv.DataCase, async: false
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
test "fuzzy_search/2 function exists" do
|
||||
assert function_exported?(Mv.Membership.Member, :fuzzy_search, 2)
|
||||
end
|
||||
|
||||
test "fuzzy_search returns only John Doe by fuzzy query 'john'" do
|
||||
test "fuzzy_search returns only John Doe by fuzzy query 'john'", %{actor: actor} do
|
||||
{:ok, john} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _jane} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Adriana",
|
||||
last_name: "Smith",
|
||||
email: "adriana.smith@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, alice} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Thomas",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, jane} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane.smith@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _alice} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
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(%{
|
||||
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(%{
|
||||
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(%{
|
||||
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(%{
|
||||
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(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "City",
|
||||
last_name: "One",
|
||||
email: "city1@example.com",
|
||||
city: "Berlin"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _m} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test.user@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Dot",
|
||||
last_name: "Test",
|
||||
email: "dot.test@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Mary-Jane",
|
||||
last_name: "Watson",
|
||||
email: "mary.jane@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Jörg",
|
||||
last_name: "Schmidt",
|
||||
email: "joerg.schmidt@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Märta",
|
||||
last_name: "Andersson",
|
||||
email: "maerta.andersson@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
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(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Müller",
|
||||
last_name: "Schmidt",
|
||||
email: "mueller.schmidt@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _other} =
|
||||
Mv.Membership.create_member(%{
|
||||
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(%{
|
||||
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(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
very_long_query = String.duplicate("test query ", 1000)
|
||||
|
||||
result =
|
||||
Mv.Membership.Member
|
||||
|> Mv.Membership.Member.fuzzy_search(%{query: very_long_query})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
# Should not crash, may return empty or some results
|
||||
assert is_list(result)
|
||||
|
|
|
|||
|
|
@ -13,64 +13,87 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
|
||||
describe "available_for_linking/2" do
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create 5 unlinked members with distinct names
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Bob",
|
||||
last_name: "Williams",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member3} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Davis",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member4} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Diana",
|
||||
last_name: "Martinez",
|
||||
email: "diana@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member5} =
|
||||
Membership.create_member(%{
|
||||
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(%{
|
||||
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(%{
|
||||
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(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Extra#{i}",
|
||||
last_name: "Member#{i}",
|
||||
email: "extra#{i}@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
end
|
||||
|
||||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Should be limited to 10
|
||||
assert length(members) == 10
|
||||
|
|
@ -125,6 +157,8 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
test "email match: returns only member with matching email when exists", %{
|
||||
unlinked_members: unlinked_members
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Get one of the unlinked members' email
|
||||
target_member = List.first(unlinked_members)
|
||||
user_email = target_member.email
|
||||
|
|
@ -132,7 +166,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
raw_members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{user_email: user_email})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Apply email match filtering (sorted results come from query)
|
||||
# When user_email matches, only that member should be returned
|
||||
|
|
@ -145,13 +179,15 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
end
|
||||
|
||||
test "email match: returns all unlinked members when no email match" do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Use an email that doesn't match any member
|
||||
non_matching_email = "nonexistent@example.com"
|
||||
|
||||
raw_members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{user_email: non_matching_email})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Apply email match filtering
|
||||
members = Mv.Membership.Member.filter_by_email_match(raw_members, non_matching_email)
|
||||
|
|
@ -163,11 +199,13 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
test "search query: filters by first_name, last_name, and email", %{
|
||||
unlinked_members: _unlinked_members
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Search by first name
|
||||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "Alice"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(members) == 1
|
||||
assert List.first(members).first_name == "Alice"
|
||||
|
|
@ -176,7 +214,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "Williams"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(members) == 1
|
||||
assert List.first(members).last_name == "Williams"
|
||||
|
|
@ -185,7 +223,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "charlie@"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(members) == 1
|
||||
assert List.first(members).email == "charlie@example.com"
|
||||
|
|
@ -194,12 +232,13 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
members =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{search_query: "NonExistent"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.empty?(members)
|
||||
end
|
||||
|
||||
test "user_email takes precedence over search_query", %{unlinked_members: unlinked_members} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
target_member = List.first(unlinked_members)
|
||||
|
||||
# Pass both email match and search query that would match different members
|
||||
|
|
@ -209,7 +248,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do
|
|||
user_email: target_member.email,
|
||||
search_query: "Bob"
|
||||
})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Apply email-match filter (as LiveView does)
|
||||
members = Mv.Membership.Member.filter_by_email_match(raw_members, target_member.email)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,13 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
defp create_fee_type(attrs, actor) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -21,11 +26,11 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
defp create_member(attrs, actor) do
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -36,11 +41,11 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
defp create_cycle(member, fee_type, attrs, actor) do
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2024-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -53,62 +58,77 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
describe "current_cycle_status" do
|
||||
test "returns status of current cycle for member with active cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "returns status of current cycle for member with active cycle", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# Create a cycle that is active today (2024-01-01 to 2024-12-31)
|
||||
# Assuming today is in 2024
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
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, %{
|
||||
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, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :paid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
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, %{
|
||||
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, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: last_month_start,
|
||||
status: :paid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
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, %{
|
||||
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, %{
|
||||
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, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: two_months_ago_start,
|
||||
status: :unpaid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: last_month_start,
|
||||
status: :paid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2020-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2021-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
status: :unpaid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
create_cycle(member, fee_type, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
fee_type,
|
||||
%{
|
||||
cycle_start: cycle_start,
|
||||
status: :unpaid
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member =
|
||||
Ash.load!(member, [:current_cycle_status, :last_cycle_status, :overdue_count])
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ defmodule Mv.Membership.MemberEmailSyncTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Member email synchronization to linked User" do
|
||||
@valid_user_attrs %{
|
||||
email: "user@example.com"
|
||||
|
|
@ -19,108 +24,119 @@ defmodule Mv.Membership.MemberEmailSyncTest do
|
|||
email: "member@example.com"
|
||||
}
|
||||
|
||||
test "updating member email syncs to linked user" do
|
||||
test "updating member email syncs to linked user", %{actor: actor} do
|
||||
# Create a user
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
assert to_string(user.email) == "user@example.com"
|
||||
|
||||
# Create a member linked to the user
|
||||
{:ok, member} =
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}))
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}),
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify initial state - member email should be overridden by user email
|
||||
{:ok, member_after_create} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, member_after_create} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert member_after_create.email == "user@example.com"
|
||||
|
||||
# Update member email
|
||||
{:ok, updated_member} =
|
||||
Membership.update_member(member, %{email: "newmember@example.com"})
|
||||
Membership.update_member(member, %{email: "newmember@example.com"}, actor: actor)
|
||||
|
||||
assert updated_member.email == "newmember@example.com"
|
||||
|
||||
# Verify user email was also updated
|
||||
{:ok, synced_user} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, synced_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert to_string(synced_user.email) == "newmember@example.com"
|
||||
end
|
||||
|
||||
test "creating member linked to user syncs user email to member" do
|
||||
test "creating member linked to user syncs user email to member", %{actor: actor} do
|
||||
# Create a user with their own email
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
assert to_string(user.email) == "user@example.com"
|
||||
|
||||
# Create a member linked to this user
|
||||
{:ok, member} =
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}))
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}),
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Member should have been created with user's email (user is source of truth)
|
||||
assert member.email == "user@example.com"
|
||||
|
||||
# Verify the link
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor)
|
||||
assert loaded_member.user.id == user.id
|
||||
end
|
||||
|
||||
test "linking member to existing user syncs user email to member" do
|
||||
test "linking member to existing user syncs user email to member", %{actor: actor} do
|
||||
# Create a standalone user
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
assert to_string(user.email) == "user@example.com"
|
||||
|
||||
# Create a standalone member
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
assert member.email == "member@example.com"
|
||||
|
||||
# Link the member to the user
|
||||
{:ok, linked_member} = Membership.update_member(member, %{user: %{id: user.id}})
|
||||
{:ok, linked_member} =
|
||||
Membership.update_member(member, %{user: %{id: user.id}}, actor: actor)
|
||||
|
||||
# Verify the link
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, linked_member.id, load: [:user])
|
||||
{:ok, loaded_member} =
|
||||
Ash.get(Mv.Membership.Member, linked_member.id, load: [:user], actor: actor)
|
||||
|
||||
assert loaded_member.user.id == user.id
|
||||
|
||||
# Verify member email was overridden with user email
|
||||
assert loaded_member.email == "user@example.com"
|
||||
end
|
||||
|
||||
test "updating member email when no user linked does not error" do
|
||||
test "updating member email when no user linked does not error", %{actor: actor} do
|
||||
# Create a standalone member without user link
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs)
|
||||
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
|
||||
assert member.email == "member@example.com"
|
||||
|
||||
# Load to verify no user link
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor)
|
||||
assert loaded_member.user == nil
|
||||
|
||||
# Update member email - should work fine without error
|
||||
{:ok, updated_member} =
|
||||
Membership.update_member(member, %{email: "newemail@example.com"})
|
||||
Membership.update_member(member, %{email: "newemail@example.com"}, actor: actor)
|
||||
|
||||
assert updated_member.email == "newemail@example.com"
|
||||
end
|
||||
|
||||
test "unlinking member from user does not sync email" do
|
||||
test "unlinking member from user does not sync email", %{actor: actor} do
|
||||
# Create user
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs)
|
||||
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
|
||||
|
||||
# Create member linked to user
|
||||
{:ok, member} =
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}))
|
||||
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}),
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify member email was synced to user email
|
||||
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert synced_member.email == "user@example.com"
|
||||
|
||||
# Verify link exists
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
|
||||
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor)
|
||||
assert loaded_member.user != nil
|
||||
|
||||
# Unlink member from user
|
||||
{:ok, unlinked_member} = Membership.update_member(member, %{user: nil})
|
||||
{:ok, unlinked_member} = Membership.update_member(member, %{user: nil}, actor: actor)
|
||||
|
||||
# Verify unlink
|
||||
{:ok, loaded_unlinked} = Ash.get(Mv.Membership.Member, unlinked_member.id, load: [:user])
|
||||
{:ok, loaded_unlinked} =
|
||||
Ash.get(Mv.Membership.Member, unlinked_member.id, load: [:user], actor: actor)
|
||||
|
||||
assert loaded_unlinked.user == nil
|
||||
|
||||
# User email should remain unchanged after unlinking
|
||||
{:ok, user_after_unlink} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, user_after_unlink} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert to_string(user_after_unlink.email) == "user@example.com"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,15 +9,23 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "available_for_linking with fuzzy search" do
|
||||
test "finds member despite typo" do
|
||||
test "finds member despite typo", %{actor: actor} do
|
||||
# Create member with specific name
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
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(%{
|
||||
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(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _member2} =
|
||||
Membership.create_member(%{
|
||||
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(%{
|
||||
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(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member",
|
||||
email: "linked@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "user@example.com",
|
||||
member: %{id: member1.id}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create unlinked member
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Unlinked",
|
||||
last_name: "Member",
|
||||
email: "unlinked@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Search for "Member"
|
||||
query =
|
||||
|
|
@ -147,7 +176,7 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
|||
search_query: "Member"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor)
|
||||
|
||||
# Should only return unlinked member
|
||||
member_ids = Enum.map(members, & &1.id)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create required custom fields for different types
|
||||
{:ok, required_string_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -22,7 +24,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :string,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_integer_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -31,7 +33,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :integer,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_boolean_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -40,7 +42,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :boolean,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_date_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -49,7 +51,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :date,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, required_email_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -58,7 +60,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :email,
|
||||
required: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, optional_field} =
|
||||
Membership.CustomField
|
||||
|
|
@ -67,7 +69,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
value_type: :string,
|
||||
required: false
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
required_string_field: required_string_field,
|
||||
|
|
@ -75,7 +77,8 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
required_boolean_field: required_boolean_field,
|
||||
required_date_field: required_date_field,
|
||||
required_email_field: required_email_field,
|
||||
optional_field: optional_field
|
||||
optional_field: optional_field,
|
||||
actor: system_actor
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -118,17 +121,23 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
email: "john@example.com"
|
||||
}
|
||||
|
||||
test "fails when required custom field is missing", %{required_string_field: field} do
|
||||
test "fails when required custom field is missing", %{
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} do
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, [])
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required string custom field has nil value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values
|
||||
custom_field_values =
|
||||
|
|
@ -143,14 +152,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required string custom field has empty string value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values
|
||||
custom_field_values =
|
||||
|
|
@ -165,14 +177,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required string custom field has whitespace-only value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values
|
||||
custom_field_values =
|
||||
|
|
@ -187,14 +202,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required string custom field has valid value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Start with all required fields having valid values, then update the string field
|
||||
custom_field_values =
|
||||
|
|
@ -209,12 +227,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required integer custom field has nil value",
|
||||
%{
|
||||
required_integer_field: field
|
||||
required_integer_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -228,14 +247,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required integer custom field has empty string value",
|
||||
%{
|
||||
required_integer_field: field
|
||||
required_integer_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -249,25 +271,29 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required integer custom field has zero value",
|
||||
%{
|
||||
required_integer_field: _field
|
||||
required_integer_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when required integer custom field has positive value",
|
||||
%{
|
||||
required_integer_field: field
|
||||
required_integer_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -281,12 +307,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required boolean custom field has nil value",
|
||||
%{
|
||||
required_boolean_field: field
|
||||
required_boolean_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -300,25 +327,29 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required boolean custom field has false value",
|
||||
%{
|
||||
required_boolean_field: _field
|
||||
required_boolean_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when required boolean custom field has true value",
|
||||
%{
|
||||
required_boolean_field: field
|
||||
required_boolean_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -332,12 +363,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required date custom field has nil value",
|
||||
%{
|
||||
required_date_field: field
|
||||
required_date_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -351,14 +383,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required date custom field has empty string value",
|
||||
%{
|
||||
required_date_field: field
|
||||
required_date_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -372,25 +407,29 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required date custom field has valid date value",
|
||||
%{
|
||||
required_date_field: _field
|
||||
required_date_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when required email custom field has nil value",
|
||||
%{
|
||||
required_email_field: field
|
||||
required_email_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -404,14 +443,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "fails when required email custom field has empty string value",
|
||||
%{
|
||||
required_email_field: field
|
||||
required_email_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -425,27 +467,31 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ field.name
|
||||
end
|
||||
|
||||
test "succeeds when required email custom field has valid email value",
|
||||
%{
|
||||
required_email_field: _field
|
||||
required_email_field: _field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when multiple required custom fields are provided",
|
||||
%{
|
||||
required_string_field: string_field,
|
||||
required_integer_field: integer_field,
|
||||
required_boolean_field: boolean_field
|
||||
required_boolean_field: boolean_field,
|
||||
actor: actor
|
||||
} = context do
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context)
|
||||
|
|
@ -467,13 +513,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "fails when one of multiple required custom fields is missing",
|
||||
%{
|
||||
required_string_field: string_field,
|
||||
required_integer_field: integer_field
|
||||
required_integer_field: integer_field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Provide only string field, missing integer, boolean, and date
|
||||
custom_field_values =
|
||||
|
|
@ -487,22 +534,24 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :custom_field_values) =~ "Required custom fields missing"
|
||||
assert error_message(errors, :custom_field_values) =~ integer_field.name
|
||||
end
|
||||
|
||||
test "succeeds when optional custom field is missing", %{} = context do
|
||||
test "succeeds when optional custom field is missing", %{actor: actor} = context do
|
||||
# Provide all required fields, but no optional field
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "succeeds when optional custom field has nil value",
|
||||
%{optional_field: field} = context do
|
||||
%{optional_field: field, actor: actor} = context do
|
||||
# Provide all required fields plus optional field with nil
|
||||
custom_field_values =
|
||||
all_required_custom_fields_with_defaults(context) ++
|
||||
|
|
@ -515,29 +564,33 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
|
|||
|
||||
attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values)
|
||||
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_member with required custom fields" do
|
||||
test "fails when removing a required custom field value",
|
||||
%{
|
||||
required_string_field: field
|
||||
required_string_field: field,
|
||||
actor: actor
|
||||
} = context do
|
||||
# Create member with all required custom fields
|
||||
custom_field_values = all_required_custom_fields_with_defaults(context)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
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(%{
|
||||
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, %{
|
||||
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(%{
|
||||
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, %{
|
||||
Membership.update_member(
|
||||
member,
|
||||
%{
|
||||
custom_field_values: updated_custom_field_values
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|
|
@ -18,7 +20,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|
|
@ -27,7 +29,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member3} =
|
||||
Member
|
||||
|
|
@ -36,7 +38,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
last_name: "Clark",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom fields for different types
|
||||
{:ok, string_field} =
|
||||
|
|
@ -45,7 +47,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "membership_number",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, integer_field} =
|
||||
CustomField
|
||||
|
|
@ -53,7 +55,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "member_id_number",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, email_field} =
|
||||
CustomField
|
||||
|
|
@ -61,7 +63,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "secondary_email",
|
||||
value_type: :email
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, date_field} =
|
||||
CustomField
|
||||
|
|
@ -69,7 +71,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "birthday",
|
||||
value_type: :date
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, boolean_field} =
|
||||
CustomField
|
||||
|
|
@ -77,7 +79,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "newsletter",
|
||||
value_type: :boolean
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
|
|
@ -87,12 +89,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
integer_field: integer_field,
|
||||
email_field: email_field,
|
||||
date_field: date_field,
|
||||
boolean_field: boolean_field
|
||||
boolean_field: boolean_field,
|
||||
system_actor: system_actor
|
||||
}
|
||||
end
|
||||
|
||||
describe "search with custom field values" do
|
||||
test "finds member by string custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -104,25 +108,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MEMBER12345"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update by reloading member
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MEMBER12345"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by integer custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
integer_field: integer_field
|
||||
} do
|
||||
|
|
@ -134,25 +139,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 42_424}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "42424"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by email custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
email_field: email_field
|
||||
} do
|
||||
|
|
@ -164,19 +170,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "alice.secondary@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for partial custom field value (should work via FTS or custom field filter)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "alice.secondary"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
|
@ -185,7 +191,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "alice.secondary@example.com"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_full) == 1
|
||||
assert List.first(results_full).id == member1.id
|
||||
|
|
@ -195,7 +201,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_domain =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "example.com"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Verify that member1 is in the results (may have other members too)
|
||||
ids = Enum.map(results_domain, & &1.id)
|
||||
|
|
@ -203,6 +209,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds member by date custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
date_field: date_field
|
||||
} do
|
||||
|
|
@ -214,25 +221,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: date_field.id,
|
||||
value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value (date is stored as text in search_vector)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "1990-05-15"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by boolean custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
boolean_field: boolean_field
|
||||
} do
|
||||
|
|
@ -244,25 +252,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: boolean_field.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the custom field value (boolean is stored as "true" or "false" text)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "true"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
# Note: "true" might match other things, so we check that member1 is in results
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value update triggers search_vector update", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -274,13 +283,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "OLDVALUE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Update custom field value
|
||||
{:ok, _updated_cfv} =
|
||||
|
|
@ -288,13 +297,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
|> Ash.Changeset.for_update(:update, %{
|
||||
value: %{"_union_type" => "string", "_union_value" => "NEWVALUE123"}
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for the new value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "NEWVALUE123"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
|
@ -303,12 +312,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
old_results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "OLDVALUE"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
refute Enum.any?(old_results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value delete triggers search_vector update", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -320,19 +330,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "TOBEDELETED"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Verify it's searchable
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "TOBEDELETED"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
|
@ -344,12 +354,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
deleted_results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "TOBEDELETED"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
refute Enum.any?(deleted_results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value create triggers search_vector update", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -361,19 +372,20 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "AUTOUPDATE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Search should find it immediately (trigger should have updated search_vector)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "AUTOUPDATE"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "member update includes custom field values in search_vector", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -385,25 +397,26 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MEMBERUPDATE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Update member (should trigger search_vector update including custom fields)
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{notes: "Updated notes"})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search should find the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MEMBERUPDATE"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "multiple custom field values are all searchable", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field,
|
||||
integer_field: integer_field,
|
||||
|
|
@ -417,7 +430,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MULTI1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -426,7 +439,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 99_999}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|
|
@ -435,38 +448,39 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "multi@test.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# All values should be searchable
|
||||
results1 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MULTI1"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results1, fn m -> m.id == member1.id end)
|
||||
|
||||
results2 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "99999"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results2, fn m -> m.id == member1.id end)
|
||||
|
||||
results3 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "multi@test.com"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results3, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "finds member by custom field value with numbers in text field (e.g. phone number)", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -478,19 +492,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "M-123-456"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for full value (should work via search_vector)
|
||||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "M-123-456"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results_full, fn m -> m.id == member1.id end),
|
||||
"Full value search should find member via search_vector"
|
||||
|
|
@ -501,6 +515,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds member by phone number in Emergency Contact custom field", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1
|
||||
} do
|
||||
# Create Emergency Contact custom field
|
||||
|
|
@ -510,7 +525,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
name: "Emergency Contact",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field value with phone number
|
||||
phone_number = "+49 123 456789"
|
||||
|
|
@ -522,19 +537,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: emergency_contact_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => phone_number}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Search for full phone number (should work via search_vector)
|
||||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: phone_number})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results_full, fn m -> m.id == member1.id end),
|
||||
"Full phone number search should find member via search_vector"
|
||||
|
|
@ -547,6 +562,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
|
||||
describe "custom field substring search (ILIKE)" do
|
||||
test "finds member by prefix of custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -558,14 +574,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Premium"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Test prefix searches - should all find the member
|
||||
for prefix <- ["Premium", "Premiu", "Premi", "Prem", "Pre"] do
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: prefix})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end),
|
||||
"Prefix '#{prefix}' should find member with custom field 'Premium'"
|
||||
|
|
@ -573,6 +589,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "custom field search is case-insensitive", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -584,7 +601,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "GoldMember"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Test case variations - should all find the member
|
||||
for variant <- [
|
||||
|
|
@ -599,7 +616,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: variant})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end),
|
||||
"Case variant '#{variant}' should find member with custom field 'GoldMember'"
|
||||
|
|
@ -607,6 +624,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds member by suffix/middle of custom field value", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
|
|
@ -618,14 +636,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "ActiveMember"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Test suffix and middle substring searches
|
||||
for substring <- ["Member", "ember", "tiveMem", "ctive"] do
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: substring})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end),
|
||||
"Substring '#{substring}' should find member with custom field 'ActiveMember'"
|
||||
|
|
@ -633,6 +651,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
end
|
||||
|
||||
test "finds correct member among multiple with different custom field values", %{
|
||||
system_actor: system_actor,
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
member3: member3,
|
||||
|
|
@ -646,7 +665,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Beginner"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -655,7 +674,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Advanced"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|
|
@ -664,13 +683,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Expert"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Search for "Begin" - should only find member1
|
||||
results_begin =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "Begin"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_begin) == 1
|
||||
assert List.first(results_begin).id == member1.id
|
||||
|
|
@ -679,7 +698,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_advan =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "Advan"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_advan) == 1
|
||||
assert List.first(results_advan).id == member2.id
|
||||
|
|
@ -688,7 +707,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
|||
results_exper =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "Exper"})
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
assert length(results_exper) == 1
|
||||
assert List.first(results_exper).id == member3.id
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ defmodule Mv.Membership.MemberTest do
|
|||
use Mv.DataCase, async: false
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Fields and Validations" do
|
||||
@valid_attrs %{
|
||||
first_name: "John",
|
||||
|
|
@ -16,60 +21,74 @@ defmodule Mv.Membership.MemberTest do
|
|||
postal_code: "12345"
|
||||
}
|
||||
|
||||
test "First name is optional" do
|
||||
test "First name is optional", %{actor: actor} do
|
||||
attrs = Map.delete(@valid_attrs, :first_name)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Last name is optional" do
|
||||
test "Last name is optional", %{actor: actor} do
|
||||
attrs = Map.delete(@valid_attrs, :last_name)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Email is required" do
|
||||
test "Email is required", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :email, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :email) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Email must be valid" do
|
||||
test "Email must be valid", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :email, "test@")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :email) =~ "is not a valid email"
|
||||
end
|
||||
|
||||
test "Join date cannot be in the future" do
|
||||
test "Join date cannot be in the future", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1))
|
||||
|
||||
assert {:error,
|
||||
%Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidAttribute{field: :join_date}]}} =
|
||||
Membership.create_member(attrs)
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Exit date is optional but must not be before join date if both are specified" do
|
||||
test "Exit date is optional but must not be before join date if both are specified", %{
|
||||
actor: actor
|
||||
} do
|
||||
attrs = Map.put(@valid_attrs, :exit_date, ~D[2010-01-01])
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :exit_date) =~ "cannot be before join date"
|
||||
attrs2 = Map.delete(@valid_attrs, :exit_date)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2, actor: actor)
|
||||
end
|
||||
|
||||
test "Notes is optional" do
|
||||
test "Notes is optional", %{actor: actor} do
|
||||
attrs = Map.delete(@valid_attrs, :notes)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "City, street, house number are optional" do
|
||||
test "City, street, house number are optional", %{actor: actor} do
|
||||
attrs = @valid_attrs |> Map.drop([:city, :street, :house_number])
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
assert {:ok, _member} = Membership.create_member(attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "Postal code is optional but must have 5 digits if specified" do
|
||||
test "Postal code is optional but must have 5 digits if specified", %{actor: actor} do
|
||||
attrs = Map.put(@valid_attrs, :postal_code, "1234")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Membership.create_member(attrs, actor: actor)
|
||||
|
||||
assert error_message(errors, :postal_code) =~ "must consist of 5 digits"
|
||||
attrs2 = Map.delete(@valid_attrs, :postal_code)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2, actor: actor)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,13 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
defp create_fee_type(attrs, actor) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -23,11 +28,11 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
defp create_member(attrs, actor) do
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -39,11 +44,11 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
defp create_cycle(member, fee_type, attrs, actor) do
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2024-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -56,17 +61,17 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
describe "type change cycle regeneration" do
|
||||
test "future unpaid cycles are regenerated with new amount" do
|
||||
test "future unpaid cycles are regenerated with new amount", %{actor: actor} do
|
||||
today = Date.utc_today()
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")})
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)
|
||||
|
||||
# Create member without fee type first to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
member = create_member(%{}, actor)
|
||||
|
||||
# Manually assign fee type (this will trigger cycle generation)
|
||||
member =
|
||||
|
|
@ -74,7 +79,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type1.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Cycle generation runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
@ -89,26 +94,31 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
# Check if it already exists (from auto-generation), if not create it
|
||||
case MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start)
|
||||
|> Ash.read_one() do
|
||||
|> Ash.read_one(actor: actor) do
|
||||
{:ok, existing_cycle} when not is_nil(existing_cycle) ->
|
||||
# Update to paid
|
||||
existing_cycle
|
||||
|> Ash.Changeset.for_update(:update, %{status: :paid})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
_ ->
|
||||
create_cycle(member, yearly_type1, %{
|
||||
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, %{
|
||||
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, %{
|
||||
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, %{
|
||||
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, %{
|
||||
create_cycle(
|
||||
member,
|
||||
yearly_type1,
|
||||
%{
|
||||
cycle_start: current_cycle_start,
|
||||
status: :unpaid,
|
||||
amount: Decimal.new("100.00")
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
end
|
||||
|
||||
# Load calculations before change
|
||||
|
|
@ -437,7 +467,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type2.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Cycle regeneration runs synchronously in the same transaction
|
||||
# No need to wait for async completion
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
alias Mv.Membership.Setting
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "membership fee settings" do
|
||||
test "default values are correct" do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
|
@ -18,7 +23,7 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
assert %Setting{} = settings
|
||||
end
|
||||
|
||||
test "settings can be written via update_membership_fee_settings" do
|
||||
test "settings can be written via update_membership_fee_settings", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
{:ok, updated} =
|
||||
|
|
@ -26,12 +31,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
include_joining_cycle: false
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated.include_joining_cycle == false
|
||||
end
|
||||
|
||||
test "default_membership_fee_type_id can be nil (optional)" do
|
||||
test "default_membership_fee_type_id can be nil (optional)", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
{:ok, updated} =
|
||||
|
|
@ -39,12 +44,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: nil
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated.default_membership_fee_type_id == nil
|
||||
end
|
||||
|
||||
test "default_membership_fee_type_id validation: must exist if set" do
|
||||
test "default_membership_fee_type_id validation: must exist if set", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
# Create a valid fee type
|
||||
|
|
@ -61,12 +66,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated.default_membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
||||
test "default_membership_fee_type_id validation: fails if not found" do
|
||||
test "default_membership_fee_type_id validation: fails if not found", %{actor: actor} do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
# Use a non-existent UUID
|
||||
|
|
@ -77,7 +82,7 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fake_uuid
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert error_on_field?(error, :default_membership_fee_type_id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,13 +6,18 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
|
||||
alias Mv.MembershipFees.Changes.SetMembershipFeeStartDate
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
# Helper to set up settings with specific include_joining_cycle value
|
||||
defp setup_settings(include_joining_cycle) do
|
||||
defp setup_settings(include_joining_cycle, actor) do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
settings
|
||||
|> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
end
|
||||
|
||||
describe "calculate_start_date/3" do
|
||||
|
|
@ -127,8 +132,8 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
end
|
||||
|
||||
describe "change/3 integration" do
|
||||
test "sets membership_fee_start_date automatically on member creation" do
|
||||
setup_settings(true)
|
||||
test "sets membership_fee_start_date automatically on member creation", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
|
||||
# Create a fee type
|
||||
fee_type =
|
||||
|
|
@ -138,7 +143,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Create member with join_date and fee type but no explicit start date
|
||||
member =
|
||||
|
|
@ -150,14 +155,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
join_date: ~D[2024-03-15],
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Should have auto-calculated start date (2024-01-01 for yearly with include_joining_cycle=true)
|
||||
assert member.membership_fee_start_date == ~D[2024-01-01]
|
||||
end
|
||||
|
||||
test "does not override manually set membership_fee_start_date" do
|
||||
setup_settings(true)
|
||||
test "does not override manually set membership_fee_start_date", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
|
||||
# Create a fee type
|
||||
fee_type =
|
||||
|
|
@ -167,7 +172,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Create member with explicit start date
|
||||
manual_start_date = ~D[2024-07-01]
|
||||
|
|
@ -182,14 +187,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: manual_start_date
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Should keep the manually set date
|
||||
assert member.membership_fee_start_date == manual_start_date
|
||||
end
|
||||
|
||||
test "respects include_joining_cycle = false setting" do
|
||||
setup_settings(false)
|
||||
test "respects include_joining_cycle = false setting", %{actor: actor} do
|
||||
setup_settings(false, actor)
|
||||
|
||||
# Create a fee type
|
||||
fee_type =
|
||||
|
|
@ -199,7 +204,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Create member
|
||||
member =
|
||||
|
|
@ -211,14 +216,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
join_date: ~D[2024-03-15],
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Should have next cycle start date (2025-01-01 for yearly with include_joining_cycle=false)
|
||||
assert member.membership_fee_start_date == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "does not set start date without join_date" do
|
||||
setup_settings(true)
|
||||
test "does not set start date without join_date", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
|
||||
# Create a fee type
|
||||
fee_type =
|
||||
|
|
@ -228,7 +233,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Create member without join_date
|
||||
member =
|
||||
|
|
@ -240,14 +245,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
membership_fee_type_id: fee_type.id
|
||||
# No join_date
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Should not have auto-calculated start date
|
||||
assert is_nil(member.membership_fee_start_date)
|
||||
end
|
||||
|
||||
test "does not set start date without membership_fee_type_id" do
|
||||
setup_settings(true)
|
||||
test "does not set start date without membership_fee_type_id", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
|
||||
# Create member without fee type
|
||||
member =
|
||||
|
|
@ -259,7 +264,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
|||
join_date: ~D[2024-03-15]
|
||||
# No membership_fee_type_id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Should not have auto-calculated start date
|
||||
assert is_nil(member.membership_fee_start_date)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.Changes.ValidateSameInterval
|
||||
|
||||
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"),
|
||||
|
|
@ -20,11 +25,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest 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",
|
||||
|
|
@ -35,15 +40,15 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
describe "validate_interval_match/1" do
|
||||
test "allows change to type with same interval" do
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"})
|
||||
test "allows change to type with same interval", %{actor: actor} do
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}, actor)
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type1.id})
|
||||
member = create_member(%{membership_fee_type_id: yearly_type1.id}, actor)
|
||||
|
||||
changeset =
|
||||
member
|
||||
|
|
@ -55,11 +60,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "prevents change to type with different interval" do
|
||||
yearly_type = create_fee_type(%{interval: :yearly})
|
||||
monthly_type = create_fee_type(%{interval: :monthly})
|
||||
test "prevents change to type with different interval", %{actor: actor} do
|
||||
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
monthly_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
||||
|
||||
changeset =
|
||||
member
|
||||
|
|
@ -78,10 +83,10 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "allows first assignment of membership fee type" do
|
||||
yearly_type = create_fee_type(%{interval: :yearly})
|
||||
test "allows first assignment of membership fee type", %{actor: actor} do
|
||||
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
# No fee type assigned
|
||||
member = create_member(%{})
|
||||
member = create_member(%{}, actor)
|
||||
|
||||
changeset =
|
||||
member
|
||||
|
|
@ -93,9 +98,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "prevents removal of membership fee type" do
|
||||
yearly_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||
test "prevents removal of membership fee type", %{actor: actor} do
|
||||
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
||||
|
||||
changeset =
|
||||
member
|
||||
|
|
@ -113,9 +118,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "does nothing when membership_fee_type_id is not changed" do
|
||||
yearly_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||
test "does nothing when membership_fee_type_id is not changed", %{actor: actor} do
|
||||
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
||||
|
||||
changeset =
|
||||
member
|
||||
|
|
@ -127,11 +132,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "error message is clear and helpful" do
|
||||
yearly_type = create_fee_type(%{interval: :yearly})
|
||||
quarterly_type = create_fee_type(%{interval: :quarterly})
|
||||
test "error message is clear and helpful", %{actor: actor} do
|
||||
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
quarterly_type = create_fee_type(%{interval: :quarterly}, actor)
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
||||
|
||||
changeset =
|
||||
member
|
||||
|
|
@ -146,25 +151,31 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
assert error.message =~ "same-interval"
|
||||
end
|
||||
|
||||
test "handles all interval types correctly" do
|
||||
test "handles all interval types correctly", %{actor: actor} do
|
||||
intervals = [:monthly, :quarterly, :half_yearly, :yearly]
|
||||
|
||||
for interval1 <- intervals,
|
||||
interval2 <- intervals,
|
||||
interval1 != interval2 do
|
||||
type1 =
|
||||
create_fee_type(%{
|
||||
create_fee_type(
|
||||
%{
|
||||
interval: interval1,
|
||||
name: "Type #{interval1} #{System.unique_integer([:positive])}"
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
type2 =
|
||||
create_fee_type(%{
|
||||
create_fee_type(
|
||||
%{
|
||||
interval: interval2,
|
||||
name: "Type #{interval2} #{System.unique_integer([:positive])}"
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
member = create_member(%{membership_fee_type_id: type1.id})
|
||||
member = create_member(%{membership_fee_type_id: type1.id}, actor)
|
||||
|
||||
changeset =
|
||||
member
|
||||
|
|
@ -180,11 +191,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
end
|
||||
|
||||
describe "integration with update_member action" do
|
||||
test "validation works when updating member via update_member action" do
|
||||
yearly_type = create_fee_type(%{interval: :yearly})
|
||||
monthly_type = create_fee_type(%{interval: :monthly})
|
||||
test "validation works when updating member via update_member action", %{actor: actor} do
|
||||
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
monthly_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
||||
|
||||
# Try to update member with different interval type
|
||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||
|
|
@ -192,7 +203,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: monthly_type.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Check that error is about interval mismatch
|
||||
error_message = extract_error_message(error)
|
||||
|
|
@ -201,11 +212,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
assert error_message =~ "same-interval"
|
||||
end
|
||||
|
||||
test "allows update when interval matches" do
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"})
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"})
|
||||
test "allows update when interval matches", %{actor: actor} do
|
||||
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}, actor)
|
||||
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}, actor)
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type1.id})
|
||||
member = create_member(%{membership_fee_type_id: yearly_type1.id}, actor)
|
||||
|
||||
# Update member with same-interval type
|
||||
assert {:ok, updated_member} =
|
||||
|
|
@ -213,7 +224,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|||
|> Ash.Changeset.for_update(:update_member, %{
|
||||
membership_fee_type_id: yearly_type2.id
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_member.membership_fee_type_id == yearly_type2.id
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,211 +8,287 @@ defmodule Mv.MembershipFees.ForeignKeyTest do
|
|||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.Membership.Member
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "CASCADE behavior" do
|
||||
test "deleting member deletes associated membership_fee_cycles" do
|
||||
test "deleting member deletes associated membership_fee_cycles", %{actor: actor} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Cascade",
|
||||
last_name: "Test",
|
||||
email: "cascade.test.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create fee type
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Cascade Test Fee #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :monthly
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create multiple cycles for this member
|
||||
{:ok, cycle1} =
|
||||
Ash.create(MembershipFeeCycle, %{
|
||||
Ash.create(
|
||||
MembershipFeeCycle,
|
||||
%{
|
||||
cycle_start: ~D[2025-01-01],
|
||||
amount: Decimal.new("100.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, cycle2} =
|
||||
Ash.create(MembershipFeeCycle, %{
|
||||
Ash.create(
|
||||
MembershipFeeCycle,
|
||||
%{
|
||||
cycle_start: ~D[2025-02-01],
|
||||
amount: Decimal.new("100.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify cycles exist
|
||||
assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle1.id)
|
||||
assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle2.id)
|
||||
assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle1.id, actor: actor)
|
||||
assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle2.id, actor: actor)
|
||||
|
||||
# Delete member
|
||||
assert :ok = Ash.destroy(member)
|
||||
assert :ok = Ash.destroy(member, actor: actor)
|
||||
|
||||
# Verify cycles are also deleted (CASCADE)
|
||||
# NotFound is wrapped in Ash.Error.Invalid
|
||||
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle1.id)
|
||||
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle2.id)
|
||||
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle1.id, actor: actor)
|
||||
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle2.id, actor: actor)
|
||||
end
|
||||
end
|
||||
|
||||
describe "RESTRICT behavior" do
|
||||
test "cannot delete membership_fee_type if cycles reference it" do
|
||||
test "cannot delete membership_fee_type if cycles reference it", %{actor: actor} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Restrict",
|
||||
last_name: "Test",
|
||||
email: "restrict.test.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create fee type
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Restrict Test Fee #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :monthly
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a cycle referencing this fee type
|
||||
{:ok, _cycle} =
|
||||
Ash.create(MembershipFeeCycle, %{
|
||||
Ash.create(
|
||||
MembershipFeeCycle,
|
||||
%{
|
||||
cycle_start: ~D[2025-01-01],
|
||||
amount: Decimal.new("100.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to delete fee type - should fail due to RESTRICT
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
|
||||
# Check that it's a foreign key violation error
|
||||
assert is_struct(error, Ash.Error.Invalid) or is_struct(error, Ash.Error.Unknown)
|
||||
end
|
||||
|
||||
test "can delete membership_fee_type if no cycles reference it" do
|
||||
test "can delete membership_fee_type if no cycles reference it", %{actor: actor} do
|
||||
# Create fee type without any cycles
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Deletable Fee #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :monthly
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should be able to delete
|
||||
assert :ok = Ash.destroy(fee_type)
|
||||
assert :ok = Ash.destroy(fee_type, actor: actor)
|
||||
|
||||
# Verify it's gone (NotFound is wrapped in Ash.Error.Invalid)
|
||||
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeType, fee_type.id)
|
||||
assert {:error, %Ash.Error.Invalid{}} =
|
||||
Ash.get(MembershipFeeType, fee_type.id, actor: actor)
|
||||
end
|
||||
|
||||
test "cannot delete membership_fee_type if members reference it" do
|
||||
test "cannot delete membership_fee_type if members reference it", %{actor: actor} do
|
||||
# Create fee type
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Member Ref Fee #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :monthly
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create member with this fee type
|
||||
{:ok, _member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "FeeType",
|
||||
last_name: "Reference",
|
||||
email: "feetype.ref.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Try to delete fee type - should fail due to RESTRICT
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
assert is_struct(error, Ash.Error.Invalid) or is_struct(error, Ash.Error.Unknown)
|
||||
end
|
||||
end
|
||||
|
||||
describe "member extensions" do
|
||||
test "member can be created with membership_fee_type_id" do
|
||||
test "member can be created with membership_fee_type_id", %{actor: actor} do
|
||||
# Create fee type first
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Create Test Fee #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create member with fee type
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "With",
|
||||
last_name: "FeeType",
|
||||
email: "with.feetype.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert member.membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
||||
test "member can be created with membership_fee_start_date" do
|
||||
test "member can be created with membership_fee_start_date", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "With",
|
||||
last_name: "StartDate",
|
||||
email: "with.startdate.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_start_date: ~D[2025-01-01]
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert member.membership_fee_start_date == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "member can be created without membership fee fields" do
|
||||
test "member can be created without membership fee fields", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "No",
|
||||
last_name: "FeeFields",
|
||||
email: "no.feefields.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert member.membership_fee_type_id == nil
|
||||
assert member.membership_fee_start_date == nil
|
||||
end
|
||||
|
||||
test "member can be updated with membership_fee_type_id" do
|
||||
test "member can be updated with membership_fee_type_id", %{actor: actor} do
|
||||
# Create fee type
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Update Test Fee #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create member without fee type
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Update",
|
||||
last_name: "Test",
|
||||
email: "update.test.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert member.membership_fee_type_id == nil
|
||||
|
||||
# Update member with fee type
|
||||
{:ok, updated_member} = Ash.update(member, %{membership_fee_type_id: fee_type.id})
|
||||
{:ok, updated_member} =
|
||||
Ash.update(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||
|
||||
assert updated_member.membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
||||
test "member can be updated with membership_fee_start_date" do
|
||||
test "member can be updated with membership_fee_start_date", %{actor: actor} do
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Start",
|
||||
last_name: "Date",
|
||||
email: "start.date.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert member.membership_fee_start_date == nil
|
||||
|
||||
{:ok, updated_member} = Ash.update(member, %{membership_fee_start_date: ~D[2025-06-01]})
|
||||
{:ok, updated_member} =
|
||||
Ash.update(member, %{membership_fee_start_date: ~D[2025-06-01]}, actor: actor)
|
||||
|
||||
assert updated_member.membership_fee_start_date == ~D[2025-06-01]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,8 +10,13 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest 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"),
|
||||
|
|
@ -22,30 +27,30 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to set up settings
|
||||
defp setup_settings(include_joining_cycle) do
|
||||
defp setup_settings(include_joining_cycle, actor) do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
settings
|
||||
|> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to get cycles for a member
|
||||
defp get_member_cycles(member_id) do
|
||||
defp get_member_cycles(member_id, actor) do
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member_id)
|
||||
|> Ash.Query.sort(cycle_start: :asc)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
end
|
||||
|
||||
describe "member creation triggers cycle generation" do
|
||||
test "creates cycles when member is created with fee type and join_date" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "creates cycles when member is created with fee type and join_date", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
member =
|
||||
Member
|
||||
|
|
@ -56,9 +61,9 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
join_date: ~D[2023-03-15],
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have cycles for 2023 and 2024 (and possibly current year)
|
||||
assert length(cycles) >= 2
|
||||
|
|
@ -72,8 +77,8 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "does not create cycles when member has no fee type" do
|
||||
setup_settings(true)
|
||||
test "does not create cycles when member has no fee type", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
|
||||
member =
|
||||
Member
|
||||
|
|
@ -84,16 +89,16 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
join_date: ~D[2023-03-15]
|
||||
# No membership_fee_type_id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
assert cycles == []
|
||||
end
|
||||
|
||||
test "does not create cycles when member has no join_date" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "does not create cycles when member has no join_date", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
member =
|
||||
Member
|
||||
|
|
@ -104,18 +109,18 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
membership_fee_type_id: fee_type.id
|
||||
# No join_date
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
assert cycles == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "member update triggers cycle generation" do
|
||||
test "generates cycles when fee type is assigned to existing member" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "generates cycles when fee type is assigned to existing member", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create member without fee type
|
||||
member =
|
||||
|
|
@ -126,17 +131,17 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
join_date: ~D[2023-03-15]
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Verify no cycles yet
|
||||
assert get_member_cycles(member.id) == []
|
||||
assert get_member_cycles(member.id, actor) == []
|
||||
|
||||
# Update to assign fee type
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have generated cycles
|
||||
assert length(cycles) >= 2
|
||||
|
|
@ -144,9 +149,9 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
end
|
||||
|
||||
describe "concurrent cycle generation" do
|
||||
test "handles multiple members being created concurrently" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "handles multiple members being created concurrently", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create multiple members concurrently
|
||||
tasks =
|
||||
|
|
@ -160,7 +165,7 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
join_date: ~D[2023-03-15],
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
@ -168,16 +173,16 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
|
||||
# Each member should have cycles
|
||||
Enum.each(members, fn member ->
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
assert length(cycles) >= 2, "Member #{member.id} should have at least 2 cycles"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "idempotent cycle generation" do
|
||||
test "running generation multiple times does not create duplicate cycles" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "running generation multiple times does not create duplicate cycles", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
member =
|
||||
Member
|
||||
|
|
@ -188,9 +193,9 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
join_date: ~D[2023-03-15],
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
initial_cycles = get_member_cycles(member.id)
|
||||
initial_cycles = get_member_cycles(member.id, actor)
|
||||
initial_count = length(initial_cycles)
|
||||
|
||||
# Use a fixed "today" date to avoid date dependency
|
||||
|
|
@ -201,7 +206,7 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|||
{:ok, _, _} =
|
||||
Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||
|
||||
final_cycles = get_member_cycles(member.id)
|
||||
final_cycles = get_member_cycles(member.id, actor)
|
||||
final_count = length(final_cycles)
|
||||
|
||||
# Should have same number of cycles (idempotent)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
|||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.Membership.Member
|
||||
|
||||
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"),
|
||||
|
|
@ -20,11 +25,11 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest 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",
|
||||
|
|
@ -35,11 +40,11 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest 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"),
|
||||
|
|
@ -51,13 +56,13 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
describe "status defaults" do
|
||||
test "status defaults to :unpaid when creating a cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "status defaults to :unpaid when creating a cycle", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
cycle =
|
||||
MembershipFeeCycle
|
||||
|
|
@ -67,29 +72,30 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
|||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
assert cycle.status == :unpaid
|
||||
end
|
||||
end
|
||||
|
||||
describe "mark_as_paid" do
|
||||
test "sets status to :paid" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||
test "sets status to :paid", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor)
|
||||
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_paid)
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_paid)
|
||||
assert updated.status == :paid
|
||||
end
|
||||
|
||||
test "can set notes when marking as paid" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||
test "can set notes when marking as paid", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor)
|
||||
|
||||
assert {:ok, updated} =
|
||||
Ash.update(cycle, %{notes: "Payment received via bank transfer"},
|
||||
actor: actor,
|
||||
action: :mark_as_paid
|
||||
)
|
||||
|
||||
|
|
@ -97,33 +103,34 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
|||
assert updated.notes == "Payment received via bank transfer"
|
||||
end
|
||||
|
||||
test "can change from suspended to paid" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :suspended})
|
||||
test "can change from suspended to paid", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :suspended}, actor)
|
||||
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_paid)
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_paid)
|
||||
assert updated.status == :paid
|
||||
end
|
||||
end
|
||||
|
||||
describe "mark_as_suspended" do
|
||||
test "sets status to :suspended" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||
test "sets status to :suspended", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor)
|
||||
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_suspended)
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_suspended)
|
||||
assert updated.status == :suspended
|
||||
end
|
||||
|
||||
test "can set notes when marking as suspended" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||
test "can set notes when marking as suspended", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor)
|
||||
|
||||
assert {:ok, updated} =
|
||||
Ash.update(cycle, %{notes: "Waived due to special circumstances"},
|
||||
actor: actor,
|
||||
action: :mark_as_suspended
|
||||
)
|
||||
|
||||
|
|
@ -131,42 +138,45 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
|||
assert updated.notes == "Waived due to special circumstances"
|
||||
end
|
||||
|
||||
test "can change from paid to suspended" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :paid})
|
||||
test "can change from paid to suspended", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :paid}, actor)
|
||||
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_suspended)
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_suspended)
|
||||
assert updated.status == :suspended
|
||||
end
|
||||
end
|
||||
|
||||
describe "mark_as_unpaid" do
|
||||
test "sets status to :unpaid" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :paid})
|
||||
test "sets status to :unpaid", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :paid}, actor)
|
||||
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid)
|
||||
assert updated.status == :unpaid
|
||||
end
|
||||
|
||||
test "can set notes when marking as unpaid" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :paid})
|
||||
test "can set notes when marking as unpaid", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :paid}, actor)
|
||||
|
||||
assert {:ok, updated} =
|
||||
Ash.update(cycle, %{notes: "Payment was reversed"}, action: :mark_as_unpaid)
|
||||
Ash.update(cycle, %{notes: "Payment was reversed"},
|
||||
actor: actor,
|
||||
action: :mark_as_unpaid
|
||||
)
|
||||
|
||||
assert updated.status == :unpaid
|
||||
assert updated.notes == "Payment was reversed"
|
||||
end
|
||||
|
||||
test "can change from suspended to unpaid" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
cycle = create_cycle(member, fee_type, %{status: :suspended})
|
||||
test "can change from suspended to unpaid", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
cycle = create_cycle(member, fee_type, %{status: :suspended}, actor)
|
||||
|
||||
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid)
|
||||
assert updated.status == :unpaid
|
||||
|
|
@ -174,12 +184,12 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
|||
end
|
||||
|
||||
describe "status transitions" do
|
||||
test "all status transitions are allowed" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
test "all status transitions are allowed", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
||||
|
||||
# unpaid -> paid
|
||||
cycle1 = create_cycle(member, fee_type, %{status: :unpaid})
|
||||
cycle1 = create_cycle(member, fee_type, %{status: :unpaid}, actor)
|
||||
assert {:ok, c1} = Ash.update(cycle1, %{}, action: :mark_as_paid)
|
||||
assert c1.status == :paid
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,13 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest 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"),
|
||||
|
|
@ -22,11 +27,11 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
describe "admin can create membership fee type" do
|
||||
test "creates type with all fields" do
|
||||
test "creates type with all fields", %{actor: actor} do
|
||||
attrs = %{
|
||||
name: "Standard Membership",
|
||||
amount: Decimal.new("120.00"),
|
||||
|
|
@ -34,7 +39,8 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
|||
description: "Standard yearly membership fee"
|
||||
}
|
||||
|
||||
assert {:ok, %MembershipFeeType{} = fee_type} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, %MembershipFeeType{} = fee_type} =
|
||||
Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
|
||||
assert fee_type.name == "Standard Membership"
|
||||
assert Decimal.equal?(fee_type.amount, Decimal.new("120.00"))
|
||||
|
|
@ -44,88 +50,106 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
|||
end
|
||||
|
||||
describe "admin can update membership fee type" do
|
||||
setup do
|
||||
setup %{actor: actor} do
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Original Name",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :yearly,
|
||||
description: "Original description"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
%{fee_type: fee_type}
|
||||
end
|
||||
|
||||
test "can update name", %{fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"})
|
||||
test "can update name", %{actor: actor, fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}, actor: actor)
|
||||
assert updated.name == "Updated Name"
|
||||
end
|
||||
|
||||
test "can update amount", %{fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")})
|
||||
test "can update amount", %{actor: actor, fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}, actor: actor)
|
||||
assert Decimal.equal?(updated.amount, Decimal.new("150.00"))
|
||||
end
|
||||
|
||||
test "can update description", %{fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{description: "Updated description"})
|
||||
test "can update description", %{actor: actor, fee_type: fee_type} do
|
||||
assert {:ok, updated} =
|
||||
Ash.update(fee_type, %{description: "Updated description"}, actor: actor)
|
||||
|
||||
assert updated.description == "Updated description"
|
||||
end
|
||||
|
||||
test "cannot update interval", %{fee_type: fee_type} do
|
||||
test "cannot update interval", %{actor: actor, fee_type: fee_type} do
|
||||
# Currently, interval is not in the accept list, so it's rejected as "NoSuchInput"
|
||||
# After implementing validation, it should return a validation error
|
||||
assert {:error, error} = Ash.update(fee_type, %{interval: :monthly})
|
||||
assert {:error, error} = Ash.update(fee_type, %{interval: :monthly}, actor: actor)
|
||||
# For now, check that it's an error (either NoSuchInput or validation error)
|
||||
assert %Ash.Error.Invalid{} = error
|
||||
end
|
||||
end
|
||||
|
||||
describe "admin cannot delete membership fee type when in use" do
|
||||
test "cannot delete when members are assigned" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "cannot delete when members are assigned", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create a member with this fee type
|
||||
{:ok, _member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
error_message = extract_error_message(error)
|
||||
assert error_message =~ "member(s) are assigned"
|
||||
end
|
||||
|
||||
test "cannot delete when cycles exist" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "cannot delete when cycles exist", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create a member with this fee type
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a cycle for this fee type
|
||||
{:ok, _cycle} =
|
||||
Ash.create(MembershipFeeCycle, %{
|
||||
Ash.create(
|
||||
MembershipFeeCycle,
|
||||
%{
|
||||
cycle_start: ~D[2025-01-01],
|
||||
amount: Decimal.new("100.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
error_message = extract_error_message(error)
|
||||
assert error_message =~ "cycle(s) reference"
|
||||
end
|
||||
|
||||
test "cannot delete when used as default in settings" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "cannot delete when used as default in settings", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Set as default in settings
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
|
@ -134,19 +158,19 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Try to delete
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
error_message = extract_error_message(error)
|
||||
assert error_message =~ "used as default in settings"
|
||||
end
|
||||
end
|
||||
|
||||
describe "settings integration" do
|
||||
test "default_membership_fee_type_id is used during member creation" do
|
||||
test "default_membership_fee_type_id is used during member creation", %{actor: actor} do
|
||||
# Create a fee type
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Set it as default in settings
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
|
@ -155,29 +179,33 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Create a member without explicitly setting membership_fee_type_id
|
||||
# The Member resource automatically assigns the default_membership_fee_type_id
|
||||
# during creation via SetDefaultMembershipFeeType change.
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify that the default membership fee type was automatically assigned
|
||||
assert member.membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
||||
test "include_joining_cycle is used during cycle generation" do
|
||||
test "include_joining_cycle is used during cycle generation", %{actor: actor} do
|
||||
# This test verifies that the include_joining_cycle setting affects
|
||||
# cycle generation. The actual cycle generation logic is tested in
|
||||
# CycleGeneratorTest, but this integration test ensures the setting
|
||||
# is properly used.
|
||||
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Set include_joining_cycle to false
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
|
@ -186,17 +214,21 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
include_joining_cycle: false
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Create a member with join_date in the middle of a year
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com",
|
||||
join_date: ~D[2023-03-15],
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Verify that membership_fee_start_date was calculated correctly
|
||||
# (should be 2024-01-01, not 2023-01-01, because include_joining_cycle = false)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,13 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
|
|||
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "create MembershipFeeType" do
|
||||
test "can create membership fee type with valid attributes" do
|
||||
test "can create membership fee type with valid attributes", %{actor: actor} do
|
||||
attrs = %{
|
||||
name: "Standard Membership",
|
||||
amount: Decimal.new("120.00"),
|
||||
|
|
@ -16,7 +21,7 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
|
|||
}
|
||||
|
||||
assert {:ok, %MembershipFeeType{} = fee_type} =
|
||||
Ash.create(MembershipFeeType, attrs)
|
||||
Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
|
||||
assert fee_type.name == "Standard Membership"
|
||||
assert Decimal.equal?(fee_type.amount, Decimal.new("120.00"))
|
||||
|
|
@ -24,212 +29,237 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
|
|||
assert fee_type.description == "Standard yearly membership fee"
|
||||
end
|
||||
|
||||
test "can create membership fee type without description" do
|
||||
test "can create membership fee type without description", %{actor: actor} do
|
||||
attrs = %{
|
||||
name: "Basic",
|
||||
amount: Decimal.new("60.00"),
|
||||
interval: :monthly
|
||||
}
|
||||
|
||||
assert {:ok, %MembershipFeeType{}} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, %MembershipFeeType{}} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
end
|
||||
|
||||
test "requires name" do
|
||||
test "requires name", %{actor: actor} do
|
||||
attrs = %{
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :yearly
|
||||
}
|
||||
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert error_on_field?(error, :name)
|
||||
end
|
||||
|
||||
test "requires amount" do
|
||||
test "requires amount", %{actor: actor} do
|
||||
attrs = %{
|
||||
name: "Test Fee",
|
||||
interval: :yearly
|
||||
}
|
||||
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert error_on_field?(error, :amount)
|
||||
end
|
||||
|
||||
test "requires interval" do
|
||||
test "requires interval", %{actor: actor} do
|
||||
attrs = %{
|
||||
name: "Test Fee",
|
||||
amount: Decimal.new("100.00")
|
||||
}
|
||||
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert error_on_field?(error, :interval)
|
||||
end
|
||||
|
||||
test "validates interval enum values - monthly" do
|
||||
test "validates interval enum values - monthly", %{actor: actor} do
|
||||
attrs = %{name: "Monthly", amount: Decimal.new("10.00"), interval: :monthly}
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert fee_type.interval == :monthly
|
||||
end
|
||||
|
||||
test "validates interval enum values - quarterly" do
|
||||
test "validates interval enum values - quarterly", %{actor: actor} do
|
||||
attrs = %{name: "Quarterly", amount: Decimal.new("30.00"), interval: :quarterly}
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert fee_type.interval == :quarterly
|
||||
end
|
||||
|
||||
test "validates interval enum values - half_yearly" do
|
||||
test "validates interval enum values - half_yearly", %{actor: actor} do
|
||||
attrs = %{name: "Half Yearly", amount: Decimal.new("60.00"), interval: :half_yearly}
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert fee_type.interval == :half_yearly
|
||||
end
|
||||
|
||||
test "validates interval enum values - yearly" do
|
||||
test "validates interval enum values - yearly", %{actor: actor} do
|
||||
attrs = %{name: "Yearly", amount: Decimal.new("120.00"), interval: :yearly}
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert fee_type.interval == :yearly
|
||||
end
|
||||
|
||||
test "rejects invalid interval values" do
|
||||
test "rejects invalid interval values", %{actor: actor} do
|
||||
attrs = %{name: "Invalid", amount: Decimal.new("100.00"), interval: :weekly}
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert error_on_field?(error, :interval)
|
||||
end
|
||||
|
||||
test "name must be unique" do
|
||||
test "name must be unique", %{actor: actor} do
|
||||
attrs = %{name: "Unique Name", amount: Decimal.new("100.00"), interval: :yearly}
|
||||
|
||||
assert {:ok, _} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, _} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
|
||||
# Check for uniqueness error
|
||||
assert error_on_field?(error, :name)
|
||||
end
|
||||
|
||||
test "rejects negative amount" do
|
||||
test "rejects negative amount", %{actor: actor} do
|
||||
attrs = %{name: "Negative Test", amount: Decimal.new("-10.00"), interval: :yearly}
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert error_on_field?(error, :amount)
|
||||
end
|
||||
|
||||
test "accepts zero amount" do
|
||||
test "accepts zero amount", %{actor: actor} do
|
||||
attrs = %{name: "Zero Amount", amount: Decimal.new("0.00"), interval: :yearly}
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert Decimal.equal?(fee_type.amount, Decimal.new("0.00"))
|
||||
end
|
||||
|
||||
test "amount respects scale of 2 decimal places" do
|
||||
test "amount respects scale of 2 decimal places", %{actor: actor} do
|
||||
attrs = %{name: "Scale Test", amount: Decimal.new("100.50"), interval: :yearly}
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs)
|
||||
assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
|
||||
assert Decimal.equal?(fee_type.amount, Decimal.new("100.50"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "update MembershipFeeType" do
|
||||
setup do
|
||||
setup %{actor: actor} do
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Original Name",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :yearly,
|
||||
description: "Original description"
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
%{fee_type: fee_type}
|
||||
end
|
||||
|
||||
test "can update name", %{fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"})
|
||||
test "can update name", %{actor: actor, fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}, actor: actor)
|
||||
assert updated.name == "Updated Name"
|
||||
end
|
||||
|
||||
test "can update amount", %{fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")})
|
||||
test "can update amount", %{actor: actor, fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}, actor: actor)
|
||||
assert Decimal.equal?(updated.amount, Decimal.new("150.00"))
|
||||
end
|
||||
|
||||
test "can update description", %{fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{description: "Updated description"})
|
||||
test "can update description", %{actor: actor, fee_type: fee_type} do
|
||||
assert {:ok, updated} =
|
||||
Ash.update(fee_type, %{description: "Updated description"}, actor: actor)
|
||||
|
||||
assert updated.description == "Updated description"
|
||||
end
|
||||
|
||||
test "can clear description", %{fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{description: nil})
|
||||
test "can clear description", %{actor: actor, fee_type: fee_type} do
|
||||
assert {:ok, updated} = Ash.update(fee_type, %{description: nil}, actor: actor)
|
||||
assert updated.description == nil
|
||||
end
|
||||
|
||||
test "interval immutability: update fails when interval is changed", %{fee_type: fee_type} do
|
||||
test "interval immutability: update fails when interval is changed", %{
|
||||
actor: actor,
|
||||
fee_type: fee_type
|
||||
} do
|
||||
# Currently, interval is not in the accept list, so it's rejected as "NoSuchInput"
|
||||
# After implementing validation, it should return a validation error
|
||||
assert {:error, error} = Ash.update(fee_type, %{interval: :monthly})
|
||||
assert {:error, error} = Ash.update(fee_type, %{interval: :monthly}, actor: actor)
|
||||
# For now, check that it's an error (either NoSuchInput or validation error)
|
||||
assert %Ash.Error.Invalid{} = error
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete MembershipFeeType" do
|
||||
setup do
|
||||
setup %{actor: actor} do
|
||||
{:ok, fee_type} =
|
||||
Ash.create(MembershipFeeType, %{
|
||||
Ash.create(
|
||||
MembershipFeeType,
|
||||
%{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("100.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
%{fee_type: fee_type}
|
||||
end
|
||||
|
||||
test "can delete when not in use", %{fee_type: fee_type} do
|
||||
result = Ash.destroy(fee_type)
|
||||
test "can delete when not in use", %{actor: actor, fee_type: fee_type} do
|
||||
result = Ash.destroy(fee_type, actor: actor)
|
||||
# Ash.destroy returns :ok or {:ok, _} depending on version
|
||||
assert result == :ok or match?({:ok, _}, result)
|
||||
end
|
||||
|
||||
test "cannot delete when members are assigned", %{fee_type: fee_type} do
|
||||
test "cannot delete when members are assigned", %{actor: actor, fee_type: fee_type} do
|
||||
alias Mv.Membership.Member
|
||||
|
||||
# Create a member with this fee type
|
||||
{:ok, _member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
# Check for either validation error message or DB constraint error
|
||||
error_message = extract_error_message(error)
|
||||
assert error_message =~ "member" or error_message =~ "referenced"
|
||||
end
|
||||
|
||||
test "cannot delete when cycles exist", %{fee_type: fee_type} do
|
||||
test "cannot delete when cycles exist", %{actor: actor, fee_type: fee_type} do
|
||||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
alias Mv.Membership.Member
|
||||
|
||||
# Create a member with this fee type
|
||||
{:ok, member} =
|
||||
Ash.create(Member, %{
|
||||
Ash.create(
|
||||
Member,
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Create a cycle for this fee type
|
||||
{:ok, _cycle} =
|
||||
Ash.create(MembershipFeeCycle, %{
|
||||
Ash.create(
|
||||
MembershipFeeCycle,
|
||||
%{
|
||||
cycle_start: ~D[2025-01-01],
|
||||
amount: Decimal.new("100.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
# Check for either validation error message or DB constraint error
|
||||
error_message = extract_error_message(error)
|
||||
assert error_message =~ "cycle" or error_message =~ "referenced"
|
||||
end
|
||||
|
||||
test "cannot delete when used as default in settings", %{fee_type: fee_type} do
|
||||
test "cannot delete when used as default in settings", %{actor: actor, fee_type: fee_type} do
|
||||
# Set as default in settings
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
|
|
@ -237,10 +267,10 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
|
|||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Try to delete
|
||||
assert {:error, error} = Ash.destroy(fee_type)
|
||||
assert {:error, error} = Ash.destroy(fee_type, actor: actor)
|
||||
error_message = extract_error_message(error)
|
||||
assert error_message =~ "used as default in settings"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
# Helper to create a role with a specific permission set
|
||||
defp create_role_with_permission_set(permission_set_name) do
|
||||
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
||||
|
|
@ -30,7 +35,7 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
|
||||
# Helper to create a user with a specific permission set
|
||||
# Returns user with role preloaded (required for authorization)
|
||||
defp create_user_with_permission_set(permission_set_name) do
|
||||
defp create_user_with_permission_set(permission_set_name, actor) do
|
||||
# Create role with permission set
|
||||
role = create_role_with_permission_set(permission_set_name)
|
||||
|
||||
|
|
@ -41,39 +46,40 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
email: "user#{System.unique_integer([:positive])}@example.com",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Assign role to user
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Reload user with role preloaded (critical for authorization!)
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts)
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
|
||||
user_with_role
|
||||
end
|
||||
|
||||
# Helper to create another user (for testing access to other users)
|
||||
defp create_other_user do
|
||||
create_user_with_permission_set("own_data")
|
||||
defp create_other_user(actor) do
|
||||
create_user_with_permission_set("own_data", actor)
|
||||
end
|
||||
|
||||
# Shared test setup for permission sets with scope :own access
|
||||
defp setup_user_with_own_access(permission_set) do
|
||||
user = create_user_with_permission_set(permission_set)
|
||||
other_user = create_other_user()
|
||||
defp setup_user_with_own_access(permission_set, actor) do
|
||||
user = create_user_with_permission_set(permission_set, actor)
|
||||
other_user = create_other_user(actor)
|
||||
|
||||
# Reload user to ensure role is preloaded
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
%{user: user, other_user: other_user}
|
||||
end
|
||||
|
||||
describe "own_data permission set (Mitglied)" do
|
||||
setup do
|
||||
setup_user_with_own_access("own_data")
|
||||
setup %{actor: actor} do
|
||||
setup_user_with_own_access("own_data", actor)
|
||||
end
|
||||
|
||||
test "can read own user record", %{user: user} do
|
||||
|
|
@ -140,8 +146,8 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
end
|
||||
|
||||
describe "read_only permission set (Vorstand/Buchhaltung)" do
|
||||
setup do
|
||||
setup_user_with_own_access("read_only")
|
||||
setup %{actor: actor} do
|
||||
setup_user_with_own_access("read_only", actor)
|
||||
end
|
||||
|
||||
test "can read own user record", %{user: user} do
|
||||
|
|
@ -208,8 +214,8 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
end
|
||||
|
||||
describe "normal_user permission set (Kassenwart)" do
|
||||
setup do
|
||||
setup_user_with_own_access("normal_user")
|
||||
setup %{actor: actor} do
|
||||
setup_user_with_own_access("normal_user", actor)
|
||||
end
|
||||
|
||||
test "can read own user record", %{user: user} do
|
||||
|
|
@ -276,12 +282,13 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
end
|
||||
|
||||
describe "admin permission set" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("admin")
|
||||
other_user = create_other_user()
|
||||
setup %{actor: actor} do
|
||||
user = create_user_with_permission_set("admin", actor)
|
||||
other_user = create_other_user(actor)
|
||||
|
||||
# Reload user to ensure role is preloaded
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
%{user: user, other_user: other_user}
|
||||
end
|
||||
|
|
@ -335,19 +342,27 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
describe "AshAuthentication bypass" do
|
||||
test "register_with_password works without actor" do
|
||||
# Registration should work without actor (AshAuthentication bypass)
|
||||
# Note: When directly calling Ash actions in tests, the AshAuthentication bypass
|
||||
# may not be active, so we use system_actor
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||
email: "register#{System.unique_integer([:positive])}@example.com",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert user.email
|
||||
end
|
||||
|
||||
test "register_with_rauthy works with OIDC user_info" do
|
||||
# OIDC registration should work (AshAuthentication bypass)
|
||||
# Note: When directly calling Ash actions in tests, the AshAuthentication bypass
|
||||
# may not be active, so we use system_actor
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
user_info = %{
|
||||
"sub" => "oidc_sub_#{System.unique_integer([:positive])}",
|
||||
"email" => "oidc#{System.unique_integer([:positive])}@example.com"
|
||||
|
|
@ -361,7 +376,7 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
user_info: user_info,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
assert user.email
|
||||
assert user.oidc_id == user_info["sub"]
|
||||
|
|
@ -376,13 +391,15 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
|
||||
oauth_tokens = %{access_token: "token", refresh_token: "refresh"}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_rauthy, %{
|
||||
user_info: user_info_create,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Now test sign_in_with_rauthy (should work via AshAuthentication bypass)
|
||||
{:ok, signed_in_user} =
|
||||
|
|
@ -391,7 +408,7 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
user_info: user_info_create,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.read_one()
|
||||
|> Ash.read_one(actor: system_actor)
|
||||
|
||||
assert signed_in_user.id == user.id
|
||||
end
|
||||
|
|
@ -403,22 +420,4 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
|||
# when called through the proper authentication flow (sign_in, token refresh, etc.).
|
||||
# Integration tests that use actual JWT tokens cover this functionality.
|
||||
end
|
||||
|
||||
describe "test environment bypass (NoActor)" do
|
||||
test "operations without actor are allowed in test environment" do
|
||||
# In test environment, NoActor check should allow operations
|
||||
{:ok, user} =
|
||||
Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "noactor#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert user.email
|
||||
|
||||
# Read should also work
|
||||
{:ok, fetched_user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts)
|
||||
assert fetched_user.id == user.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,12 +7,17 @@ defmodule Mv.Authorization.ActorTest do
|
|||
alias Mv.Accounts
|
||||
alias Mv.Authorization.Actor
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "ensure_loaded/1" do
|
||||
test "returns nil when actor is nil" do
|
||||
assert Actor.ensure_loaded(nil) == nil
|
||||
end
|
||||
|
||||
test "returns actor as-is when role is already loaded" do
|
||||
test "returns actor as-is when role is already loaded", %{actor: actor} do
|
||||
# Create user with role
|
||||
{:ok, user} =
|
||||
Accounts.User
|
||||
|
|
@ -20,7 +25,7 @@ defmodule Mv.Authorization.ActorTest do
|
|||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Load role
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts)
|
||||
|
|
@ -31,7 +36,7 @@ defmodule Mv.Authorization.ActorTest do
|
|||
assert result.role != %Ash.NotLoaded{}
|
||||
end
|
||||
|
||||
test "loads role when it's NotLoaded" do
|
||||
test "loads role when it's NotLoaded", %{actor: actor} do
|
||||
# Create a role first
|
||||
{:ok, role} =
|
||||
Mv.Authorization.Role
|
||||
|
|
@ -40,7 +45,7 @@ defmodule Mv.Authorization.ActorTest do
|
|||
description: "Test role",
|
||||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create user with role
|
||||
{:ok, user} =
|
||||
|
|
@ -49,18 +54,18 @@ defmodule Mv.Authorization.ActorTest do
|
|||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Assign role to user
|
||||
{:ok, user_with_role} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Fetch user again WITHOUT loading role (simulates "role not preloaded" scenario)
|
||||
{:ok, user_without_role_loaded} =
|
||||
Ash.get(Accounts.User, user_with_role.id, domain: Mv.Accounts)
|
||||
Ash.get(Accounts.User, user_with_role.id, domain: Mv.Accounts, actor: actor)
|
||||
|
||||
# User has role as NotLoaded (relationship not preloaded)
|
||||
assert match?(%Ash.NotLoaded{}, user_without_role_loaded.role)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ defmodule Mv.Authorization.Checks.HasPermissionFailClosedTest do
|
|||
|> Ash.Query.new()
|
||||
|> Ash.Query.filter_input(deny_filter)
|
||||
|
||||
{:ok, results} = Ash.read(query, domain: Mv.Membership, authorize?: false)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, results} =
|
||||
Ash.read(query, domain: Mv.Membership, authorize?: false, actor: system_actor)
|
||||
|
||||
# Assert: deny-filter must match nothing
|
||||
assert results == []
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ defmodule Mv.Authorization.RoleTest do
|
|||
|
||||
alias Mv.Authorization
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "permission_set_name validation" do
|
||||
test "accepts valid permission set names" do
|
||||
attrs = %{
|
||||
|
|
@ -42,7 +47,7 @@ defmodule Mv.Authorization.RoleTest do
|
|||
end
|
||||
|
||||
describe "system role deletion protection" do
|
||||
test "prevents deletion of system roles" do
|
||||
test "prevents deletion of system roles", %{actor: actor} do
|
||||
# is_system_role is not settable via public API, so we use Ash.Changeset directly
|
||||
changeset =
|
||||
Mv.Authorization.Role
|
||||
|
|
@ -52,7 +57,7 @@ defmodule Mv.Authorization.RoleTest do
|
|||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|
||||
{:ok, system_role} = Ash.create(changeset)
|
||||
{:ok, system_role} = Ash.create(changeset, actor: actor)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Authorization.destroy_role(system_role)
|
||||
|
|
|
|||
|
|
@ -43,51 +43,55 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
# Helper function to ensure system user exists with admin role
|
||||
defp ensure_system_user(admin_role) do
|
||||
# Use authorize?: false for bootstrap operations
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||
|> Ash.update!(authorize?: false)
|
||||
|> Ash.load!(:role, domain: Mv.Accounts, authorize?: false)
|
||||
|
||||
_ ->
|
||||
Accounts.create_user!(%{email: "system@mila.local"},
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email
|
||||
upsert_identity: :unique_email,
|
||||
authorize?: false
|
||||
)
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||
|> Ash.update!(authorize?: false)
|
||||
|> Ash.load!(:role, domain: Mv.Accounts, authorize?: false)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to ensure admin user exists with admin role
|
||||
defp ensure_admin_user(admin_role) do
|
||||
# Use authorize?: false for bootstrap operations
|
||||
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^admin_email)
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||
|> Ash.update!(authorize?: false)
|
||||
|> Ash.load!(:role, domain: Mv.Accounts, authorize?: false)
|
||||
|
||||
_ ->
|
||||
Accounts.create_user!(%{email: admin_email},
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email
|
||||
upsert_identity: :unique_email,
|
||||
authorize?: false
|
||||
)
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||
|> Ash.update!(authorize?: false)
|
||||
|> Ash.load!(:role, domain: Mv.Accounts, authorize?: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -114,11 +118,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
test "falls back to admin user if system user doesn't exist", %{admin_user: _admin_user} do
|
||||
# Delete system user if it exists
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -151,11 +157,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
test "creates system user in test environment if none exists", %{admin_role: _admin_role} do
|
||||
# In test environment, system actor should auto-create if missing
|
||||
# Delete all users to test auto-creation
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -163,11 +171,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^admin_email)
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -211,11 +221,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
test "returns error tuple when system actor cannot be loaded" do
|
||||
# Delete all users to force error
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -223,11 +235,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^admin_email)
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -252,18 +266,22 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
describe "edge cases" do
|
||||
test "raises error if admin user has no role", %{admin_user: admin_user} do
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
# Remove role from admin user
|
||||
admin_user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, nil, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
# Delete system user to force fallback
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -279,11 +297,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
test "handles concurrent calls without race conditions" do
|
||||
# Delete system user and admin user to force creation
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -291,11 +311,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|
||||
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^admin_email)
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do
|
||||
{:ok, user} when not is_nil(user) ->
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
|
@ -330,11 +352,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
permission_set_name: "read_only"
|
||||
})
|
||||
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
# Assign wrong role to system user
|
||||
system_user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, read_only_role, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
SystemActor.invalidate_cache()
|
||||
|
||||
|
|
@ -345,11 +369,13 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
end
|
||||
|
||||
test "raises error if system user has no role", %{system_user: system_user} do
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
# Remove role from system user
|
||||
system_user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, nil, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
SystemActor.invalidate_cache()
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ defmodule Mv.Membership.Import.MemberCSVTest do
|
|||
assert chunk_result.errors == []
|
||||
|
||||
# Verify member was created
|
||||
members = Mv.Membership.list_members!()
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
members = Mv.Membership.list_members!(actor: system_actor)
|
||||
assert Enum.any?(members, &(&1.email == "john@example.com"))
|
||||
end
|
||||
|
||||
|
|
@ -174,8 +175,12 @@ defmodule Mv.Membership.Import.MemberCSVTest do
|
|||
|
||||
test "returns error for duplicate email" do
|
||||
# Create existing member first
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, _existing} =
|
||||
Mv.Membership.create_member(%{email: "duplicate@example.com", first_name: "Existing"})
|
||||
Mv.Membership.create_member(%{email: "duplicate@example.com", first_name: "Existing"},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
chunk_rows_with_lines = [
|
||||
{2, %{member: %{email: "duplicate@example.com", first_name: "New"}, custom: %{}}}
|
||||
|
|
@ -199,6 +204,8 @@ defmodule Mv.Membership.Import.MemberCSVTest do
|
|||
end
|
||||
|
||||
test "creates member with custom field values" do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create custom field first
|
||||
{:ok, custom_field} =
|
||||
Mv.Membership.CustomField
|
||||
|
|
@ -206,7 +213,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
|
|||
name: "Phone",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
chunk_rows_with_lines = [
|
||||
{2,
|
||||
|
|
@ -232,7 +239,8 @@ defmodule Mv.Membership.Import.MemberCSVTest do
|
|||
assert chunk_result.failed == 0
|
||||
|
||||
# Verify member and custom field value were created
|
||||
members = Mv.Membership.list_members!()
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
members = Mv.Membership.list_members!(actor: system_actor)
|
||||
member = Enum.find(members, &(&1.email == "withcustom@example.com"))
|
||||
assert member != nil
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
# Helper to create a role with a specific permission set
|
||||
defp create_role_with_permission_set(permission_set_name) do
|
||||
defp create_role_with_permission_set(permission_set_name, _actor) do
|
||||
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
||||
|
||||
case Authorization.create_role(%{
|
||||
|
|
@ -32,9 +37,9 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
|
||||
# Helper to create a user with a specific permission set
|
||||
# Returns user with role preloaded (required for authorization)
|
||||
defp create_user_with_permission_set(permission_set_name) do
|
||||
defp create_user_with_permission_set(permission_set_name, actor) do
|
||||
# Create role with permission set
|
||||
role = create_role_with_permission_set(permission_set_name)
|
||||
role = create_role_with_permission_set(permission_set_name, actor)
|
||||
|
||||
# Create user
|
||||
{:ok, user} =
|
||||
|
|
@ -43,28 +48,28 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
email: "user#{System.unique_integer([:positive])}@example.com",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Assign role to user
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Reload user with role preloaded (critical for authorization!)
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts)
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
|
||||
user_with_role
|
||||
end
|
||||
|
||||
# Helper to create an admin user (for creating test fixtures)
|
||||
defp create_admin_user do
|
||||
create_user_with_permission_set("admin")
|
||||
defp create_admin_user(actor) do
|
||||
create_user_with_permission_set("admin", actor)
|
||||
end
|
||||
|
||||
# Helper to create a member linked to a user
|
||||
defp create_linked_member_for_user(user) do
|
||||
admin = create_admin_user()
|
||||
defp create_linked_member_for_user(user, actor) do
|
||||
admin = create_admin_user(actor)
|
||||
|
||||
# Create member
|
||||
# NOTE: We need to ensure the member is actually persisted to the database
|
||||
|
|
@ -96,8 +101,8 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
|
||||
# Helper to create an unlinked member (no user relationship)
|
||||
defp create_unlinked_member do
|
||||
admin = create_admin_user()
|
||||
defp create_unlinked_member(actor) do
|
||||
admin = create_admin_user(actor)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.Member
|
||||
|
|
@ -112,14 +117,16 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
|
||||
describe "own_data permission set (Mitglied)" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("own_data")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
setup %{actor: actor} do
|
||||
user = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(user, actor)
|
||||
unlinked_member = create_unlinked_member(actor)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
|
@ -165,7 +172,10 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "list members returns only linked member", %{user: user, linked_member: linked_member} do
|
||||
test "list members returns only linked member", %{
|
||||
user: user,
|
||||
linked_member: linked_member
|
||||
} do
|
||||
{:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)
|
||||
|
||||
# Should only return the linked member (scope :linked filters)
|
||||
|
|
@ -185,7 +195,10 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "cannot destroy member (returns forbidden)", %{user: user, linked_member: linked_member} do
|
||||
test "cannot destroy member (returns forbidden)", %{
|
||||
user: user,
|
||||
linked_member: linked_member
|
||||
} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
Ash.destroy!(linked_member, actor: user)
|
||||
end
|
||||
|
|
@ -193,13 +206,14 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
|
||||
describe "read_only permission set (Vorstand/Buchhaltung)" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("read_only")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
setup %{actor: actor} do
|
||||
user = create_user_with_permission_set("read_only", actor)
|
||||
linked_member = create_linked_member_for_user(user, actor)
|
||||
unlinked_member = create_unlinked_member(actor)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
|
@ -217,7 +231,10 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
assert unlinked_member.id in member_ids
|
||||
end
|
||||
|
||||
test "can read individual member", %{user: user, unlinked_member: unlinked_member} do
|
||||
test "can read individual member", %{
|
||||
user: user,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
{:ok, member} =
|
||||
Ash.get(Membership.Member, unlinked_member.id, actor: user, domain: Mv.Membership)
|
||||
|
||||
|
|
@ -258,13 +275,14 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
|
||||
describe "normal_user permission set (Kassenwart)" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("normal_user")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
setup %{actor: actor} do
|
||||
user = create_user_with_permission_set("normal_user", actor)
|
||||
linked_member = create_linked_member_for_user(user, actor)
|
||||
unlinked_member = create_unlinked_member(actor)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
|
@ -315,13 +333,14 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
|
||||
describe "admin permission set" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("admin")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
setup %{actor: actor} do
|
||||
user = create_user_with_permission_set("admin", actor)
|
||||
linked_member = create_linked_member_for_user(user, actor)
|
||||
unlinked_member = create_unlinked_member(actor)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
|
@ -361,7 +380,10 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
assert updated_member.first_name == "Updated"
|
||||
end
|
||||
|
||||
test "can destroy any member", %{user: user, unlinked_member: unlinked_member} do
|
||||
test "can destroy any member", %{
|
||||
user: user,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
:ok = Ash.destroy(unlinked_member, actor: user)
|
||||
|
||||
# Verify member is deleted
|
||||
|
|
@ -370,19 +392,24 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
end
|
||||
|
||||
describe "special case: user can always READ linked member" do
|
||||
setup %{actor: _actor} do
|
||||
# Note: The special case policy only applies to :read actions.
|
||||
# Updates are handled by HasPermission with :linked scope (if permission exists).
|
||||
:ok
|
||||
end
|
||||
|
||||
test "read_only user can read linked member (via special case bypass)" do
|
||||
test "read_only user can read linked member (via special case bypass)", %{actor: actor} do
|
||||
# read_only has Member.read scope :all, but the special case ensures
|
||||
# users can ALWAYS read their linked member, even if they had no read permission.
|
||||
# This test verifies the special case works independently of permission sets.
|
||||
user = create_user_with_permission_set("read_only")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
user = create_user_with_permission_set("read_only", actor)
|
||||
linked_member = create_linked_member_for_user(user, actor)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)
|
||||
|
||||
# Should succeed (special case bypass policy for :read takes precedence)
|
||||
{:ok, member} =
|
||||
|
|
@ -391,15 +418,17 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
assert member.id == linked_member.id
|
||||
end
|
||||
|
||||
test "own_data user can read linked member (via special case bypass)" do
|
||||
test "own_data user can read linked member (via special case bypass)", %{actor: actor} do
|
||||
# own_data has Member.read scope :linked, but the special case ensures
|
||||
# users can ALWAYS read their linked member regardless of permission set.
|
||||
user = create_user_with_permission_set("own_data")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
user = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(user, actor)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)
|
||||
|
||||
# Should succeed (special case bypass policy for :read takes precedence)
|
||||
{:ok, member} =
|
||||
|
|
@ -408,15 +437,19 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
|||
assert member.id == linked_member.id
|
||||
end
|
||||
|
||||
test "own_data user can update linked member (via HasPermission :linked scope)" do
|
||||
test "own_data user can update linked member (via HasPermission :linked scope)", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Update is NOT handled by special case - it's handled by HasPermission
|
||||
# with :linked scope. own_data has Member.update scope :linked.
|
||||
user = create_user_with_permission_set("own_data")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
user = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(user, actor)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
{:ok, user} =
|
||||
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)
|
||||
|
||||
# Should succeed via HasPermission check (not special case)
|
||||
{:ok, updated_member} =
|
||||
|
|
|
|||
|
|
@ -19,8 +19,13 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest 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"),
|
||||
|
|
@ -31,12 +36,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a member. Note: If membership_fee_type_id is provided,
|
||||
# cycles will be auto-generated during creation in test environment.
|
||||
defp create_member(attrs) do
|
||||
defp create_member(attrs, actor) do
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
|
|
@ -47,7 +52,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a member and explicitly generate cycles with a fixed "today" date.
|
||||
|
|
@ -56,7 +61,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
# Note: We first create the member without fee_type_id, then assign it via update,
|
||||
# which triggers the after_action hook. However, we then explicitly regenerate
|
||||
# cycles with the fixed "today" date to ensure consistency.
|
||||
defp create_member_with_cycles(attrs, today) do
|
||||
defp create_member_with_cycles(attrs, today, actor) do
|
||||
# Extract membership_fee_type_id if present
|
||||
fee_type_id = Map.get(attrs, :membership_fee_type_id)
|
||||
|
||||
|
|
@ -64,14 +69,14 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
attrs_without_fee_type = Map.delete(attrs, :membership_fee_type_id)
|
||||
|
||||
member =
|
||||
create_member(attrs_without_fee_type)
|
||||
create_member(attrs_without_fee_type, actor)
|
||||
|
||||
# Assign fee type if provided (this will trigger auto-generation with real today)
|
||||
member =
|
||||
if fee_type_id do
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type_id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
else
|
||||
member
|
||||
end
|
||||
|
|
@ -80,8 +85,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
# This ensures the test uses the fixed date, not the real current date
|
||||
if fee_type_id && member.join_date do
|
||||
# Delete any existing cycles first to ensure clean state
|
||||
existing_cycles = get_member_cycles(member.id)
|
||||
Enum.each(existing_cycles, &Ash.destroy!(&1))
|
||||
existing_cycles = get_member_cycles(member.id, actor)
|
||||
Enum.each(existing_cycles, &Ash.destroy!(&1, actor: actor))
|
||||
|
||||
# Generate cycles with fixed "today" date
|
||||
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||
|
|
@ -91,85 +96,91 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
# Helper to get cycles for a member
|
||||
defp get_member_cycles(member_id) do
|
||||
defp get_member_cycles(member_id, actor) do
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member_id)
|
||||
|> Ash.Query.sort(cycle_start: :asc)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to set up settings
|
||||
defp setup_settings(include_joining_cycle) do
|
||||
defp setup_settings(include_joining_cycle, actor) do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
settings
|
||||
|> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
end
|
||||
|
||||
describe "member joins today" do
|
||||
test "current cycle is generated (yearly)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "current cycle is generated (yearly)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
# Create member WITHOUT fee type first to avoid auto-generation with real today
|
||||
member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
join_date: today,
|
||||
membership_fee_start_date: ~D[2024-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Explicitly generate cycles with fixed "today" date
|
||||
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have the current year's cycle
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year)
|
||||
assert 2024 in cycle_years
|
||||
end
|
||||
|
||||
test "current cycle is generated (monthly)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
test "current cycle is generated (monthly)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
# Create member WITHOUT fee type first to avoid auto-generation with real today
|
||||
member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
join_date: today,
|
||||
membership_fee_start_date: ~D[2024-06-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Explicitly generate cycles with fixed "today" date
|
||||
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have June 2024 cycle
|
||||
assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-06-01] end)
|
||||
end
|
||||
|
||||
test "current cycle is generated (quarterly)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :quarterly})
|
||||
test "current cycle is generated (quarterly)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :quarterly}, actor)
|
||||
|
||||
today = ~D[2024-05-15]
|
||||
|
||||
|
|
@ -181,11 +192,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-04-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have Q2 2024 cycle
|
||||
assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-04-01] end)
|
||||
|
|
@ -193,9 +205,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
describe "member left yesterday" do
|
||||
test "no future cycles are generated" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "no future cycles are generated", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
yesterday = Date.add(today, -1)
|
||||
|
|
@ -209,11 +221,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||
|
||||
# 2024 should be included because the member was still active during that cycle
|
||||
|
|
@ -225,21 +238,24 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
refute 2025 in cycle_years
|
||||
end
|
||||
|
||||
test "exit during first month of year stops at that year (monthly)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
test "exit during first month of year stops at that year (monthly)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
|
||||
# Create member - cycles will be auto-generated
|
||||
member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
join_date: ~D[2024-01-15],
|
||||
exit_date: ~D[2024-03-15],
|
||||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_months = Enum.map(cycles, & &1.cycle_start.month) |> Enum.sort()
|
||||
|
||||
assert 1 in cycle_months
|
||||
|
|
@ -253,18 +269,21 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
describe "member has no cycles initially" do
|
||||
test "returns error when fee type is not assigned" do
|
||||
setup_settings(true)
|
||||
test "returns error when fee type is not assigned", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
|
||||
# Create member WITHOUT fee type (no auto-generation)
|
||||
member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
join_date: ~D[2022-03-15],
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Verify no cycles exist initially
|
||||
initial_cycles = get_member_cycles(member.id)
|
||||
initial_cycles = get_member_cycles(member.id, actor)
|
||||
assert initial_cycles == []
|
||||
|
||||
# Trying to generate cycles without fee type should return error
|
||||
|
|
@ -272,9 +291,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
assert result == {:error, :no_membership_fee_type}
|
||||
end
|
||||
|
||||
test "generates all cycles when member is created with fee type" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "generates all cycles when member is created with fee type", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
|
|
@ -286,11 +305,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have generated all cycles from 2022 to 2024 (3 cycles)
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||
|
|
@ -303,16 +323,19 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
describe "member has existing cycles" do
|
||||
test "generates from last cycle (not duplicating existing)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "generates from last cycle (not duplicating existing)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create member WITHOUT fee type first
|
||||
member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
join_date: ~D[2022-03-15],
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Manually create an existing cycle for 2022
|
||||
MembershipFeeCycle
|
||||
|
|
@ -323,20 +346,20 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
amount: fee_type.amount,
|
||||
status: :paid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Now assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Explicitly generate cycles with fixed "today" date
|
||||
today = ~D[2024-06-15]
|
||||
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||
|
||||
# Check all cycles
|
||||
all_cycles = get_member_cycles(member.id)
|
||||
all_cycles = get_member_cycles(member.id, actor)
|
||||
all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()
|
||||
|
||||
# Should have 2022 (manually created), 2023 and 2024 (auto-generated)
|
||||
|
|
@ -350,9 +373,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
describe "year boundary handling" do
|
||||
test "cycles span across year boundaries correctly (yearly)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "cycles span across year boundaries correctly (yearly)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
|
|
@ -364,11 +387,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2023-01-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||
|
||||
# Should have 2023 and 2024
|
||||
|
|
@ -376,9 +400,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
assert 2024 in cycle_years
|
||||
end
|
||||
|
||||
test "cycles span across year boundaries correctly (quarterly)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :quarterly})
|
||||
test "cycles span across year boundaries correctly (quarterly)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :quarterly}, actor)
|
||||
|
||||
today = ~D[2024-12-15]
|
||||
|
||||
|
|
@ -390,20 +414,21 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-10-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date)
|
||||
|
||||
# Should have Q4 2024
|
||||
assert ~D[2024-10-01] in cycle_starts
|
||||
end
|
||||
|
||||
test "December to January transition (monthly)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
test "December to January transition (monthly)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
|
||||
today = ~D[2024-12-31]
|
||||
|
||||
|
|
@ -415,11 +440,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-12-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date)
|
||||
|
||||
# Should have Dec 2024
|
||||
|
|
@ -428,9 +454,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
describe "leap year handling" do
|
||||
test "February cycles in leap year" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
test "February cycles in leap year", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
|
||||
today = ~D[2024-03-15]
|
||||
|
||||
|
|
@ -443,11 +469,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-02-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have February 2024 cycle
|
||||
feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-02-01] end)
|
||||
|
|
@ -455,9 +482,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
assert feb_cycle != nil
|
||||
end
|
||||
|
||||
test "February cycles in non-leap year" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :monthly})
|
||||
test "February cycles in non-leap year", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
||||
|
||||
today = ~D[2023-03-15]
|
||||
|
||||
|
|
@ -470,11 +497,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2023-02-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have February 2023 cycle
|
||||
feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2023-02-01] end)
|
||||
|
|
@ -482,9 +510,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
assert feb_cycle != nil
|
||||
end
|
||||
|
||||
test "yearly cycle in leap year" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "yearly cycle in leap year", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-12-31]
|
||||
|
||||
|
|
@ -496,11 +524,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-01-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# Should have 2024 cycle
|
||||
cycle_2024 = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-01-01] end)
|
||||
|
|
@ -510,9 +539,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
describe "include_joining_cycle variations" do
|
||||
test "include_joining_cycle = true starts from joining cycle" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "include_joining_cycle = true starts from joining cycle", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
|
|
@ -525,20 +554,21 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id
|
||||
# membership_fee_start_date will be auto-calculated
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||
|
||||
# Should include 2023 (joining year)
|
||||
assert 2023 in cycle_years
|
||||
end
|
||||
|
||||
test "include_joining_cycle = false starts from next cycle" do
|
||||
setup_settings(false)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "include_joining_cycle = false starts from next cycle", %{actor: actor} do
|
||||
setup_settings(false, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
|
|
@ -551,11 +581,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id
|
||||
# membership_fee_start_date will be auto-calculated
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check all cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||
|
||||
# Should NOT include 2023 (joining year)
|
||||
|
|
@ -567,17 +598,22 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
end
|
||||
|
||||
describe "inactive member processing" do
|
||||
test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members", %{
|
||||
actor: actor
|
||||
} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create an inactive member (left in 2023) WITHOUT fee type initially
|
||||
# This simulates a member that was created before the fee system existed
|
||||
member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
join_date: ~D[2021-03-15],
|
||||
exit_date: ~D[2023-06-15]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Now assign fee type (simulating a retroactive assignment)
|
||||
member =
|
||||
|
|
@ -586,7 +622,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2021-01-01]
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Run batch generation with a "today" date after the member left
|
||||
today = ~D[2024-06-15]
|
||||
|
|
@ -596,7 +632,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
assert results.total >= 1
|
||||
|
||||
# Check the member's cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()
|
||||
|
||||
# Should have 2021, 2022, 2023 (exit year included)
|
||||
|
|
@ -608,9 +644,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
refute 2024 in cycle_years
|
||||
end
|
||||
|
||||
test "exit_date on cycle_start still generates that cycle" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "exit_date on cycle_start still generates that cycle", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
today = ~D[2024-12-31]
|
||||
|
||||
|
|
@ -624,11 +660,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
},
|
||||
today
|
||||
today,
|
||||
actor
|
||||
)
|
||||
|
||||
# Check cycles
|
||||
cycles = get_member_cycles(member.id)
|
||||
cycles = get_member_cycles(member.id, actor)
|
||||
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||
|
||||
# 2024 should be included because exit_date == cycle_start means
|
||||
|
|
|
|||
|
|
@ -11,8 +11,13 @@ defmodule Mv.MembershipFees.CycleGeneratorTest 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.MembershipFees.CycleGeneratorTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to create a member without triggering cycle generation
|
||||
defp create_member_without_cycles(attrs) do
|
||||
defp create_member_without_cycles(attrs, actor) do
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
|
|
@ -38,50 +43,53 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to set up settings with specific include_joining_cycle value
|
||||
defp setup_settings(include_joining_cycle) do
|
||||
defp setup_settings(include_joining_cycle, actor) do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
settings
|
||||
|> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
end
|
||||
|
||||
# Helper to get cycles for a member
|
||||
defp get_member_cycles(member_id) do
|
||||
defp get_member_cycles(member_id, actor) do
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member_id)
|
||||
|> Ash.Query.sort(cycle_start: :asc)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
end
|
||||
|
||||
describe "generate_cycles_for_member/2" do
|
||||
test "generates cycles from start date to today" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "generates cycles from start date to today", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create member WITHOUT fee type first to avoid auto-generation
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2022-03-15],
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Explicitly generate cycles with fixed "today" date to avoid date dependency
|
||||
today = ~D[2024-06-15]
|
||||
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||
|
||||
# Verify cycles were generated
|
||||
all_cycles = get_member_cycles(member.id)
|
||||
all_cycles = get_member_cycles(member.id, actor)
|
||||
cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()
|
||||
|
||||
# With include_joining_cycle=true and join_date=2022-03-15,
|
||||
|
|
@ -92,16 +100,19 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
assert 2024 in cycle_years
|
||||
end
|
||||
|
||||
test "generates cycles from last existing cycle" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "generates cycles from last existing cycle", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create member without fee type first to avoid auto-generation
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2022-03-15],
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Manually create a cycle for 2022
|
||||
MembershipFeeCycle
|
||||
|
|
@ -112,13 +123,13 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
amount: fee_type.amount,
|
||||
status: :paid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Now assign fee type to member
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Generate cycles with specific "today" date
|
||||
today = ~D[2024-06-15]
|
||||
|
|
@ -130,17 +141,20 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
assert 2022 not in new_cycle_years
|
||||
end
|
||||
|
||||
test "respects left_at boundary (stops generation)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "respects left_at boundary (stops generation)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2022-03-15],
|
||||
exit_date: ~D[2023-06-15],
|
||||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Generate cycles with specific "today" date far in the future
|
||||
today = ~D[2025-06-15]
|
||||
|
|
@ -154,16 +168,19 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
assert 2025 not in cycle_years
|
||||
end
|
||||
|
||||
test "skips existing cycles (idempotent)" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "skips existing cycles (idempotent)", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2023-03-15],
|
||||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2023-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
|
|
@ -177,37 +194,43 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
assert second_cycles == []
|
||||
end
|
||||
|
||||
test "does not fill gaps when cycles were deleted" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "does not fill gaps when cycles were deleted", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create member without fee type first to control which cycles exist
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2020-03-15],
|
||||
membership_fee_start_date: ~D[2020-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Manually create cycles for 2020, 2021, 2022, 2023
|
||||
for year <- [2020, 2021, 2022, 2023] do
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|> Ash.Changeset.for_create(
|
||||
:create,
|
||||
%{
|
||||
cycle_start: Date.new!(year, 1, 1),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
amount: fee_type.amount,
|
||||
status: :unpaid
|
||||
})
|
||||
|> Ash.create!()
|
||||
}
|
||||
)
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
# Delete the 2021 cycle (create a gap)
|
||||
cycle_2021 =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^~D[2021-01-01])
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|
||||
Ash.destroy!(cycle_2021)
|
||||
Ash.destroy!(cycle_2021, actor: actor)
|
||||
|
||||
# Now assign fee type to member (this triggers generation)
|
||||
# Since cycles already exist (2020, 2022, 2023), the generator will
|
||||
|
|
@ -215,10 +238,10 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Verify gap was NOT filled and new cycles were generated from last existing
|
||||
all_cycles = get_member_cycles(member.id)
|
||||
all_cycles = get_member_cycles(member.id, actor)
|
||||
all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||
|
||||
# 2021 should NOT exist (gap was not filled)
|
||||
|
|
@ -234,20 +257,23 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
assert 2025 in all_cycle_years
|
||||
end
|
||||
|
||||
test "sets correct amount from membership fee type" do
|
||||
setup_settings(true)
|
||||
test "sets correct amount from membership fee type", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
amount = Decimal.new("75.50")
|
||||
fee_type = create_fee_type(%{interval: :yearly, amount: amount})
|
||||
fee_type = create_fee_type(%{interval: :yearly, amount: amount}, actor)
|
||||
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2024-03-15],
|
||||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Verify cycles were generated with correct amount
|
||||
all_cycles = get_member_cycles(member.id)
|
||||
all_cycles = get_member_cycles(member.id, actor)
|
||||
refute Enum.empty?(all_cycles), "Expected cycles to be generated"
|
||||
|
||||
# All cycles should have the correct amount
|
||||
|
|
@ -256,21 +282,24 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "handles NULL membership_fee_start_date by calculating from join_date" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :quarterly})
|
||||
test "handles NULL membership_fee_start_date by calculating from join_date", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :quarterly}, actor)
|
||||
|
||||
# Create member without membership_fee_start_date - it will be auto-calculated
|
||||
# and cycles will be auto-generated
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2024-02-15],
|
||||
membership_fee_type_id: fee_type.id
|
||||
# No membership_fee_start_date - should be calculated
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
# Verify cycles were auto-generated
|
||||
all_cycles = get_member_cycles(member.id)
|
||||
all_cycles = get_member_cycles(member.id, actor)
|
||||
|
||||
# With include_joining_cycle=true and join_date=2024-02-15 (quarterly),
|
||||
# start_date should be 2024-01-01 (Q1 start)
|
||||
|
|
@ -284,28 +313,34 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
assert first_cycle_start == ~D[2024-01-01]
|
||||
end
|
||||
|
||||
test "returns error when member has no membership_fee_type" do
|
||||
test "returns error when member has no membership_fee_type", %{actor: actor} do
|
||||
# Create member without fee type - no auto-generation will occur
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2024-03-15]
|
||||
# No membership_fee_type_id
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
{:error, reason} = CycleGenerator.generate_cycles_for_member(member.id)
|
||||
assert reason == :no_membership_fee_type
|
||||
end
|
||||
|
||||
test "returns error when member has no join_date" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "returns error when member has no join_date", %{actor: actor} do
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create member without join_date - no auto-generation will occur
|
||||
# (after_action hook checks for join_date)
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
membership_fee_type_id: fee_type.id
|
||||
# No join_date
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
{:error, reason} = CycleGenerator.generate_cycles_for_member(member.id)
|
||||
assert reason == :no_join_date
|
||||
|
|
@ -357,24 +392,30 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
end
|
||||
|
||||
describe "generate_cycles_for_all_members/1" do
|
||||
test "generates cycles for multiple members" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "generates cycles for multiple members", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
# Create multiple members
|
||||
_member1 =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2024-01-15],
|
||||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
_member2 =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2024-02-15],
|
||||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2024-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
{:ok, results} = CycleGenerator.generate_cycles_for_all_members(today: today)
|
||||
|
|
@ -387,16 +428,19 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
|||
end
|
||||
|
||||
describe "lock mechanism" do
|
||||
test "prevents concurrent generation for same member" do
|
||||
setup_settings(true)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
test "prevents concurrent generation for same member", %{actor: actor} do
|
||||
setup_settings(true, actor)
|
||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||
|
||||
member =
|
||||
create_member_without_cycles(%{
|
||||
create_member_without_cycles(
|
||||
%{
|
||||
join_date: ~D[2022-03-15],
|
||||
membership_fee_type_id: fee_type.id,
|
||||
membership_fee_start_date: ~D[2022-01-01]
|
||||
})
|
||||
},
|
||||
actor
|
||||
)
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "E2E: New OIDC user registration" do
|
||||
test "new user can register via OIDC", %{conn: _conn} do
|
||||
test "new user can register via OIDC", %{conn: _conn, actor: actor} do
|
||||
# Simulate OIDC callback for brand new user
|
||||
user_info = %{
|
||||
"sub" => "new_oidc_user_123",
|
||||
|
|
@ -18,10 +23,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Call register action
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:ok, new_user} = result
|
||||
assert to_string(new_user.email) == "newuser@example.com"
|
||||
|
|
@ -30,17 +38,20 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Verify user can be found by oidc_id
|
||||
{:ok, [found_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert found_user.id == new_user.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "E2E: Existing OIDC user sign-in" do
|
||||
test "existing OIDC user can sign in and email updates", %{conn: _conn} do
|
||||
test "existing OIDC user can sign in and email updates", %{conn: _conn, actor: actor} do
|
||||
# Create OIDC user
|
||||
user =
|
||||
create_test_user(%{
|
||||
|
|
@ -56,10 +67,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Register (upsert) with new email
|
||||
{:ok, updated_user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: updated_user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Same user, updated email
|
||||
assert updated_user.id == user.id
|
||||
|
|
@ -70,7 +84,7 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
describe "E2E: OIDC with existing password account (Email Collision)" do
|
||||
test "OIDC registration with password account email triggers PasswordVerificationRequired",
|
||||
%{conn: _conn} do
|
||||
%{conn: _conn, actor: actor} do
|
||||
# Step 1: Create a password-only user
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -86,10 +100,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Step 3: Should fail with PasswordVerificationRequired
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -106,7 +123,7 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end
|
||||
|
||||
test "full E2E flow: OIDC collision -> password verification -> account linked",
|
||||
%{conn: _conn} do
|
||||
%{conn: _conn, actor: actor} do
|
||||
# Step 1: Create password user
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -122,10 +139,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Extract the error
|
||||
password_error =
|
||||
|
|
@ -142,12 +162,12 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
{:ok, linked_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^password_user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Verify account is now linked
|
||||
assert linked_user.id == password_user.id
|
||||
|
|
@ -158,17 +178,20 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Step 5: User can now sign in via OIDC
|
||||
{:ok, [signed_in_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert signed_in_user.id == password_user.id
|
||||
assert signed_in_user.oidc_id == "oidc_link_888"
|
||||
end
|
||||
|
||||
test "E2E: OIDC collision with different email at provider updates email after linking",
|
||||
%{conn: _conn} do
|
||||
%{conn: _conn, actor: actor} do
|
||||
# Password user with old email
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -199,12 +222,12 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
{:ok, linked_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^password_user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: updated_user_info["sub"],
|
||||
oidc_user_info: updated_user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Email should be updated to match OIDC provider
|
||||
assert to_string(linked_user.email) == "new@example.com"
|
||||
|
|
@ -213,7 +236,10 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end
|
||||
|
||||
describe "E2E: OIDC with linked member" do
|
||||
test "E2E: email sync to member when linking OIDC to password account", %{conn: _conn} do
|
||||
test "E2E: email sync to member when linking OIDC to password account", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# Create member
|
||||
member =
|
||||
Ash.Seed.seed!(Mv.Membership.Member, %{
|
||||
|
|
@ -239,10 +265,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Collision detected
|
||||
{:error, %Ash.Error.Invalid{}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# After password verification, link OIDC with NEW email
|
||||
updated_user_info = %{
|
||||
|
|
@ -253,24 +282,27 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
{:ok, linked_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^password_user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: updated_user_info["sub"],
|
||||
oidc_user_info: updated_user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# User email updated
|
||||
assert to_string(linked_user.email) == "newmember@example.com"
|
||||
|
||||
# Member email should be synced
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert to_string(updated_member.email) == "newmember@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "E2E: Security scenarios" do
|
||||
test "E2E: password-only user cannot be accessed via OIDC without password", %{conn: _conn} do
|
||||
test "E2E: password-only user cannot be accessed via OIDC without password", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# Create password user
|
||||
_password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -287,10 +319,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Sign-in should fail (no matching oidc_id)
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
case result do
|
||||
{:ok, []} ->
|
||||
|
|
@ -305,17 +340,23 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Registration should trigger password requirement
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert Enum.any?(errors, fn err ->
|
||||
match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
|
||||
end)
|
||||
end
|
||||
|
||||
test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{conn: _conn} do
|
||||
test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# User linked to OIDC provider A
|
||||
_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -331,10 +372,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Should trigger hard error (not PasswordVerificationRequired)
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should have hard error about "already linked to a different OIDC account"
|
||||
assert Enum.any?(errors, fn
|
||||
|
|
@ -351,7 +395,10 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "E2E: empty string oidc_id is treated as password-only account", %{conn: _conn} do
|
||||
test "E2E: empty string oidc_id is treated as password-only account", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# User with empty oidc_id
|
||||
_password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -367,10 +414,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should require password (empty string = no OIDC)
|
||||
assert Enum.any?(errors, fn err ->
|
||||
|
|
@ -380,32 +430,38 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end
|
||||
|
||||
describe "E2E: Error scenarios" do
|
||||
test "E2E: OIDC registration without oidc_id fails", %{conn: _conn} do
|
||||
test "E2E: OIDC registration without oidc_id fails", %{conn: _conn, actor: actor} do
|
||||
user_info = %{
|
||||
"preferred_username" => "noid@example.com"
|
||||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert Enum.any?(errors, fn err ->
|
||||
match?(%Ash.Error.Changes.InvalidChanges{}, err)
|
||||
end)
|
||||
end
|
||||
|
||||
test "E2E: OIDC registration without email fails", %{conn: _conn} do
|
||||
test "E2E: OIDC registration without email fails", %{conn: _conn, actor: actor} do
|
||||
user_info = %{
|
||||
"sub" => "noemail_123"
|
||||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert Enum.any?(errors, fn err ->
|
||||
match?(%Ash.Error.Changes.Required{field: :email}, err)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "OIDC user updates email to available email" do
|
||||
test "should succeed and update email" do
|
||||
test "should succeed and update email", %{actor: actor} do
|
||||
# Create OIDC user
|
||||
{:ok, oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -14,7 +19,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "original@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_123")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# User logs in via OIDC with NEW email
|
||||
user_info = %{
|
||||
|
|
@ -23,10 +28,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should succeed and email should be updated
|
||||
assert {:ok, updated_user} = result
|
||||
|
|
@ -37,7 +45,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "OIDC user updates email to email of passwordless user" do
|
||||
test "should fail with clear error message" do
|
||||
test "should fail with clear error message", %{actor: actor} do
|
||||
# Create OIDC user
|
||||
{:ok, _oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -45,7 +53,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_456")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create passwordless user with target email
|
||||
{:ok, _passwordless_user} =
|
||||
|
|
@ -53,7 +61,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "taken@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# OIDC user tries to update email to taken email
|
||||
user_info = %{
|
||||
|
|
@ -62,10 +70,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with email update conflict error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -88,7 +99,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "OIDC user updates email to email of password-protected user" do
|
||||
test "should fail with clear error message" do
|
||||
test "should fail with clear error message", %{actor: actor} do
|
||||
# Create OIDC user
|
||||
{:ok, _oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -96,7 +107,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser2@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_789")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create password user with target email (explicitly NO oidc_id)
|
||||
password_user =
|
||||
|
|
@ -106,14 +117,14 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
})
|
||||
|
||||
# Ensure it's a password-only user
|
||||
{:ok, password_user} = Ash.reload(password_user)
|
||||
{:ok, password_user} = Ash.reload(password_user, actor: actor)
|
||||
assert not is_nil(password_user.hashed_password)
|
||||
# Force oidc_id to be nil to avoid any confusion
|
||||
{:ok, password_user} =
|
||||
password_user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, nil)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert is_nil(password_user.oidc_id)
|
||||
|
||||
|
|
@ -124,10 +135,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with email update conflict error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -150,7 +164,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "OIDC user updates email to email of different OIDC user" do
|
||||
test "should fail with clear error message about different OIDC account" do
|
||||
test "should fail with clear error message about different OIDC account", %{actor: actor} do
|
||||
# Create first OIDC user
|
||||
{:ok, _oidc_user1} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -158,7 +172,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser1@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_aaa")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create second OIDC user with target email
|
||||
{:ok, _oidc_user2} =
|
||||
|
|
@ -167,7 +181,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser2@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_bbb")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# First OIDC user tries to update email to second user's email
|
||||
user_info = %{
|
||||
|
|
@ -176,10 +190,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with "already linked to different OIDC account" error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -201,14 +218,14 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "New OIDC user registration scenarios (for comparison)" do
|
||||
test "new OIDC user with email of passwordless user triggers linking flow" do
|
||||
test "new OIDC user with email of passwordless user triggers linking flow", %{actor: actor} do
|
||||
# Create passwordless user
|
||||
{:ok, passwordless_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "passwordless@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# New OIDC user tries to register
|
||||
user_info = %{
|
||||
|
|
@ -217,10 +234,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should trigger PasswordVerificationRequired (linking flow)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -234,7 +254,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "new OIDC user with email of existing OIDC user shows hard error" do
|
||||
test "new OIDC user with email of existing OIDC user shows hard error", %{actor: actor} do
|
||||
# Create existing OIDC user
|
||||
{:ok, _existing_oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -242,7 +262,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "existing@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_existing")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# New OIDC user tries to register with same email
|
||||
user_info = %{
|
||||
|
|
@ -251,10 +271,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with "already linked to different OIDC account" error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
# Test OIDC callback scenarios by directly calling the actions
|
||||
# This simulates what happens during real OIDC authentication
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "OIDC sign-in scenarios" do
|
||||
test "existing OIDC user with unchanged email can sign in" do
|
||||
# Create user with OIDC ID
|
||||
|
|
@ -20,11 +25,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
# Test sign_in_with_rauthy action directly
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, [found_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert found_user.id == user.id
|
||||
assert to_string(found_user.email) == "existing@example.com"
|
||||
|
|
@ -39,10 +49,15 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
# Test register_with_rauthy action
|
||||
case Mv.Accounts.create_register_with_rauthy(%{
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
case Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
}) do
|
||||
},
|
||||
actor: system_actor
|
||||
) do
|
||||
{:ok, new_user} ->
|
||||
assert to_string(new_user.email) == "newuser@example.com"
|
||||
assert new_user.oidc_id == "brand_new_oidc_456"
|
||||
|
|
@ -73,11 +88,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
# Should NOT find any user (security requirement)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
@ -107,11 +127,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "oidc.user@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, [found_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: correct_user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert found_user.id == user.id
|
||||
|
||||
|
|
@ -122,10 +147,13 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: wrong_user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
@ -154,11 +182,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "empty.oidc@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
@ -189,11 +222,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "conflict@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Should fail with hard error (not PasswordVerificationRequired)
|
||||
# This prevents someone with OIDC provider B from taking over an account
|
||||
|
|
@ -220,11 +258,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "nosub@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert {:error,
|
||||
%Ash.Error.Invalid{
|
||||
|
|
@ -239,11 +282,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"sub" => "noemail_oidc_123"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
||||
|
|
@ -264,11 +312,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "new@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert user.id == existing_user.id
|
||||
assert to_string(user.email) == "new@example.com"
|
||||
|
|
@ -281,11 +334,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "altid@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert user.oidc_id == "alt_oidc_id_123"
|
||||
assert to_string(user.email) == "altid@example.com"
|
||||
|
|
|
|||
|
|
@ -8,9 +8,15 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "OIDC login with existing email (no oidc_id) - Email Collision" do
|
||||
@tag :test_proposal
|
||||
test "OIDC register with existing password user email fails with PasswordVerificationRequired" do
|
||||
test "OIDC register with existing password user email fails with PasswordVerificationRequired",
|
||||
%{actor: actor} do
|
||||
# Create password-only user
|
||||
existing_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -26,10 +32,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with PasswordVerificationRequired error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -47,7 +56,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "PasswordVerificationRequired error contains necessary context" do
|
||||
test "PasswordVerificationRequired error contains necessary context", %{actor: actor} do
|
||||
existing_user =
|
||||
create_test_user(%{
|
||||
email: "test@example.com",
|
||||
|
|
@ -61,10 +70,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
password_error =
|
||||
Enum.find(errors, fn err ->
|
||||
|
|
@ -78,7 +90,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "after successful password verification, oidc_id can be set" do
|
||||
test "after successful password verification, oidc_id can be set", %{actor: actor} do
|
||||
# Create password user
|
||||
user =
|
||||
create_test_user(%{
|
||||
|
|
@ -97,12 +109,12 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
{:ok, updated_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_user.id == user.id
|
||||
assert updated_user.oidc_id == "linked_oidc_555"
|
||||
|
|
@ -112,7 +124,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "password verification with wrong password keeps oidc_id as nil" do
|
||||
test "password verification with wrong password keeps oidc_id as nil", %{actor: actor} do
|
||||
# This test verifies that if password verification fails,
|
||||
# the oidc_id should NOT be set
|
||||
|
||||
|
|
@ -131,7 +143,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
# before link_oidc_id is called, so here we just verify the user state
|
||||
|
||||
# User should still have no oidc_id (no linking happened)
|
||||
{:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert is_nil(unchanged_user.oidc_id)
|
||||
assert unchanged_user.hashed_password == user.hashed_password
|
||||
end
|
||||
|
|
@ -139,7 +151,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|
||||
describe "OIDC login with email of user having different oidc_id - Account Conflict" do
|
||||
@tag :test_proposal
|
||||
test "OIDC register with email of user having different oidc_id fails" do
|
||||
test "OIDC register with email of user having different oidc_id fails", %{actor: actor} do
|
||||
# User already linked to OIDC provider A
|
||||
_existing_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -155,10 +167,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail - cannot link different OIDC account to same email
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -171,7 +186,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "existing OIDC user email remains unchanged when oidc_id matches" do
|
||||
test "existing OIDC user email remains unchanged when oidc_id matches", %{actor: actor} do
|
||||
user =
|
||||
create_test_user(%{
|
||||
email: "oidc@example.com",
|
||||
|
|
@ -186,10 +201,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|
||||
# This should work via upsert
|
||||
{:ok, updated_user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert updated_user.id == user.id
|
||||
assert updated_user.oidc_id == "oidc_stable_789"
|
||||
|
|
@ -199,7 +217,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|
||||
describe "Email update during OIDC linking" do
|
||||
@tag :test_proposal
|
||||
test "linking OIDC to password account updates email if different in OIDC" do
|
||||
test "linking OIDC to password account updates email if different in OIDC", %{actor: actor} do
|
||||
# Password user with old email
|
||||
user =
|
||||
create_test_user(%{
|
||||
|
|
@ -218,19 +236,19 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
{:ok, updated_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_user.oidc_id == "oidc_link_999"
|
||||
assert to_string(updated_user.email) == "newemail@example.com"
|
||||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "email change during linking triggers member email sync" do
|
||||
test "email change during linking triggers member email sync", %{actor: actor} do
|
||||
# Create member
|
||||
member =
|
||||
Ash.Seed.seed!(Mv.Membership.Member, %{
|
||||
|
|
@ -257,25 +275,25 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
{:ok, updated_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Verify user email changed
|
||||
assert to_string(updated_user.email) == "newemail@example.com"
|
||||
|
||||
# Verify member email was synced
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert to_string(updated_member.email) == "newemail@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Edge cases" do
|
||||
@tag :test_proposal
|
||||
test "user with empty string oidc_id is treated as password-only user" do
|
||||
test "user with empty string oidc_id is treated as password-only user", %{actor: actor} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "empty@example.com",
|
||||
|
|
@ -290,10 +308,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should trigger PasswordVerificationRequired (empty string = no OIDC)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -307,7 +328,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "cannot link same oidc_id to multiple users" do
|
||||
test "cannot link same oidc_id to multiple users", %{actor: actor} do
|
||||
# User 1 with OIDC
|
||||
_user1 =
|
||||
create_test_user(%{
|
||||
|
|
@ -323,7 +344,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
email: "user2@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "shared_oidc_333")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail due to unique constraint on oidc_id
|
||||
assert match?({:error, %Ash.Error.Invalid{}}, result)
|
||||
|
|
@ -337,14 +358,16 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
describe "OIDC login with passwordless user - Requires Linking Flow" do
|
||||
test "user without password and without oidc_id triggers PasswordVerificationRequired" do
|
||||
test "user without password and without oidc_id triggers PasswordVerificationRequired", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create user without password (e.g., invited user)
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "invited@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Verify user has no password and no oidc_id
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
|
|
@ -372,14 +395,14 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "user without password but WITH password later requires verification" do
|
||||
test "user without password but WITH password later requires verification", %{actor: actor} do
|
||||
# Create user without password first
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "added-password@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# User sets password later (using admin action)
|
||||
{:ok, user_with_password} =
|
||||
|
|
@ -387,7 +410,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|> Ash.Changeset.for_update(:admin_set_password, %{
|
||||
password: "newpassword123"
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert not is_nil(user_with_password.hashed_password)
|
||||
|
||||
|
|
@ -398,10 +421,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with PasswordVerificationRequired
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
|
@ -414,7 +440,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
describe "OIDC login with different oidc_id - Hard Error" do
|
||||
test "user with different oidc_id cannot be linked (hard error)" do
|
||||
test "user with different oidc_id cannot be linked (hard error)", %{actor: actor} do
|
||||
# Create user with existing OIDC ID
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -422,7 +448,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
email: "already-linked@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert existing_user.oidc_id == "original_oidc_999"
|
||||
|
||||
|
|
@ -433,10 +459,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with hard error (not PasswordVerificationRequired)
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
|
@ -459,7 +488,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "cannot link different oidc_id even with password verification" do
|
||||
test "cannot link different oidc_id even with password verification", %{actor: actor} do
|
||||
# Create user with password AND existing OIDC ID
|
||||
existing_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -478,10 +507,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail - cannot link different OIDC ID
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
|
|
|||
|
|
@ -7,15 +7,20 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Passwordless user - Automatic linking via special action" do
|
||||
test "passwordless user can be linked via link_passwordless_oidc action" do
|
||||
test "passwordless user can be linked via link_passwordless_oidc action", %{actor: actor} do
|
||||
# Create user without password (e.g., invited user)
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "invited@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Verify user has no password and no oidc_id
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
|
|
@ -31,7 +36,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
"preferred_username" => "invited@example.com"
|
||||
}
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# User should now have oidc_id linked
|
||||
assert linked_user.oidc_id == "auto_link_oidc_123"
|
||||
|
|
@ -47,20 +52,22 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
},
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
|> Ash.read_one()
|
||||
|> Ash.read_one(actor: actor)
|
||||
|
||||
assert {:ok, signed_in_user} = result
|
||||
assert signed_in_user.id == existing_user.id
|
||||
end
|
||||
|
||||
test "passwordless user triggers PasswordVerificationRequired for linking flow" do
|
||||
test "passwordless user triggers PasswordVerificationRequired for linking flow", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create passwordless user
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "passwordless@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
assert is_nil(existing_user.oidc_id)
|
||||
|
|
@ -95,7 +102,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
end
|
||||
|
||||
describe "User with different OIDC ID - Hard Error" do
|
||||
test "user with different oidc_id gets hard error, not password verification" do
|
||||
test "user with different oidc_id gets hard error, not password verification", %{actor: actor} do
|
||||
# Create user with existing OIDC ID
|
||||
{:ok, _existing_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -103,7 +110,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
email: "already-linked@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Try to register with same email but different OIDC ID
|
||||
user_info = %{
|
||||
|
|
@ -138,7 +145,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "passwordless user with different oidc_id also gets hard error" do
|
||||
test "passwordless user with different oidc_id also gets hard error", %{actor: actor} do
|
||||
# Create passwordless user with OIDC ID
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -146,7 +153,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
email: "passwordless-linked@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "first_oidc_777")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
assert existing_user.oidc_id == "first_oidc_777"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
alias MvWeb.Helpers.MembershipFeeHelpers
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "format_currency/1" do
|
||||
test "formats decimal amount correctly" do
|
||||
assert MembershipFeeHelpers.format_currency(Decimal.new("60.00")) == "60,00 €"
|
||||
|
|
@ -63,7 +68,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
end
|
||||
|
||||
describe "get_last_completed_cycle/2" do
|
||||
test "returns last completed cycle for member" do
|
||||
test "returns last completed cycle for member", %{actor: actor} do
|
||||
# Create test data
|
||||
fee_type =
|
||||
Mv.MembershipFees.MembershipFeeType
|
||||
|
|
@ -72,7 +77,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Create member without fee type first to avoid auto-generation
|
||||
member =
|
||||
|
|
@ -83,21 +88,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
join_date: ~D[2022-01-01]
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Assign fee type after member creation (this may generate cycles, but we'll create our own)
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Delete any auto-generated cycles first
|
||||
cycles =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
|
||||
|
||||
# Create cycles manually
|
||||
_cycle_2022 =
|
||||
|
|
@ -109,7 +114,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
status: :paid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
cycle_2023 =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|
|
@ -120,7 +125,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
status: :paid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Load cycles with membership_fee_type relationship
|
||||
member =
|
||||
|
|
@ -135,7 +140,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
assert last_cycle.id == cycle_2023.id
|
||||
end
|
||||
|
||||
test "returns nil if no cycles exist" do
|
||||
test "returns nil if no cycles exist", %{actor: actor} do
|
||||
fee_type =
|
||||
Mv.MembershipFees.MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -143,7 +148,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Create member without fee type first
|
||||
member =
|
||||
|
|
@ -153,21 +158,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
last_name: "Member",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Delete any auto-generated cycles
|
||||
cycles =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
|
||||
|
||||
# Load cycles and fee type (will be empty)
|
||||
member =
|
||||
|
|
@ -181,7 +186,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
end
|
||||
|
||||
describe "get_current_cycle/2" do
|
||||
test "returns current cycle for member" do
|
||||
test "returns current cycle for member", %{actor: actor} do
|
||||
fee_type =
|
||||
Mv.MembershipFees.MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -189,7 +194,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Create member without fee type first
|
||||
member =
|
||||
|
|
@ -200,21 +205,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
join_date: ~D[2023-01-01]
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: actor)
|
||||
|
||||
# Delete any auto-generated cycles
|
||||
cycles =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
|
||||
|
||||
today = Date.utc_today()
|
||||
current_year_start = %{today | month: 1, day: 1}
|
||||
|
|
@ -228,7 +233,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
# Load cycles with membership_fee_type relationship
|
||||
member =
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create admin user for testing
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -26,7 +28,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = log_in_user(build_conn(), user)
|
||||
%{conn: conn, user: user}
|
||||
|
|
@ -156,14 +158,16 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
# Should show success message
|
||||
assert render(view) =~ "Data field deleted successfully"
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Custom field should be gone from database
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: system_actor)
|
||||
|
||||
# Custom field value should also be gone (CASCADE)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
|
||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id, actor: system_actor)
|
||||
|
||||
# Member should still exist
|
||||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
assert {:ok, _} = Ash.get(Member, member.id, actor: system_actor)
|
||||
end
|
||||
|
||||
test "button remains disabled and custom field not deleted when slug doesn't match", %{
|
||||
|
|
@ -188,7 +192,8 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
assert html =~ ~r/disabled(?:=""|(?!\w))/
|
||||
|
||||
# Custom field should still exist since deletion couldn't proceed
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id, actor: system_actor)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -214,38 +219,45 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
refute has_element?(view, "#delete-custom-field-modal")
|
||||
|
||||
# Custom field should still exist
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id, actor: system_actor)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
defp create_member do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
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: system_actor)
|
||||
end
|
||||
|
||||
defp create_custom_field(name, value_type) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "#{name}_#{System.unique_integer([:positive])}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
end
|
||||
|
||||
defp create_custom_field_value(member, custom_field, value) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => value}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
end
|
||||
|
||||
defp log_in_user(conn, user) do
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
|||
require Ash.Query
|
||||
|
||||
setup %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create admin user
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -19,7 +21,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
|||
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
authenticated_conn = conn_with_password_user(conn, user)
|
||||
%{conn: authenticated_conn, user: user}
|
||||
|
|
@ -27,6 +29,8 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
|||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -37,11 +41,13 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -52,7 +58,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
describe "create form" do
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ defmodule MvWeb.ProfileNavigationTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "profile navigation" do
|
||||
test "clicking profile button redirects to current user profile", %{conn: conn} do
|
||||
# Setup: Create and login a user
|
||||
|
|
@ -60,7 +65,7 @@ defmodule MvWeb.ProfileNavigationTest do
|
|||
end
|
||||
|
||||
describe "profile navigation with OIDC user" do
|
||||
test "shows correct profile data for OIDC user", %{conn: conn} do
|
||||
test "shows correct profile data for OIDC user", %{conn: conn, actor: actor} do
|
||||
# Setup: Create OIDC user with sub claim
|
||||
user_info = %{
|
||||
"sub" => "oidc_123",
|
||||
|
|
@ -78,7 +83,7 @@ defmodule MvWeb.ProfileNavigationTest do
|
|||
user_info: user_info,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create!(domain: Mv.Accounts)
|
||||
|> Ash.create!(domain: Mv.Accounts, actor: actor)
|
||||
|
||||
# Login user via OIDC
|
||||
conn = sign_in_user_via_oidc(conn, user)
|
||||
|
|
@ -94,7 +99,10 @@ defmodule MvWeb.ProfileNavigationTest do
|
|||
assert html =~ "Not enabled"
|
||||
end
|
||||
|
||||
test "profile navigation works across different authentication methods", %{conn: conn} do
|
||||
test "profile navigation works across different authentication methods", %{
|
||||
conn: conn,
|
||||
actor: actor
|
||||
} do
|
||||
# Create password user
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -119,7 +127,7 @@ defmodule MvWeb.ProfileNavigationTest do
|
|||
user_info: user_info,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create!(domain: Mv.Accounts)
|
||||
|> Ash.create!(domain: Mv.Accounts, actor: actor)
|
||||
|
||||
# Test with password user
|
||||
conn_password = conn_with_password_user(conn, password_user)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
end
|
||||
|
||||
# Helper to create admin user with admin role
|
||||
defp create_admin_user(conn) do
|
||||
defp create_admin_user(conn, actor) do
|
||||
# Create admin role
|
||||
admin_role =
|
||||
case Authorization.list_roles() do
|
||||
|
|
@ -69,14 +69,14 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Assign admin role using manage_relationship
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Load role for authorization checks (must be loaded for can?/3 to work)
|
||||
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts)
|
||||
|
|
@ -88,8 +88,9 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
|
||||
describe "mount and display" do
|
||||
setup %{conn: conn} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor}
|
||||
end
|
||||
|
||||
test "mounts successfully with valid role ID", %{conn: conn} do
|
||||
|
|
@ -135,7 +136,7 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
assert html =~ gettext("Permission Set")
|
||||
end
|
||||
|
||||
test "displays system role badge when is_system_role is true", %{conn: conn} do
|
||||
test "displays system role badge when is_system_role is true", %{conn: conn, actor: actor} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
|
|
@ -143,7 +144,7 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
|
||||
|
||||
|
|
@ -172,8 +173,9 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
|
||||
describe "navigation" do
|
||||
setup %{conn: conn} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor}
|
||||
end
|
||||
|
||||
test "back button navigates to role list", %{conn: conn} do
|
||||
|
|
@ -209,8 +211,9 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
|
||||
describe "error handling" do
|
||||
setup %{conn: conn} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor}
|
||||
end
|
||||
|
||||
test "redirects to role list with error for invalid role ID", %{conn: conn} do
|
||||
|
|
@ -226,11 +229,12 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
|
||||
describe "delete functionality" do
|
||||
setup %{conn: conn} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor}
|
||||
end
|
||||
|
||||
test "delete button is not shown for system roles", %{conn: conn} do
|
||||
test "delete button is not shown for system roles", %{conn: conn, actor: actor} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
|
|
@ -238,7 +242,7 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
|
||||
|
||||
|
|
@ -258,8 +262,9 @@ defmodule MvWeb.RoleLive.ShowTest do
|
|||
|
||||
describe "page title" do
|
||||
setup %{conn: conn} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor}
|
||||
end
|
||||
|
||||
test "sets correct page title", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
end
|
||||
|
||||
# Helper to create admin user with admin role
|
||||
defp create_admin_user(conn) do
|
||||
defp create_admin_user(conn, actor) do
|
||||
# Create admin role
|
||||
admin_role =
|
||||
case Authorization.list_roles() do
|
||||
|
|
@ -60,14 +60,14 @@ defmodule MvWeb.RoleLiveTest do
|
|||
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Assign admin role using manage_relationship
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Load role for authorization checks (must be loaded for can?/3 to work)
|
||||
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts)
|
||||
|
|
@ -78,14 +78,14 @@ defmodule MvWeb.RoleLiveTest do
|
|||
end
|
||||
|
||||
# Helper to create non-admin user
|
||||
defp create_non_admin_user(conn) do
|
||||
defp create_non_admin_user(conn, actor) do
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||
email: "user#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
conn = conn_with_password_user(conn, user)
|
||||
{conn, user}
|
||||
|
|
@ -93,8 +93,9 @@ defmodule MvWeb.RoleLiveTest do
|
|||
|
||||
describe "index page" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor, user: user}
|
||||
end
|
||||
|
||||
test "mounts successfully", %{conn: conn} do
|
||||
|
|
@ -121,7 +122,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
assert html =~ role.permission_set_name
|
||||
end
|
||||
|
||||
test "shows system role badge", %{conn: conn} do
|
||||
test "shows system role badge", %{conn: conn, actor: actor} do
|
||||
_system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
|
|
@ -129,14 +130,14 @@ defmodule MvWeb.RoleLiveTest do
|
|||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles")
|
||||
|
||||
assert html =~ "System Role" || html =~ "system"
|
||||
end
|
||||
|
||||
test "delete button disabled for system roles", %{conn: conn} do
|
||||
test "delete button disabled for system roles", %{conn: conn, actor: actor} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
|
|
@ -144,7 +145,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/admin/roles")
|
||||
|
||||
|
|
@ -191,8 +192,9 @@ defmodule MvWeb.RoleLiveTest do
|
|||
|
||||
describe "show page" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor, user: user}
|
||||
end
|
||||
|
||||
test "mounts with valid role ID", %{conn: conn} do
|
||||
|
|
@ -215,7 +217,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
assert match?({:error, {:redirect, %{to: "/admin/roles"}}}, result)
|
||||
end
|
||||
|
||||
test "shows system role badge if is_system_role is true", %{conn: conn} do
|
||||
test "shows system role badge if is_system_role is true", %{conn: conn, actor: actor} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
|
|
@ -223,7 +225,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
|
||||
|
||||
|
|
@ -233,8 +235,9 @@ defmodule MvWeb.RoleLiveTest do
|
|||
|
||||
describe "form - create" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor, user: user}
|
||||
end
|
||||
|
||||
test "mounts successfully", %{conn: conn} do
|
||||
|
|
@ -306,9 +309,10 @@ defmodule MvWeb.RoleLiveTest do
|
|||
|
||||
describe "form - edit" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
role = create_role()
|
||||
%{conn: conn, user: user, role: role}
|
||||
%{conn: conn, actor: system_actor, user: user, role: role}
|
||||
end
|
||||
|
||||
test "mounts with valid role ID", %{conn: conn, role: role} do
|
||||
|
|
@ -347,7 +351,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
assert updated_role.name == "Updated Role Name"
|
||||
end
|
||||
|
||||
test "updates system role's permission_set_name", %{conn: conn} do
|
||||
test "updates system role's permission_set_name", %{conn: conn, actor: actor} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
|
|
@ -355,7 +359,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/admin/roles/#{system_role.id}/edit?return_to=show")
|
||||
|
||||
|
|
@ -379,8 +383,9 @@ defmodule MvWeb.RoleLiveTest do
|
|||
|
||||
describe "delete functionality" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{conn, user, _admin_role} = create_admin_user(conn, system_actor)
|
||||
%{conn: conn, actor: system_actor, user: user}
|
||||
end
|
||||
|
||||
test "deletes non-system role", %{conn: conn} do
|
||||
|
|
@ -400,7 +405,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
Authorization.get_role(role.id)
|
||||
end
|
||||
|
||||
test "fails to delete system role with error message", %{conn: conn} do
|
||||
test "fails to delete system role with error message", %{conn: conn, actor: actor} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
|
|
@ -408,7 +413,7 @@ defmodule MvWeb.RoleLiveTest do
|
|||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
{:ok, view, html} = live(conn, "/admin/roles")
|
||||
|
||||
|
|
@ -428,8 +433,13 @@ defmodule MvWeb.RoleLiveTest do
|
|||
end
|
||||
|
||||
describe "authorization" do
|
||||
test "only admin can access /admin/roles", %{conn: conn} do
|
||||
{conn, _user} = create_non_admin_user(conn)
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
test "only admin can access /admin/roles", %{conn: conn, actor: actor} do
|
||||
{conn, _user} = create_non_admin_user(conn, actor)
|
||||
|
||||
# Non-admin should be redirected or see error
|
||||
# Note: Authorization is checked via can_access_page? which returns false
|
||||
|
|
@ -443,8 +453,8 @@ defmodule MvWeb.RoleLiveTest do
|
|||
assert html =~ "Listing Roles" || html =~ "Roles"
|
||||
end
|
||||
|
||||
test "admin can access /admin/roles", %{conn: conn} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||
test "admin can access /admin/roles", %{conn: conn, actor: actor} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn, actor)
|
||||
|
||||
{:ok, _view, _html} = live(conn, "/admin/roles")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ defmodule MvWeb.UserLive.ShowTest do
|
|||
end
|
||||
|
||||
test "displays linked member when present", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -72,7 +74,7 @@ defmodule MvWeb.UserLive.ShowTest do
|
|||
last_name: "Smith",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create user and link to member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
|
|
@ -81,7 +83,7 @@ defmodule MvWeb.UserLive.ShowTest do
|
|||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:member, member, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
|||
|
||||
describe "error handling - flash messages" do
|
||||
test "shows flash message when member creation fails with validation error", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create a member with the same email to trigger uniqueness error
|
||||
{:ok, _existing_member} =
|
||||
Member
|
||||
|
|
@ -20,7 +22,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
|||
last_name: "Member",
|
||||
email: "duplicate@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members/new")
|
||||
|
|
@ -73,6 +75,8 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
|||
end
|
||||
|
||||
test "shows flash message when member update fails", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create a member to edit
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -81,7 +85,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
|||
last_name: "Member",
|
||||
email: "original@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create another member with different email
|
||||
{:ok, _other_member} =
|
||||
|
|
@ -91,7 +95,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
|||
last_name: "Member",
|
||||
email: "other@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -23,11 +25,13 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -38,7 +42,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
describe "membership fee type dropdown" do
|
||||
|
|
@ -123,10 +127,12 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
|> render_submit()
|
||||
|
||||
# Verify member was created with fee type
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
member =
|
||||
Member
|
||||
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: system_actor)
|
||||
|
||||
assert member.membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
|
@ -135,13 +141,14 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
# Set default fee type in settings
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
settings
|
||||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/new")
|
||||
|
||||
|
|
@ -156,6 +163,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
conn: conn,
|
||||
current_user: admin_user
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create custom field
|
||||
custom_field =
|
||||
Mv.Membership.CustomField
|
||||
|
|
@ -164,7 +173,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
value_type: :string,
|
||||
required: false
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
|
||||
# Create two fee types with same interval
|
||||
fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly})
|
||||
|
|
@ -250,6 +259,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
end
|
||||
|
||||
test "removing custom field values works correctly", %{conn: conn, current_user: admin_user} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create custom field
|
||||
custom_field =
|
||||
Mv.Membership.CustomField
|
||||
|
|
@ -258,7 +269,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
|||
value_type: :string,
|
||||
required: false
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -23,11 +25,13 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -38,13 +42,15 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
# Note: Does not delete existing cycles - tests should manage their own test data
|
||||
# If cleanup is needed, it should be done in setup or explicitly in the test
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -57,7 +63,7 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
describe "load_cycles_for_members/2" do
|
||||
|
|
@ -75,7 +81,8 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
|> Ash.Query.filter(id in [^member1.id, ^member2.id])
|
||||
|> MembershipFeeStatus.load_cycles_for_members()
|
||||
|
||||
members = Ash.read!(query)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
members = Ash.read!(query, actor: system_actor)
|
||||
|
||||
assert length(members) == 2
|
||||
|
||||
|
|
@ -94,19 +101,21 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
# Create member without fee type to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
# Delete any auto-generated cycles
|
||||
cycles =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
|
||||
|
||||
# Create cycles with dates that ensure 2023 is last completed
|
||||
# Use a fixed "today" date in 2024 to make 2023 the last completed
|
||||
|
|
@ -137,19 +146,21 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
# Create member without fee type to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
# Delete any auto-generated cycles
|
||||
cycles =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
|
||||
|
||||
# Create cycles - use current year for current cycle
|
||||
today = Date.utc_today()
|
||||
|
|
@ -176,19 +187,21 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
# Create member without fee type to avoid auto-generation
|
||||
member = create_member(%{})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Assign fee type
|
||||
member =
|
||||
member
|
||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
# Delete any auto-generated cycles
|
||||
cycles =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
|
||||
|
||||
# Load cycles and fee type first (will be empty)
|
||||
member =
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -22,7 +24,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field with show_in_overview: true
|
||||
{:ok, field} =
|
||||
|
|
@ -32,7 +34,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
|
|
@ -42,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "A001"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{member: member, field: field}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|
|
@ -25,7 +27,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|
|
@ -34,7 +36,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom fields
|
||||
{:ok, field_show_string} =
|
||||
|
|
@ -44,7 +46,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field_hide} =
|
||||
CustomField
|
||||
|
|
@ -53,7 +55,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
value_type: :string,
|
||||
show_in_overview: false
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field_show_integer} =
|
||||
CustomField
|
||||
|
|
@ -62,7 +64,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
value_type: :integer,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field_show_boolean} =
|
||||
CustomField
|
||||
|
|
@ -71,7 +73,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
value_type: :boolean,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field_show_date} =
|
||||
CustomField
|
||||
|
|
@ -80,7 +82,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
value_type: :date,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field_show_email} =
|
||||
CustomField
|
||||
|
|
@ -89,7 +91,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
value_type: :email,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field values for member1
|
||||
{:ok, _cfv1} =
|
||||
|
|
@ -99,7 +101,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
custom_field_id: field_show_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -108,7 +110,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
custom_field_id: field_show_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 12_345}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|
|
@ -117,7 +119,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
custom_field_id: field_show_boolean.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv4} =
|
||||
CustomFieldValue
|
||||
|
|
@ -126,7 +128,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
custom_field_id: field_show_date.id,
|
||||
value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv5} =
|
||||
CustomFieldValue
|
||||
|
|
@ -135,7 +137,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
custom_field_id: field_show_email.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "alice.private@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create hidden custom field value (should not be displayed)
|
||||
{:ok, _cfv_hidden} =
|
||||
|
|
@ -145,7 +147,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
custom_field_id: field_hide.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Internal note"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
alias Mv.Membership.{CustomField, Member}
|
||||
|
||||
test "displays custom field column even when no members have values", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test members without custom field values
|
||||
{:ok, _member1} =
|
||||
Member
|
||||
|
|
@ -21,7 +23,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _member2} =
|
||||
Member
|
||||
|
|
@ -30,7 +32,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field with show_in_overview: true but no values
|
||||
{:ok, field} =
|
||||
|
|
@ -40,7 +42,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
|
@ -50,6 +52,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
end
|
||||
|
||||
test "displays very long custom field values correctly", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -58,7 +62,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field
|
||||
{:ok, field} =
|
||||
|
|
@ -68,7 +72,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create very long value (but within limits)
|
||||
long_value = String.duplicate("A", 500)
|
||||
|
|
@ -80,7 +84,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => long_value}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
|
@ -91,6 +95,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
end
|
||||
|
||||
test "handles multiple custom fields with show_in_overview correctly", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -99,7 +105,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create multiple custom fields with show_in_overview: true
|
||||
{:ok, field1} =
|
||||
|
|
@ -109,7 +115,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field2} =
|
||||
CustomField
|
||||
|
|
@ -118,7 +124,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field3} =
|
||||
CustomField
|
||||
|
|
@ -127,7 +133,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create values for all fields
|
||||
{:ok, _cfv1} =
|
||||
|
|
@ -137,7 +143,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
custom_field_id: field1.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Value1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|
|
@ -146,7 +152,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
custom_field_id: field2.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Value2"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|
|
@ -155,7 +161,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
custom_field_id: field3.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Value3"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|
|
@ -24,7 +26,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|
|
@ -33,7 +35,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member3} =
|
||||
Member
|
||||
|
|
@ -42,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Clark",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field with show_in_overview: true
|
||||
{:ok, field_string} =
|
||||
|
|
@ -52,7 +54,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, field_integer} =
|
||||
CustomField
|
||||
|
|
@ -61,7 +63,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
value_type: :integer,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field values
|
||||
{:ok, _cfv1} =
|
||||
|
|
@ -71,7 +73,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "A001"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -80,7 +82,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "C003"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|
|
@ -89,7 +91,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "B002"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv4} =
|
||||
CustomFieldValue
|
||||
|
|
@ -98,7 +100,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 10}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv5} =
|
||||
CustomFieldValue
|
||||
|
|
@ -107,7 +109,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 30}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv6} =
|
||||
CustomFieldValue
|
||||
|
|
@ -116,7 +118,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 20}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
|
|
@ -236,6 +238,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
end
|
||||
|
||||
test "NULL values and empty strings are always sorted last (ASC)", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create additional members with NULL and empty string values
|
||||
{:ok, member_with_value} =
|
||||
Member
|
||||
|
|
@ -244,7 +248,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "withvalue@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member_with_empty} =
|
||||
Member
|
||||
|
|
@ -253,7 +257,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "withempty@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member_with_null} =
|
||||
Member
|
||||
|
|
@ -262,7 +266,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "withnull@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member_with_another_value} =
|
||||
Member
|
||||
|
|
@ -271,7 +275,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "another@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field
|
||||
{:ok, field} =
|
||||
|
|
@ -281,7 +285,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
|
||||
{:ok, _cfv1} =
|
||||
|
|
@ -291,7 +295,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -300,7 +304,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# member_with_null has no custom field value (NULL)
|
||||
|
||||
|
|
@ -311,7 +315,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Apple"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
|
|
@ -347,6 +351,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
end
|
||||
|
||||
test "NULL values and empty strings are always sorted last (DESC)", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create additional members with NULL and empty string values
|
||||
{:ok, member_with_value} =
|
||||
Member
|
||||
|
|
@ -355,7 +361,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "withvalue@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member_with_empty} =
|
||||
Member
|
||||
|
|
@ -364,7 +370,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "withempty@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member_with_null} =
|
||||
Member
|
||||
|
|
@ -373,7 +379,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "withnull@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member_with_another_value} =
|
||||
Member
|
||||
|
|
@ -382,7 +388,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
last_name: "Test",
|
||||
email: "another@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field
|
||||
{:ok, field} =
|
||||
|
|
@ -392,7 +398,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
|
||||
{:ok, _cfv1} =
|
||||
|
|
@ -402,7 +408,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Apple"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -411,7 +417,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# member_with_null has no custom field value (NULL)
|
||||
|
||||
|
|
@ -422,7 +428,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|
|
@ -29,7 +31,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
street: "Main St",
|
||||
city: "Berlin"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|
|
@ -40,7 +42,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
street: "Second St",
|
||||
city: "Hamburg"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field
|
||||
{:ok, custom_field} =
|
||||
|
|
@ -50,7 +52,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
# Create custom field values
|
||||
{:ok, _cfv1} =
|
||||
|
|
@ -60,7 +62,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: "M001"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|
|
@ -69,7 +71,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: "M002"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
|
|||
alias Mv.Membership.Member
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
|
|
@ -18,7 +20,7 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
|
|||
city: "Berlin",
|
||||
join_date: ~D[2020-01-15]
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|
|
@ -27,7 +29,7 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
|
|||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
|||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -24,11 +26,13 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -39,18 +43,20 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Delete any auto-generated cycles first to avoid conflicts
|
||||
existing_cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
|
||||
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
|
|
@ -64,7 +70,7 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
describe "status column display" do
|
||||
|
|
@ -172,16 +178,18 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
|||
member2 = create_member(%{first_name: "PaidMember", membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Verify cycles exist in database
|
||||
cycles1 =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member1.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
cycles2 =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member2.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
refute Enum.empty?(cycles1)
|
||||
refute Enum.empty?(cycles2)
|
||||
|
|
@ -206,16 +214,18 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
|||
member2 = create_member(%{first_name: "PaidCurrent", membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Verify cycles exist in database
|
||||
cycles1 =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member1.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
cycles2 =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member2.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
refute Enum.empty?(cycles1)
|
||||
refute Enum.empty?(cycles2)
|
||||
|
|
|
|||
|
|
@ -266,13 +266,18 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
end
|
||||
|
||||
test "can delete a member without error", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create a test member first
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, index_view, _html} = live(conn, "/members")
|
||||
|
|
@ -294,27 +299,38 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
describe "copy_emails feature" do
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Max",
|
||||
last_name: "Mustermann",
|
||||
email: "max@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member2} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Erika",
|
||||
last_name: "Musterfrau",
|
||||
email: "erika@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, member3} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Hans",
|
||||
last_name: "Müller-Lüdenscheidt",
|
||||
email: "hans@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
%{member1: member1, member2: member2, member3: member3}
|
||||
end
|
||||
|
|
@ -394,7 +410,8 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
render_click(view, "select_member", %{"id" => member1.id})
|
||||
|
||||
# Delete the member from the database
|
||||
Ash.destroy!(member1)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
Ash.destroy!(member1, actor: system_actor)
|
||||
|
||||
# Trigger copy_emails event directly - selection still contains the deleted ID
|
||||
# but the member is no longer in @members list after reload
|
||||
|
|
@ -434,12 +451,17 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Create a member with known data
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, test_member} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Test",
|
||||
last_name: "Format",
|
||||
email: "test.format@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
|
|
@ -500,8 +522,26 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
end
|
||||
|
||||
describe "cycle status filter" do
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs, actor) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!(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",
|
||||
|
|
@ -512,32 +552,74 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
Mv.Membership.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, actor) do
|
||||
# Delete any auto-generated cycles first to avoid conflicts
|
||||
existing_cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!(actor: actor)
|
||||
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
|
||||
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!(actor: actor)
|
||||
end
|
||||
|
||||
test "filter shows only members with paid status in last cycle", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
|
||||
today = Date.utc_today()
|
||||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||
|
||||
# Member with paid last cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "PaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||
create_cycle(
|
||||
paid_member,
|
||||
fee_type,
|
||||
%{cycle_start: last_year_start, status: :paid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
# Member with unpaid last cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "UnpaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||
create_cycle(
|
||||
unpaid_member,
|
||||
fee_type,
|
||||
%{cycle_start: last_year_start, status: :unpaid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
|
|
@ -546,28 +628,45 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
end
|
||||
|
||||
test "filter shows only members with unpaid status in last cycle", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
|
||||
today = Date.utc_today()
|
||||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||
|
||||
# Member with paid last cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "PaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||
create_cycle(
|
||||
paid_member,
|
||||
fee_type,
|
||||
%{cycle_start: last_year_start, status: :paid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
# Member with unpaid last cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "UnpaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||
create_cycle(
|
||||
unpaid_member,
|
||||
fee_type,
|
||||
%{cycle_start: last_year_start, status: :unpaid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||||
|
||||
|
|
@ -576,28 +675,45 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
end
|
||||
|
||||
test "filter shows only members with paid status in current cycle", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
|
||||
today = Date.utc_today()
|
||||
current_year_start = Date.new!(today.year, 1, 1)
|
||||
|
||||
# Member with paid current cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "PaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
create_cycle(
|
||||
paid_member,
|
||||
fee_type,
|
||||
%{cycle_start: current_year_start, status: :paid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
# Member with unpaid current cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "UnpaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||
create_cycle(
|
||||
unpaid_member,
|
||||
fee_type,
|
||||
%{cycle_start: current_year_start, status: :unpaid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid&show_current_cycle=true")
|
||||
|
||||
|
|
@ -606,28 +722,45 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
end
|
||||
|
||||
test "filter shows only members with unpaid status in current cycle", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
|
||||
today = Date.utc_today()
|
||||
current_year_start = Date.new!(today.year, 1, 1)
|
||||
|
||||
# Member with paid current cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "PaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
create_cycle(
|
||||
paid_member,
|
||||
fee_type,
|
||||
%{cycle_start: current_year_start, status: :paid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
# Member with unpaid current cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
create_member(
|
||||
%{
|
||||
first_name: "UnpaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
},
|
||||
system_actor
|
||||
)
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||
create_cycle(
|
||||
unpaid_member,
|
||||
fee_type,
|
||||
%{cycle_start: current_year_start, status: :unpaid},
|
||||
system_actor
|
||||
)
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, "/members?cycle_status_filter=unpaid&show_current_cycle=true")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -24,11 +26,13 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -39,7 +43,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
describe "end-to-end workflows" do
|
||||
|
|
@ -75,7 +79,13 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
|> render_click()
|
||||
|
||||
# Verify status changed
|
||||
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
updated_cycle =
|
||||
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert updated_cycle.status == :paid
|
||||
end
|
||||
end
|
||||
|
|
@ -115,13 +125,14 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
# Update settings
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
settings
|
||||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|> Ash.update!(actor: system_actor)
|
||||
|
||||
# Create new member
|
||||
{:ok, view, _html} = live(conn, "/members/new")
|
||||
|
|
@ -138,10 +149,12 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
|> render_submit()
|
||||
|
||||
# Verify member got default type
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
member =
|
||||
Member
|
||||
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: system_actor)
|
||||
|
||||
assert member.membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
|
@ -150,6 +163,8 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -159,7 +174,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||
|
||||
|
|
@ -187,6 +202,8 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
|
|
@ -196,7 +213,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||
|
||||
|
|
@ -216,7 +233,13 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
|||
|> render_submit()
|
||||
|
||||
# Verify amount updated
|
||||
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
updated_cycle =
|
||||
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert updated_cycle.amount == Decimal.new("75.00")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -24,11 +26,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
|
|
@ -39,18 +43,20 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|||
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Delete any auto-generated cycles first to avoid conflicts
|
||||
existing_cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: system_actor)
|
||||
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
|
||||
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
|
|
@ -64,7 +70,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
describe "cycles table display" do
|
||||
|
|
@ -161,7 +167,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|||
|> render_click()
|
||||
|
||||
# Verify cycle is now paid
|
||||
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
updated_cycle =
|
||||
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert updated_cycle.status == :paid
|
||||
end
|
||||
|
||||
|
|
@ -186,7 +198,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|||
|> render_click()
|
||||
|
||||
# Verify cycle is now suspended
|
||||
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
updated_cycle =
|
||||
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert updated_cycle.status == :suspended
|
||||
end
|
||||
|
||||
|
|
@ -211,7 +229,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|||
|> render_click()
|
||||
|
||||
# Verify cycle is now unpaid
|
||||
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
updated_cycle =
|
||||
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert updated_cycle.status == :unpaid
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|
|
@ -29,15 +31,16 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
%{member: member}
|
||||
%{member: member, actor: system_actor}
|
||||
end
|
||||
|
||||
describe "custom fields section visibility (Issue #282)" do
|
||||
test "displays Custom Fields section even when member has no custom field values", %{
|
||||
conn: conn,
|
||||
member: member
|
||||
member: member,
|
||||
actor: actor
|
||||
} do
|
||||
# Create a custom field but no value for the member
|
||||
{:ok, custom_field} =
|
||||
|
|
@ -46,7 +49,7 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
name: "phone_mobile",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
|
||||
|
|
@ -63,7 +66,8 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
|
||||
test "displays Custom Fields section with multiple custom fields, some without values", %{
|
||||
conn: conn,
|
||||
member: member
|
||||
member: member,
|
||||
actor: actor
|
||||
} do
|
||||
# Create multiple custom fields
|
||||
{:ok, field1} =
|
||||
|
|
@ -72,7 +76,7 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
name: "phone_mobile",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
{:ok, field2} =
|
||||
CustomField
|
||||
|
|
@ -80,7 +84,7 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
name: "membership_number",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create value only for first field
|
||||
{:ok, _cfv} =
|
||||
|
|
@ -90,7 +94,7 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
custom_field_id: field1.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
|
||||
|
|
@ -111,18 +115,19 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
|
||||
test "does not display Custom Fields section when no custom fields exist", %{
|
||||
conn: conn,
|
||||
member: member
|
||||
member: member,
|
||||
actor: actor
|
||||
} do
|
||||
# Ensure no custom fields exist for this test
|
||||
# This ensures test isolation even if previous tests created custom fields
|
||||
existing_custom_fields = Ash.read!(CustomField)
|
||||
existing_custom_fields = Ash.read!(CustomField, actor: actor)
|
||||
|
||||
for cf <- existing_custom_fields do
|
||||
Ash.destroy!(cf)
|
||||
Ash.destroy!(cf, actor: actor)
|
||||
end
|
||||
|
||||
# Verify no custom fields exist
|
||||
assert Ash.read!(CustomField) == []
|
||||
assert Ash.read!(CustomField, actor: actor) == []
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
|
||||
|
|
@ -133,14 +138,14 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
end
|
||||
|
||||
describe "custom field value formatting" do
|
||||
test "formats string custom field values", %{conn: conn, member: member} do
|
||||
test "formats string custom field values", %{conn: conn, member: member, actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "phone_mobile",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|
|
@ -149,7 +154,7 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
|
||||
|
|
@ -157,14 +162,18 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
assert html =~ "+49123456789"
|
||||
end
|
||||
|
||||
test "formats email custom field values as mailto links", %{conn: conn, member: member} do
|
||||
test "formats email custom field values as mailto links", %{
|
||||
conn: conn,
|
||||
member: member,
|
||||
actor: actor
|
||||
} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "private_email",
|
||||
value_type: :email
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|
|
@ -173,7 +182,7 @@ defmodule MvWeb.MemberLive.ShowTest do
|
|||
custom_field_id: custom_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "private@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
|
||||
|
|
|
|||
|
|
@ -70,12 +70,17 @@ defmodule MvWeb.UserLive.FormMemberDropdownTest do
|
|||
test "links user and member with identical email successfully", %{conn: conn} do
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "David",
|
||||
last_name: "Miller",
|
||||
email: "david@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -106,12 +111,17 @@ defmodule MvWeb.UserLive.FormMemberDropdownTest do
|
|||
test "shows member with same email in dropdown", %{conn: conn} do
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Emma",
|
||||
last_name: "Davis",
|
||||
email: "emma@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -135,13 +145,18 @@ defmodule MvWeb.UserLive.FormMemberDropdownTest do
|
|||
|
||||
# Helper functions
|
||||
defp create_unlinked_members(count) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
for i <- 1..count do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "FirstName#{i}",
|
||||
last_name: "LastName#{i}",
|
||||
email: "member#{i}@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
member
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,14 +18,18 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do
|
|||
|
||||
describe "fuzzy search" do
|
||||
test "finds member with exact name", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jonathan",
|
||||
last_name: "Smith",
|
||||
email: "jonathan.smith@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -41,14 +45,18 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do
|
|||
end
|
||||
|
||||
test "finds member with typo (Jon finds Jonathan)", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Jonathan",
|
||||
last_name: "Smith",
|
||||
email: "jonathan.smith@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -65,14 +73,18 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do
|
|||
end
|
||||
|
||||
test "finds member with partial substring", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Alexander",
|
||||
last_name: "Williams",
|
||||
email: "alex@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -87,14 +99,18 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do
|
|||
end
|
||||
|
||||
test "shows partial match with similar names", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
{:ok, _member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Johnny",
|
||||
last_name: "Doeson",
|
||||
email: "johnny@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,18 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do
|
|||
|
||||
describe "member selection" do
|
||||
test "input field shows selected member name", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -47,14 +51,18 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do
|
|||
end
|
||||
|
||||
test "confirmation box appears", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Bob",
|
||||
last_name: "Williams",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -77,14 +85,18 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do
|
|||
end
|
||||
|
||||
test "hidden input stores member ID", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Brown",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
|
|
@ -105,20 +117,27 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do
|
|||
|
||||
describe "unlink workflow" do
|
||||
test "unlink hides dropdown", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Frank",
|
||||
last_name: "Wilson",
|
||||
email: "frank@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "frank@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
|
|
@ -134,20 +153,27 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do
|
|||
end
|
||||
|
||||
test "unlink shows warning", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Grace",
|
||||
last_name: "Taylor",
|
||||
email: "grace@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "grace@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
|
|
@ -164,20 +190,27 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do
|
|||
end
|
||||
|
||||
test "unlink disables input", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Henry",
|
||||
last_name: "Anderson",
|
||||
email: "henry@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "henry@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
|
|
@ -193,20 +226,27 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do
|
|||
end
|
||||
|
||||
test "save re-enables member selection", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
conn = setup_admin_conn(conn)
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Isabel",
|
||||
last_name: "Martinez",
|
||||
email: "isabel@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
Accounts.create_user(
|
||||
%{
|
||||
email: "isabel@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
|
|
|
|||
|
|
@ -75,11 +75,14 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
|> form("#user-form", user: %{email: "storetest@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
user =
|
||||
Ash.get!(
|
||||
Mv.Accounts.User,
|
||||
[email: Ash.CiString.new("storetest@example.com")],
|
||||
domain: Mv.Accounts
|
||||
domain: Mv.Accounts,
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert to_string(user.email) == "storetest@example.com"
|
||||
|
|
@ -101,11 +104,14 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
)
|
||||
|> render_submit()
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
user =
|
||||
Ash.get!(
|
||||
Mv.Accounts.User,
|
||||
[email: Ash.CiString.new("passwordstoretest@example.com")],
|
||||
domain: Mv.Accounts
|
||||
domain: Mv.Accounts,
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert user.hashed_password != nil
|
||||
|
|
@ -181,7 +187,8 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
|
||||
assert_redirected(view, "/users")
|
||||
|
||||
updated_user = Ash.reload!(user, domain: Mv.Accounts)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
assert to_string(updated_user.email) == "new@example.com"
|
||||
assert updated_user.hashed_password == original_password
|
||||
end
|
||||
|
|
@ -204,7 +211,8 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
|
||||
assert_redirected(view, "/users")
|
||||
|
||||
updated_user = Ash.reload!(user, domain: Mv.Accounts)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor)
|
||||
assert updated_user.hashed_password != original_password
|
||||
assert String.starts_with?(updated_user.hashed_password, "$2b$")
|
||||
end
|
||||
|
|
@ -285,17 +293,24 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
|
||||
describe "member linking - display" do
|
||||
test "shows linked member with unlink button when user has member", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Create user linked to member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
{:ok, _updated_user} = Mv.Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
|
||||
{:ok, _updated_user} =
|
||||
Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor)
|
||||
|
||||
# Load form
|
||||
{:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
|
@ -322,13 +337,18 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
|
||||
describe "member linking - workflow" do
|
||||
test "selecting member and saving links member to user", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create unlinked member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Create user without member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
|
|
@ -345,22 +365,35 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
assert_redirected(view, "/users")
|
||||
|
||||
# Verify member is linked
|
||||
updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member])
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
updated_user =
|
||||
Ash.get!(Mv.Accounts.User, user.id,
|
||||
domain: Mv.Accounts,
|
||||
actor: system_actor,
|
||||
load: [:member]
|
||||
)
|
||||
|
||||
assert updated_user.member.id == member.id
|
||||
end
|
||||
|
||||
test "unlinking member and saving removes member from user", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Bob",
|
||||
last_name: "Wilson",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Create user linked to member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
{:ok, _} = Mv.Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
{:ok, _} = Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor)
|
||||
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
|
|
@ -375,7 +408,15 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
assert_redirected(view, "/users")
|
||||
|
||||
# Verify member is unlinked
|
||||
updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member])
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
updated_user =
|
||||
Ash.get!(Mv.Accounts.User, user.id,
|
||||
domain: Mv.Accounts,
|
||||
actor: system_actor,
|
||||
load: [:member]
|
||||
)
|
||||
|
||||
assert is_nil(updated_user.member)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -407,17 +407,24 @@ defmodule MvWeb.UserLive.IndexTest do
|
|||
|
||||
describe "member linking display" do
|
||||
test "displays linked member name in user list", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Create user linked to member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
{:ok, _updated_user} = Mv.Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
|
||||
{:ok, _updated_user} =
|
||||
Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor)
|
||||
|
||||
# Create another user without member
|
||||
_unlinked_user = create_test_user(%{email: "unlinked@example.com"})
|
||||
|
|
|
|||
|
|
@ -3,37 +3,42 @@ defmodule Mv.SeedsTest do
|
|||
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Seeds script" do
|
||||
test "runs successfully without errors" do
|
||||
test "runs successfully without errors", %{actor: actor} do
|
||||
# Run the seeds script - should not raise any errors
|
||||
assert Code.eval_file("priv/repo/seeds.exs")
|
||||
|
||||
# Basic smoke test: ensure some data was created
|
||||
{:ok, users} = Ash.read(Mv.Accounts.User)
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member)
|
||||
{:ok, custom_fields} = Ash.read(Mv.Membership.CustomField)
|
||||
{:ok, users} = Ash.read(Mv.Accounts.User, actor: actor)
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member, actor: actor)
|
||||
{:ok, custom_fields} = Ash.read(Mv.Membership.CustomField, actor: actor)
|
||||
|
||||
assert not Enum.empty?(users), "Seeds should create at least one user"
|
||||
assert not Enum.empty?(members), "Seeds should create at least one member"
|
||||
assert not Enum.empty?(custom_fields), "Seeds should create at least one custom field"
|
||||
end
|
||||
|
||||
test "can be run multiple times (idempotent)" do
|
||||
test "can be run multiple times (idempotent)", %{actor: actor} do
|
||||
# Run seeds first time
|
||||
assert Code.eval_file("priv/repo/seeds.exs")
|
||||
|
||||
# Count records
|
||||
{:ok, users_count_1} = Ash.read(Mv.Accounts.User)
|
||||
{:ok, members_count_1} = Ash.read(Mv.Membership.Member)
|
||||
{:ok, custom_fields_count_1} = Ash.read(Mv.Membership.CustomField)
|
||||
{:ok, users_count_1} = Ash.read(Mv.Accounts.User, actor: actor)
|
||||
{:ok, members_count_1} = Ash.read(Mv.Membership.Member, actor: actor)
|
||||
{:ok, custom_fields_count_1} = Ash.read(Mv.Membership.CustomField, actor: actor)
|
||||
|
||||
# Run seeds second time - should not raise errors
|
||||
assert Code.eval_file("priv/repo/seeds.exs")
|
||||
|
||||
# Count records again - should be the same (upsert, not duplicate)
|
||||
{:ok, users_count_2} = Ash.read(Mv.Accounts.User)
|
||||
{:ok, members_count_2} = Ash.read(Mv.Membership.Member)
|
||||
{:ok, custom_fields_count_2} = Ash.read(Mv.Membership.CustomField)
|
||||
{:ok, users_count_2} = Ash.read(Mv.Accounts.User, actor: actor)
|
||||
{:ok, members_count_2} = Ash.read(Mv.Membership.Member, actor: actor)
|
||||
{:ok, custom_fields_count_2} = Ash.read(Mv.Membership.CustomField, actor: actor)
|
||||
|
||||
assert length(users_count_1) == length(users_count_2),
|
||||
"Users count should remain same after re-running seeds"
|
||||
|
|
@ -45,12 +50,12 @@ defmodule Mv.SeedsTest do
|
|||
"CustomFields count should remain same after re-running seeds"
|
||||
end
|
||||
|
||||
test "at least one member has no membership fee type assigned" do
|
||||
test "at least one member has no membership fee type assigned", %{actor: actor} do
|
||||
# Run the seeds script
|
||||
assert Code.eval_file("priv/repo/seeds.exs")
|
||||
|
||||
# Get all members
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member)
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member, actor: actor)
|
||||
|
||||
# At least one member should have no membership_fee_type_id
|
||||
members_without_fee_type =
|
||||
|
|
@ -60,13 +65,13 @@ defmodule Mv.SeedsTest do
|
|||
"At least one member should have no membership fee type assigned"
|
||||
end
|
||||
|
||||
test "each membership fee type has at least one member" do
|
||||
test "each membership fee type has at least one member", %{actor: actor} do
|
||||
# Run the seeds script
|
||||
assert Code.eval_file("priv/repo/seeds.exs")
|
||||
|
||||
# Get all fee types and members
|
||||
{:ok, fee_types} = Ash.read(Mv.MembershipFees.MembershipFeeType)
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member)
|
||||
{:ok, fee_types} = Ash.read(Mv.MembershipFees.MembershipFeeType, actor: actor)
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member, actor: actor)
|
||||
|
||||
# Group members by fee type (excluding nil)
|
||||
members_by_fee_type =
|
||||
|
|
@ -83,12 +88,12 @@ defmodule Mv.SeedsTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "members with fee types have cycles with various statuses" do
|
||||
test "members with fee types have cycles with various statuses", %{actor: actor} do
|
||||
# Run the seeds script
|
||||
assert Code.eval_file("priv/repo/seeds.exs")
|
||||
|
||||
# Get all members with fee types
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member)
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member, actor: actor)
|
||||
|
||||
members_with_fee_types =
|
||||
members
|
||||
|
|
@ -104,7 +109,7 @@ defmodule Mv.SeedsTest do
|
|||
|> Enum.flat_map(fn member ->
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|> Ash.read!(actor: actor)
|
||||
end)
|
||||
|> Enum.map(& &1.status)
|
||||
|
||||
|
|
|
|||
|
|
@ -115,15 +115,16 @@ defmodule MvWeb.ConnCase do
|
|||
|
||||
# Create admin role and assign it
|
||||
admin_role = Mv.Fixtures.role_fixture("admin")
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Load role for authorization
|
||||
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts)
|
||||
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts, actor: system_actor)
|
||||
|
||||
sign_in_user_via_oidc(conn, user_with_role)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ defmodule Mv.Fixtures do
|
|||
@doc """
|
||||
Creates a member with default or custom attributes.
|
||||
|
||||
Uses system_actor for authorization to bypass permission checks in tests.
|
||||
|
||||
## Parameters
|
||||
- `attrs` - Map or keyword list of attributes to override defaults
|
||||
|
||||
|
|
@ -25,13 +27,15 @@ defmodule Mv.Fixtures do
|
|||
|
||||
"""
|
||||
def member_fixture(attrs \\ %{}) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Mv.Membership.create_member()
|
||||
|> Mv.Membership.create_member(actor: system_actor)
|
||||
|> case do
|
||||
{:ok, member} -> member
|
||||
{:error, error} -> raise "Failed to create member: #{inspect(error)}"
|
||||
|
|
@ -41,6 +45,11 @@ defmodule Mv.Fixtures do
|
|||
@doc """
|
||||
Creates a user with default or custom attributes.
|
||||
|
||||
Uses system_actor for authorization to bypass permission checks in tests.
|
||||
|
||||
Note: create_user action should work via AshAuthentication bypass,
|
||||
but we use system_actor for consistency and safety.
|
||||
|
||||
## Parameters
|
||||
- `attrs` - Map or keyword list of attributes to override defaults
|
||||
|
||||
|
|
@ -57,11 +66,13 @@ defmodule Mv.Fixtures do
|
|||
|
||||
"""
|
||||
def user_fixture(attrs \\ %{}) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
email: "user#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Mv.Accounts.create_user()
|
||||
|> Mv.Accounts.create_user(actor: system_actor)
|
||||
|> case do
|
||||
{:ok, user} -> user
|
||||
{:error, error} -> raise "Failed to create user: #{inspect(error)}"
|
||||
|
|
@ -97,6 +108,8 @@ defmodule Mv.Fixtures do
|
|||
@doc """
|
||||
Creates a role with a specific permission set.
|
||||
|
||||
Uses system_actor for authorization to bypass permission checks in tests.
|
||||
|
||||
## Parameters
|
||||
- `permission_set_name` - The permission set name (e.g., "admin", "read_only", "normal_user", "own_data")
|
||||
|
||||
|
|
@ -110,13 +123,17 @@ defmodule Mv.Fixtures do
|
|||
|
||||
"""
|
||||
def role_fixture(permission_set_name) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
||||
|
||||
case Mv.Authorization.create_role(%{
|
||||
case Mv.Authorization.create_role(
|
||||
%{
|
||||
name: role_name,
|
||||
description: "Test role for #{permission_set_name}",
|
||||
permission_set_name: permission_set_name
|
||||
}) do
|
||||
},
|
||||
actor: system_actor
|
||||
) do
|
||||
{:ok, role} -> role
|
||||
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||
end
|
||||
|
|
@ -140,6 +157,8 @@ defmodule Mv.Fixtures do
|
|||
|
||||
"""
|
||||
def user_with_role_fixture(permission_set_name \\ "admin", user_attrs \\ %{}) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
# Create role with permission set
|
||||
role = role_fixture(permission_set_name)
|
||||
|
||||
|
|
@ -149,14 +168,14 @@ defmodule Mv.Fixtures do
|
|||
|> Enum.into(%{
|
||||
email: "user#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Mv.Accounts.create_user()
|
||||
|> Mv.Accounts.create_user(actor: system_actor)
|
||||
|
||||
# Assign role to user
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: system_actor)
|
||||
|
||||
# Reload user with role preloaded (critical for authorization!)
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue