mitgliederverwaltung/test/membership/member_available_for_linking_test.exs
Moritz 1819a1e2d1
Some checks failed
continuous-integration/drone/push Build is failing
feat: add user to member linking
2025-11-20 15:27:41 +01:00

222 lines
7 KiB
Elixir

defmodule Mv.Membership.MemberAvailableForLinkingTest do
@moduledoc """
Tests for the Member.available_for_linking action.
This action returns members that can be linked to a user account:
- Only members without existing user links (user_id == nil)
- Limited to 10 results
- Special email-match logic: if user_email matches member email, only return that member
- Optional search query filtering by name and email
"""
use Mv.DataCase, async: true
alias Mv.Membership
describe "available_for_linking/2" do
setup do
# Create 5 unlinked members with distinct names
{:ok, member1} =
Membership.create_member(%{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
{:ok, member2} =
Membership.create_member(%{
first_name: "Bob",
last_name: "Williams",
email: "bob@example.com"
})
{:ok, member3} =
Membership.create_member(%{
first_name: "Charlie",
last_name: "Davis",
email: "charlie@example.com"
})
{:ok, member4} =
Membership.create_member(%{
first_name: "Diana",
last_name: "Martinez",
email: "diana@example.com"
})
{:ok, member5} =
Membership.create_member(%{
first_name: "Emma",
last_name: "Taylor",
email: "emma@example.com"
})
unlinked_members = [member1, member2, member3, member4, member5]
# Create 2 linked members (with users)
{:ok, user1} = Mv.Accounts.create_user(%{email: "user1@example.com"})
{:ok, linked_member1} =
Membership.create_member(%{
first_name: "Linked",
last_name: "Member1",
email: "linked1@example.com",
user: %{id: user1.id}
})
{:ok, user2} = Mv.Accounts.create_user(%{email: "user2@example.com"})
{:ok, linked_member2} =
Membership.create_member(%{
first_name: "Linked",
last_name: "Member2",
email: "linked2@example.com",
user: %{id: user2.id}
})
%{
unlinked_members: unlinked_members,
linked_members: [linked_member1, linked_member2]
}
end
test "returns only unlinked members and limits to 10", %{
unlinked_members: unlinked_members,
linked_members: _linked_members
} do
# Call the action without any arguments
members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{})
|> Ash.read!()
# Should return only the 5 unlinked members, not the 2 linked ones
assert length(members) == 5
returned_ids = Enum.map(members, & &1.id) |> MapSet.new()
expected_ids = Enum.map(unlinked_members, & &1.id) |> MapSet.new()
assert MapSet.equal?(returned_ids, expected_ids)
# 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])
assert is_nil(member_with_user.user)
end)
end
test "limits results to 10 members even when more exist" do
# Create 15 additional unlinked members (total 20 unlinked)
for i <- 6..20 do
Membership.create_member(%{
first_name: "Extra#{i}",
last_name: "Member#{i}",
email: "extra#{i}@example.com"
})
end
members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{})
|> Ash.read!()
# Should be limited to 10
assert length(members) == 10
end
test "email match: returns only member with matching email when exists", %{
unlinked_members: unlinked_members
} do
# Get one of the unlinked members' email
target_member = List.first(unlinked_members)
user_email = target_member.email
raw_members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{user_email: user_email})
|> Ash.read!()
# Apply email match filtering (sorted results come from query)
# When user_email matches, only that member should be returned
members = Mv.Membership.Member.filter_by_email_match(raw_members, user_email)
# Should return only the member with matching email
assert length(members) == 1
assert List.first(members).id == target_member.id
assert List.first(members).email == user_email
end
test "email match: returns all unlinked members when no email match" do
# 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!()
# Apply email match filtering
members = Mv.Membership.Member.filter_by_email_match(raw_members, non_matching_email)
# Should return all 5 unlinked members since no match
assert length(members) == 5
end
test "search query: filters by first_name, last_name, and email", %{
unlinked_members: _unlinked_members
} do
# Search by first name
members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{search_query: "Alice"})
|> Ash.read!()
assert length(members) == 1
assert List.first(members).first_name == "Alice"
# Search by last name
members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{search_query: "Williams"})
|> Ash.read!()
assert length(members) == 1
assert List.first(members).last_name == "Williams"
# Search by email
members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{search_query: "charlie@"})
|> Ash.read!()
assert length(members) == 1
assert List.first(members).email == "charlie@example.com"
# Search returns empty when no matches
members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{search_query: "NonExistent"})
|> Ash.read!()
assert Enum.empty?(members)
end
test "search query takes precedence over email match", %{unlinked_members: unlinked_members} do
target_member = List.first(unlinked_members)
# Pass both email match and search query that would match different members
raw_members =
Mv.Membership.Member
|> Ash.Query.for_read(:available_for_linking, %{
user_email: target_member.email,
search_query: "Bob"
})
|> Ash.read!()
# Search query takes precedence, should match "Bob" in the first name
# user_email is used for POST-filtering only, not in the query
assert length(raw_members) == 1
# Should find the member with "Bob" first name, not target_member (Alice)
assert List.first(raw_members).first_name == "Bob"
refute List.first(raw_members).id == target_member.id
end
end
end