feat: add user to member linking
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
21ec86839a
commit
1819a1e2d1
22 changed files with 2061 additions and 45 deletions
33
test/accounts/debug_changeset_test.exs
Normal file
33
test/accounts/debug_changeset_test.exs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
defmodule Mv.Accounts.DebugChangesetTest do
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
test "debug: what's in the changeset when linking with same email" do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Emma",
|
||||
last_name: "Davis",
|
||||
email: "emma@example.com"
|
||||
})
|
||||
|
||||
IO.puts("\n=== MEMBER CREATED ===")
|
||||
IO.puts("Member ID: #{member.id}")
|
||||
IO.puts("Member Email: #{member.email}")
|
||||
|
||||
# Try to create user with same email and link
|
||||
IO.puts("\n=== ATTEMPTING TO CREATE USER WITH LINK ===")
|
||||
|
||||
# Let's intercept the validation to see what's in the changeset
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
email: "emma@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
IO.puts("\n=== RESULT ===")
|
||||
IO.inspect(result, label: "Result")
|
||||
end
|
||||
end
|
||||
169
test/accounts/user_member_linking_email_test.exs
Normal file
169
test/accounts/user_member_linking_email_test.exs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
defmodule Mv.Accounts.UserMemberLinkingEmailTest do
|
||||
@moduledoc """
|
||||
Tests email validation during user-member linking.
|
||||
Implements rules from docs/email-sync.md.
|
||||
Tests for Issue #168, specifically Problem #4: Email validation bug.
|
||||
"""
|
||||
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
describe "link with same email" do
|
||||
test "succeeds when user.email == member.email" do
|
||||
# Create member with specific email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|
||||
# Create user with same email and link to member
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
email: "alice@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
# 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)
|
||||
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
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Bob",
|
||||
last_name: "Smith",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|
||||
# Create user and link
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
email: "bob@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
# Update user (should not trigger email validation error)
|
||||
result = Accounts.update_user(user, %{email: "bob@example.com"})
|
||||
|
||||
assert {:ok, updated_user} = result
|
||||
assert to_string(updated_user.email) == "bob@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "link with different emails" do
|
||||
test "fails if member.email is used by a DIFFERENT linked user" do
|
||||
# Create first user and link to a different member
|
||||
{:ok, other_member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Other",
|
||||
last_name: "Member",
|
||||
email: "other@example.com"
|
||||
})
|
||||
|
||||
{:ok, _user1} =
|
||||
Accounts.create_user(%{
|
||||
email: "user1@example.com",
|
||||
member: %{id: other_member.id}
|
||||
})
|
||||
|
||||
# Reload to ensure email sync happened
|
||||
_other_member = Ash.reload!(other_member)
|
||||
|
||||
# Create a NEW member with different email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Brown",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|
||||
# Try to create user2 with email that matches the linked other_member
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
email: "user1@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
# 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
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "David",
|
||||
last_name: "Wilson",
|
||||
email: "david@example.com"
|
||||
})
|
||||
|
||||
# Create user with different but unique email
|
||||
result =
|
||||
Accounts.create_user(%{
|
||||
email: "user@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
# 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)
|
||||
assert user.member.email == "user@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "unlinking and relinking with same email works (Problem #4)" 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)
|
||||
# 3. Try to relink (validation should NOT fail)
|
||||
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Emma",
|
||||
last_name: "Davis",
|
||||
email: "emma@example.com"
|
||||
})
|
||||
|
||||
# Create user and link
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
email: "emma@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
# Verify they are linked
|
||||
user = Ash.load!(user, [:member], domain: Mv.Accounts)
|
||||
assert user.member.id == member.id
|
||||
assert user.member.email == "emma@example.com"
|
||||
|
||||
# Unlink
|
||||
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil})
|
||||
assert is_nil(unlinked_user.member_id)
|
||||
|
||||
# Member still has the email after unlink
|
||||
member = Ash.reload!(member)
|
||||
assert member.email == "emma@example.com"
|
||||
|
||||
# Relink (should work - this is Problem #4)
|
||||
result = Accounts.update_user(unlinked_user, %{member: %{id: member.id}})
|
||||
|
||||
assert {:ok, relinked_user} = result
|
||||
assert relinked_user.member_id == member.id
|
||||
end
|
||||
end
|
||||
end
|
||||
130
test/accounts/user_member_linking_test.exs
Normal file
130
test/accounts/user_member_linking_test.exs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
defmodule Mv.Accounts.UserMemberLinkingTest do
|
||||
@moduledoc """
|
||||
Integration tests for User-Member linking functionality.
|
||||
|
||||
Tests the complete workflow of linking and unlinking members to users,
|
||||
including email synchronization and validation rules.
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
describe "User-Member Linking with Email Sync" do
|
||||
test "link user to member with different email syncs member email" do
|
||||
# Create user with one email
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
|
||||
|
||||
# Create member with different email
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "member@example.com"
|
||||
})
|
||||
|
||||
# Link user to member
|
||||
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
|
||||
# Verify link exists
|
||||
user_with_member = Ash.get!(Mv.Accounts.User, updated_user.id, 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)
|
||||
assert synced_member.email == "user@example.com"
|
||||
end
|
||||
|
||||
test "unlink member from user sets member to nil" do
|
||||
# Create and link user and member
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
|
||||
# Verify link exists
|
||||
user_with_member = Ash.get!(Mv.Accounts.User, linked_user.id, 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})
|
||||
|
||||
# Verify link is removed
|
||||
user_without_member = Ash.get!(Mv.Accounts.User, unlinked_user.id, load: [:member])
|
||||
assert is_nil(user_without_member.member)
|
||||
|
||||
# Verify member still exists independently
|
||||
member_still_exists = Ash.get!(Mv.Membership.Member, member.id)
|
||||
assert member_still_exists.id == member.id
|
||||
end
|
||||
|
||||
test "cannot link member already linked to another user" do
|
||||
# Create first user and link to member
|
||||
{:ok, user1} = Accounts.create_user(%{email: "user1@example.com"})
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Bob",
|
||||
last_name: "Wilson",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|
||||
{:ok, _linked_user1} = Accounts.update_user(user1, %{member: %{id: member.id}})
|
||||
|
||||
# Create second user and try to link to same member
|
||||
{:ok, user2} = Accounts.create_user(%{email: "user2@example.com"})
|
||||
|
||||
# Should fail because member is already linked
|
||||
assert {:error, %Ash.Error.Invalid{}} =
|
||||
Accounts.update_user(user2, %{member: %{id: member.id}})
|
||||
end
|
||||
|
||||
test "cannot change member link directly, must unlink first" do
|
||||
# Create user and link to first member
|
||||
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
|
||||
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|
||||
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member1.id}})
|
||||
|
||||
# Create second member
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Brown",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|
||||
# Try to directly change member link (should fail)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Accounts.update_user(linked_user, %{member: %{id: member2.id}})
|
||||
|
||||
# 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})
|
||||
|
||||
# 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, relinked_user} = Accounts.update_user(unlinked_user, %{member: %{id: member2.id}})
|
||||
|
||||
# Verify new link is established
|
||||
user_with_new_member = Ash.get!(Mv.Accounts.User, relinked_user.id, load: [:member])
|
||||
assert user_with_new_member.member.id == member2.id
|
||||
end
|
||||
end
|
||||
end
|
||||
222
test/membership/member_available_for_linking_test.exs
Normal file
222
test/membership/member_available_for_linking_test.exs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
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
|
||||
158
test/membership/member_fuzzy_search_linking_test.exs
Normal file
158
test/membership/member_fuzzy_search_linking_test.exs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
defmodule Mv.Membership.MemberFuzzySearchLinkingTest do
|
||||
@moduledoc """
|
||||
Tests fuzzy search in Member.available_for_linking action.
|
||||
Verifies PostgreSQL trigram matching for member search.
|
||||
"""
|
||||
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
describe "available_for_linking with fuzzy search" do
|
||||
test "finds member despite typo" do
|
||||
# Create member with specific name
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Jonathan",
|
||||
last_name: "Smith",
|
||||
email: "jonathan@example.com"
|
||||
})
|
||||
|
||||
# Search with typo
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{
|
||||
user_email: nil,
|
||||
search_query: "Jonatan"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
|
||||
# Should find Jonathan despite typo
|
||||
assert length(members) == 1
|
||||
assert hd(members).id == member.id
|
||||
end
|
||||
|
||||
test "finds member with partial match" do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Alexander",
|
||||
last_name: "Williams",
|
||||
email: "alex@example.com"
|
||||
})
|
||||
|
||||
# Search with partial
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{
|
||||
user_email: nil,
|
||||
search_query: "Alex"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
|
||||
# Should find Alexander
|
||||
assert length(members) == 1
|
||||
assert hd(members).id == member.id
|
||||
end
|
||||
|
||||
test "email match overrides fuzzy search" do
|
||||
# Create two members
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
})
|
||||
|
||||
{:ok, _member2} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
|
||||
# Search with user_email that matches member1, but search_query that would match member2
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{
|
||||
user_email: "john@example.com",
|
||||
search_query: "Jane"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
|
||||
# Apply email filter
|
||||
filtered_members = Mv.Membership.Member.filter_by_email_match(members, "john@example.com")
|
||||
|
||||
# Should only return member1 (email match takes precedence)
|
||||
assert length(filtered_members) == 1
|
||||
assert hd(filtered_members).id == member1.id
|
||||
end
|
||||
|
||||
test "limits to 10 results" do
|
||||
# Create 15 members with similar names
|
||||
for i <- 1..15 do
|
||||
Membership.create_member(%{
|
||||
first_name: "Test#{i}",
|
||||
last_name: "Member",
|
||||
email: "test#{i}@example.com"
|
||||
})
|
||||
end
|
||||
|
||||
# Search for "Test"
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{
|
||||
user_email: nil,
|
||||
search_query: "Test"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
|
||||
# Should return max 10 members
|
||||
assert length(members) == 10
|
||||
end
|
||||
|
||||
test "excludes linked members" do
|
||||
# Create member and link to user
|
||||
{:ok, member1} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member",
|
||||
email: "linked@example.com"
|
||||
})
|
||||
|
||||
{:ok, _user} =
|
||||
Accounts.create_user(%{
|
||||
email: "user@example.com",
|
||||
member: %{id: member1.id}
|
||||
})
|
||||
|
||||
# Create unlinked member
|
||||
{:ok, member2} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Unlinked",
|
||||
last_name: "Member",
|
||||
email: "unlinked@example.com"
|
||||
})
|
||||
|
||||
# Search for "Member"
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.for_read(:available_for_linking, %{
|
||||
user_email: nil,
|
||||
search_query: "Member"
|
||||
})
|
||||
|
||||
{:ok, members} = Ash.read(query, domain: Mv.Membership)
|
||||
|
||||
# Should only return unlinked member
|
||||
member_ids = Enum.map(members, & &1.id)
|
||||
refute member1.id in member_ids
|
||||
assert member2.id in member_ids
|
||||
end
|
||||
end
|
||||
end
|
||||
48
test/mv_web/user_live/form_debug2_test.exs
Normal file
48
test/mv_web/user_live/form_debug2_test.exs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
defmodule MvWeb.UserLive.FormDebug2Test do
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
describe "direct ash query test" do
|
||||
test "check if available_for_linking works in LiveView context" do
|
||||
# Create an unlinked member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
|
||||
IO.puts("\n=== Created member: #{inspect(member.id)} ===")
|
||||
|
||||
# Try the same query as in the LiveView
|
||||
user_email_str = "user@example.com"
|
||||
search_query_str = nil
|
||||
|
||||
IO.puts("\n=== Calling Ash.read with domain: Mv.Membership ===")
|
||||
|
||||
result =
|
||||
Ash.read(Mv.Membership.Member,
|
||||
domain: Mv.Membership,
|
||||
action: :available_for_linking,
|
||||
arguments: %{user_email: user_email_str, search_query: search_query_str}
|
||||
)
|
||||
|
||||
IO.puts("Result: #{inspect(result)}")
|
||||
|
||||
case result do
|
||||
{:ok, members} ->
|
||||
IO.puts("\n✓ Query succeeded, found #{length(members)} members")
|
||||
|
||||
Enum.each(members, fn m ->
|
||||
IO.puts(" - #{m.first_name} #{m.last_name} (#{m.email})")
|
||||
end)
|
||||
|
||||
# Apply filter
|
||||
filtered = Mv.Membership.Member.filter_by_email_match(members, user_email_str)
|
||||
IO.puts("\n✓ After filter_by_email_match: #{length(filtered)} members")
|
||||
|
||||
{:error, error} ->
|
||||
IO.puts("\n✗ Query failed: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
52
test/mv_web/user_live/form_debug_test.exs
Normal file
52
test/mv_web/user_live/form_debug_test.exs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
defmodule MvWeb.UserLive.FormDebugTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
# Helper to setup authenticated connection and live view
|
||||
defp setup_live_view(conn, path) do
|
||||
conn = conn_with_oidc_user(conn, %{email: "admin@example.com"})
|
||||
live(conn, path)
|
||||
end
|
||||
|
||||
describe "debug member loading" do
|
||||
test "check if members are loaded on mount", %{conn: conn} do
|
||||
# Create an unlinked member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
|
||||
# Create user without member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
|
||||
# Mount the form
|
||||
{:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
# Debug: Check what's in the HTML
|
||||
IO.puts("\n=== HTML OUTPUT ===")
|
||||
IO.puts(html)
|
||||
IO.puts("\n=== END HTML ===")
|
||||
|
||||
# Check socket assigns
|
||||
IO.puts("\n=== SOCKET ASSIGNS ===")
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
IO.puts("available_members: #{inspect(assigns[:available_members])}")
|
||||
IO.puts("show_member_dropdown: #{inspect(assigns[:show_member_dropdown])}")
|
||||
IO.puts("member_search_query: #{inspect(assigns[:member_search_query])}")
|
||||
IO.puts("user.member: #{inspect(assigns[:user].member)}")
|
||||
IO.puts("\n=== END ASSIGNS ===")
|
||||
|
||||
# Try to find the dropdown
|
||||
assert has_element?(view, "input[name='member_search']")
|
||||
|
||||
# Check if member is in the dropdown
|
||||
if has_element?(view, "div[data-member-id='#{member.id}']") do
|
||||
IO.puts("\n✓ Member found in dropdown")
|
||||
else
|
||||
IO.puts("\n✗ Member NOT found in dropdown")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
433
test/mv_web/user_live/form_member_linking_ui_test.exs
Normal file
433
test/mv_web/user_live/form_member_linking_ui_test.exs
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
defmodule MvWeb.UserLive.FormMemberLinkingUiTest do
|
||||
@moduledoc """
|
||||
UI tests for member linking in UserLive.Form.
|
||||
Tests dropdown behavior, fuzzy search, selection, and unlink workflow.
|
||||
Related to Issue #168.
|
||||
"""
|
||||
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
|
||||
# Helper to setup authenticated connection for admin
|
||||
defp setup_admin_conn(conn) do
|
||||
conn_with_oidc_user(conn, %{email: "admin@example.com"})
|
||||
end
|
||||
|
||||
describe "dropdown visibility" do
|
||||
test "dropdown hidden on mount", %{conn: conn} do
|
||||
conn = setup_admin_conn(conn)
|
||||
html = conn |> live(~p"/users/new") |> render()
|
||||
|
||||
# Dropdown should not be visible initially
|
||||
refute html =~ ~r/role="listbox"/
|
||||
end
|
||||
|
||||
test "dropdown shows after focus event", %{conn: conn} do
|
||||
conn = setup_admin_conn(conn)
|
||||
# Create unlinked members
|
||||
create_unlinked_members(3)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Focus the member search input
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_focus()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Dropdown should now be visible
|
||||
assert html =~ ~r/role="listbox"/
|
||||
end
|
||||
|
||||
test "dropdown shows top 10 unlinked members on focus", %{conn: conn} do
|
||||
# Create 15 unlinked members
|
||||
members = create_unlinked_members(15)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Focus the member search input
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_focus()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Should show only 10 members
|
||||
shown_members = Enum.take(members, 10)
|
||||
hidden_members = Enum.drop(members, 10)
|
||||
|
||||
for member <- shown_members do
|
||||
assert html =~ member.first_name
|
||||
end
|
||||
|
||||
for member <- hidden_members do
|
||||
refute html =~ member.first_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "fuzzy search" do
|
||||
test "finds member with exact name", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Jonathan",
|
||||
last_name: "Smith",
|
||||
email: "jonathan.smith@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Type exact name
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_change(%{"member_search_query" => "Jonathan"})
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "Jonathan"
|
||||
assert html =~ "Smith"
|
||||
end
|
||||
|
||||
test "finds member with typo (Jon finds Jonathan)", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Jonathan",
|
||||
last_name: "Smith",
|
||||
email: "jonathan.smith@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Type with typo
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_change(%{"member_search_query" => "Jon"})
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Fuzzy search should find Jonathan
|
||||
assert html =~ "Jonathan"
|
||||
assert html =~ "Smith"
|
||||
end
|
||||
|
||||
test "finds member with partial substring", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Alexander",
|
||||
last_name: "Williams",
|
||||
email: "alex@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Type partial
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_change(%{"member_search_query" => "lex"})
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "Alexander"
|
||||
end
|
||||
|
||||
test "returns empty for no matches", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Type something that doesn't match
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_change(%{"member_search_query" => "zzzzzzz"})
|
||||
|
||||
html = render(view)
|
||||
|
||||
refute html =~ "John"
|
||||
end
|
||||
end
|
||||
|
||||
describe "member selection" do
|
||||
test "input field shows selected member name", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Focus and search
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_focus()
|
||||
|
||||
# Select member
|
||||
view
|
||||
|> element("[data-member-id='#{member.id}']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Input field should show member name
|
||||
assert html =~ "Alice Johnson"
|
||||
end
|
||||
|
||||
test "confirmation box appears", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Bob",
|
||||
last_name: "Williams",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Focus input
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_focus()
|
||||
|
||||
# Select member
|
||||
view
|
||||
|> element("[data-member-id='#{member.id}']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Confirmation box should appear
|
||||
assert html =~ "Selected"
|
||||
assert html =~ "Bob Williams"
|
||||
assert html =~ "Save to confirm linking"
|
||||
end
|
||||
|
||||
test "hidden input stores member ID", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Charlie",
|
||||
last_name: "Brown",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Focus input
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_focus()
|
||||
|
||||
# Select member
|
||||
view
|
||||
|> element("[data-member-id='#{member.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Check socket assigns (member ID should be stored)
|
||||
assert view |> element("#user-form") |> has_element?()
|
||||
end
|
||||
end
|
||||
|
||||
describe "email handling" do
|
||||
test "links user and member with identical email successfully", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "David",
|
||||
last_name: "Miller",
|
||||
email: "david@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Fill user form with same email
|
||||
view
|
||||
|> form("#user-form", user: %{email: "david@example.com"})
|
||||
|> render_change()
|
||||
|
||||
# Focus input
|
||||
view
|
||||
|> element("#member-search-input")
|
||||
|> render_focus()
|
||||
|
||||
# Select member
|
||||
view
|
||||
|> element("[data-member-id='#{member.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Submit form
|
||||
view
|
||||
|> form("#user-form", user: %{email: "david@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
# Should succeed without errors
|
||||
assert_redirected(view, ~p"/users")
|
||||
end
|
||||
|
||||
test "shows info when member has same email", %{conn: conn} do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Emma",
|
||||
last_name: "Davis",
|
||||
email: "emma@example.com"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/new")
|
||||
|
||||
# Fill user form with same email
|
||||
view
|
||||
|> form("#user-form", user: %{email: "emma@example.com"})
|
||||
|> render_change()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Should show info message about email conflict
|
||||
assert html =~ "A member with this email already exists"
|
||||
end
|
||||
end
|
||||
|
||||
describe "unlink workflow" do
|
||||
test "unlink hides dropdown", %{conn: conn} do
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Frank",
|
||||
last_name: "Wilson",
|
||||
email: "frank@example.com"
|
||||
})
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
email: "frank@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
# Click unlink button
|
||||
view
|
||||
|> element("button[phx-click='unlink_member']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Dropdown should not be visible
|
||||
refute html =~ ~r/role="listbox"/
|
||||
end
|
||||
|
||||
test "unlink shows warning", %{conn: conn} do
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Grace",
|
||||
last_name: "Taylor",
|
||||
email: "grace@example.com"
|
||||
})
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
email: "grace@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
# Click unlink button
|
||||
view
|
||||
|> element("button[phx-click='unlink_member']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Should show warning
|
||||
assert html =~ "Unlinking scheduled"
|
||||
assert html =~ "Cannot select new member until saved"
|
||||
end
|
||||
|
||||
test "unlink disables input", %{conn: conn} do
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Henry",
|
||||
last_name: "Anderson",
|
||||
email: "henry@example.com"
|
||||
})
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
email: "henry@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
# Click unlink button
|
||||
view
|
||||
|> element("button[phx-click='unlink_member']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Input should be disabled
|
||||
assert html =~ ~r/disabled/
|
||||
end
|
||||
|
||||
test "save re-enables member selection", %{conn: conn} do
|
||||
# Create user with linked member
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "Isabel",
|
||||
last_name: "Martinez",
|
||||
email: "isabel@example.com"
|
||||
})
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.create_user(%{
|
||||
email: "isabel@example.com",
|
||||
member: %{id: member.id}
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
# Click unlink button
|
||||
view
|
||||
|> element("button[phx-click='unlink_member']")
|
||||
|> render_click()
|
||||
|
||||
# Submit form
|
||||
view
|
||||
|> form("#user-form")
|
||||
|> render_submit()
|
||||
|
||||
# Navigate back to edit
|
||||
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Should now show member selection input (not disabled)
|
||||
assert html =~ "member-search-input"
|
||||
refute html =~ "Unlinking scheduled"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
defp create_unlinked_members(count) do
|
||||
for i <- 1..count do
|
||||
{:ok, member} =
|
||||
Membership.create_member(%{
|
||||
first_name: "FirstName#{i}",
|
||||
last_name: "LastName#{i}",
|
||||
email: "member#{i}@example.com"
|
||||
})
|
||||
|
||||
member
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -281,4 +281,101 @@ defmodule MvWeb.UserLive.FormTest do
|
|||
assert edit_html =~ "Change Password"
|
||||
end
|
||||
end
|
||||
|
||||
describe "member linking - display" do
|
||||
test "shows linked member with unlink button when user has member", %{conn: conn} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
})
|
||||
|
||||
# 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}})
|
||||
|
||||
# Load form
|
||||
{:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
# Should show linked member section
|
||||
assert html =~ "Linked Member"
|
||||
assert html =~ "John Doe"
|
||||
assert html =~ "user@example.com"
|
||||
assert has_element?(view, "button[phx-click='unlink_member']")
|
||||
assert html =~ "Unlink Member"
|
||||
end
|
||||
|
||||
test "shows member search field when user has no member", %{conn: conn} do
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
{:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
# Should show member search section
|
||||
assert html =~ "Linked Member"
|
||||
assert has_element?(view, "input[phx-change='search_members']")
|
||||
# Should not show unlink button
|
||||
refute has_element?(view, "button[phx-click='unlink_member']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "member linking - workflow" do
|
||||
test "selecting member and saving links member to user", %{conn: conn} do
|
||||
# Create unlinked member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
email: "jane@example.com"
|
||||
})
|
||||
|
||||
# Create user without member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
# Select member
|
||||
view |> element("div[data-member-id='#{member.id}']") |> render_click()
|
||||
|
||||
# Submit form
|
||||
view
|
||||
|> form("#user-form", user: %{email: "user@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
assert_redirected(view, "/users")
|
||||
|
||||
# Verify member is linked
|
||||
updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member])
|
||||
assert updated_user.member.id == member.id
|
||||
end
|
||||
|
||||
test "unlinking member and saving removes member from user", %{conn: conn} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Bob",
|
||||
last_name: "Wilson",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|
||||
# Create user linked to member
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
{:ok, _} = Mv.Accounts.update_user(user, %{member: %{id: member.id}})
|
||||
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
# Click unlink button
|
||||
view |> element("button[phx-click='unlink_member']") |> render_click()
|
||||
|
||||
# Submit form
|
||||
view
|
||||
|> form("#user-form", user: %{email: "user@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
assert_redirected(view, "/users")
|
||||
|
||||
# Verify member is unlinked
|
||||
updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member])
|
||||
assert is_nil(updated_user.member)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -410,4 +410,35 @@ defmodule MvWeb.UserLive.IndexTest do
|
|||
assert html =~ long_email
|
||||
end
|
||||
end
|
||||
|
||||
describe "member linking display" do
|
||||
test "displays linked member name in user list", %{conn: conn} do
|
||||
# Create member
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Alice",
|
||||
last_name: "Johnson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|
||||
# 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}})
|
||||
|
||||
# Create another user without member
|
||||
_unlinked_user = create_test_user(%{email: "unlinked@example.com"})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
# Should show linked member name
|
||||
assert html =~ "Alice Johnson"
|
||||
# Should show user email
|
||||
assert html =~ "user@example.com"
|
||||
# Should show unlinked user
|
||||
assert html =~ "unlinked@example.com"
|
||||
# Should show "No member linked" or similar for unlinked user
|
||||
assert html =~ "No member linked"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue