defmodule Mv.Accounts.EmailUniquenessTest do use Mv.DataCase, async: false alias Mv.Accounts alias Mv.Membership describe "Email uniqueness validation - Creation" do test "CAN create member with existing unlinked user email" do # Create a user with email {:ok, _user} = Accounts.create_user(%{ email: "existing@example.com" }) # Create member with same email - should succeed {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "existing@example.com" }) assert to_string(member.email) == "existing@example.com" end test "CAN create user with existing unlinked member email" do # Create a member with email {:ok, _member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "existing@example.com" }) # Create user with same email - should succeed {:ok, user} = Accounts.create_user(%{ email: "existing@example.com" }) 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 # Create a user with email {:ok, _user} = Accounts.create_user(%{ email: "existing_user@example.com" }) # Create an unlinked member with different email {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "member@example.com" }) # Change member email to existing user email - should succeed (member is unlinked) {:ok, updated_member} = Membership.update_member(member, %{ email: "existing_user@example.com" }) 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 # Create a member with email {:ok, _member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "existing_member@example.com" }) # Create an unlinked user with different email {:ok, user} = Accounts.create_user(%{ email: "user@example.com" }) # Change user email to existing member email - should succeed (user is unlinked) {:ok, updated_user} = Accounts.update_user(user, %{ email: "existing_member@example.com" }) 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 # Create a user and link it to a member - this makes the user "linked" {:ok, user} = Accounts.create_user(%{ email: "linked_user@example.com" }) {:ok, _member_a} = Membership.create_member(%{ first_name: "Member", last_name: "A", email: "temp@example.com", user: %{id: user.id} }) # Create an unlinked member with different email {:ok, member_b} = Membership.create_member(%{ first_name: "Member", last_name: "B", email: "member_b@example.com" }) # Try to change unlinked member's email to linked user's email - should fail result = Membership.update_member(member_b, %{ email: "linked_user@example.com" }) assert {:error, %Ash.Error.Invalid{} = error} = result assert error.errors |> Enum.any?(fn e -> e.field == :email and (String.contains?(e.message, "already") or String.contains?(e.message, "used")) end) end test "unlinked user email CANNOT be changed to an existing linked member email" do # Create a user and link it to a member - this makes the member "linked" {:ok, user_a} = Accounts.create_user(%{ email: "user_a@example.com" }) {:ok, _member_a} = Membership.create_member(%{ first_name: "Member", last_name: "A", email: "temp@example.com", user: %{id: user_a.id} }) # 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) linked_member_email = to_string(user_a_with_member.member.email) # Create an unlinked user with different email {:ok, user_b} = Accounts.create_user(%{ email: "user_b@example.com" }) # Try to change unlinked user's email to linked member's email - should fail result = Accounts.update_user(user_b, %{ email: linked_member_email }) assert {:error, %Ash.Error.Invalid{} = error} = result assert error.errors |> Enum.any?(fn e -> e.field == :email and (String.contains?(e.message, "already") or String.contains?(e.message, "used")) end) end end describe "Email uniqueness validation - Creating with linked emails" do test "CANNOT create member with existing linked user email" do # Create a user and link it to a member {:ok, user} = Accounts.create_user(%{ email: "linked@example.com" }) {:ok, _member} = Membership.create_member(%{ first_name: "First", last_name: "Member", email: "temp@example.com", user: %{id: user.id} }) # Try to create a new member with the linked user's email - should fail result = Membership.create_member(%{ first_name: "Second", last_name: "Member", email: "linked@example.com" }) assert {:error, %Ash.Error.Invalid{} = error} = result assert error.errors |> Enum.any?(fn e -> e.field == :email and (String.contains?(e.message, "already") or String.contains?(e.message, "used")) end) end test "CANNOT create user with existing linked member email" do # Create a user and link it to a member {:ok, user} = Accounts.create_user(%{ email: "user@example.com" }) {:ok, _member} = Membership.create_member(%{ first_name: "Member", last_name: "One", email: "temp@example.com", user: %{id: user.id} }) # 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) 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(%{ email: linked_member_email }) assert {:error, %Ash.Error.Invalid{} = error} = result assert error.errors |> Enum.any?(fn e -> e.field == :email and (String.contains?(e.message, "already") or String.contains?(e.message, "used")) end) end end describe "Email uniqueness validation - Updating linked entities" do test "linked member email CANNOT be changed to an existing user email" do # Create a user with email {:ok, _other_user} = Accounts.create_user(%{ email: "other_user@example.com" }) # Create a user and link it to a member {:ok, user} = Accounts.create_user(%{ email: "user@example.com" }) {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "temp@example.com", user: %{id: user.id} }) # Try to change linked member's email to other user's email - should fail result = Membership.update_member(member, %{ email: "other_user@example.com" }) assert {:error, %Ash.Error.Invalid{} = error} = result assert error.errors |> Enum.any?(fn e -> e.field == :email and (String.contains?(e.message, "already") or String.contains?(e.message, "used")) end) end test "linked user email CANNOT be changed to an existing member email" do # Create a member with email {:ok, _other_member} = Membership.create_member(%{ first_name: "Jane", last_name: "Doe", email: "other_member@example.com" }) # Create a user and link it to a member {:ok, user} = Accounts.create_user(%{ email: "user@example.com" }) {:ok, _member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "temp@example.com", user: %{id: user.id} }) # Reload user to get updated member_id {:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id) # Try to change linked user's email to other member's email - should fail result = Accounts.update_user(user_reloaded, %{ email: "other_member@example.com" }) assert {:error, %Ash.Error.Invalid{} = error} = result assert error.errors |> Enum.any?(fn e -> e.field == :email and (String.contains?(e.message, "already") or String.contains?(e.message, "used")) end) end end describe "Email uniqueness validation - Linking" do test "CANNOT link user to member if user email is already used by another unlinked member" do # Create a member with email {:ok, _other_member} = Membership.create_member(%{ first_name: "Jane", last_name: "Doe", email: "duplicate@example.com" }) # Create a user with same email {:ok, user} = Accounts.create_user(%{ email: "duplicate@example.com" }) # Create a member to link with the user {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Smith", email: "john@example.com" }) # Try to link user to member - should fail because user.email is already used by other_member result = Accounts.update_user(user, %{ member: %{id: member.id} }) assert {:error, %Ash.Error.Invalid{} = error} = result assert error.errors |> Enum.any?(fn e -> e.field == :email and (String.contains?(e.message, "already") or String.contains?(e.message, "used")) end) end test "CAN link member to user even if member email is used by another user (member email gets overridden)" do # Create a user with email {:ok, _other_user} = Accounts.create_user(%{ email: "duplicate@example.com" }) # Create a member with same email {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "duplicate@example.com" }) # Create a user to link with the member {:ok, user} = Accounts.create_user(%{ email: "user@example.com" }) # Link member to user - should succeed because member.email will be overridden {:ok, updated_member} = Membership.update_member(member, %{ user: %{id: user.id} }) # Member email should now be the same as user email {:ok, member_reloaded} = Ash.get(Mv.Membership.Member, updated_member.id) 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 # Create a user {:ok, user} = Accounts.create_user(%{ email: "user@example.com" }) # Create a member linked to this user # The override change will set member.email = user.email automatically {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "member@example.com", user: %{id: user.id} }) # 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) assert member_after_link.email == "user@example.com" end test "user email syncs to linked member without validation error" do # Create a member {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "member@example.com" }) # Create a user linked to this member # The override change will set member.email = user.email automatically {:ok, _user} = Accounts.create_user(%{ email: "user@example.com", member: %{id: member.id} }) # 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) assert member_after_link.email == "user@example.com" end test "two unlinked users cannot have the same email" do # Create first user {:ok, _user1} = Accounts.create_user(%{ email: "duplicate@example.com" }) # Try to create second user with same email result = Accounts.create_user(%{ email: "duplicate@example.com" }) assert {:error, %Ash.Error.Invalid{}} = result end test "two unlinked members cannot have the same email (members have unique constraint)" do # Create first member {:ok, _member1} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "duplicate@example.com" }) # Try to create second member with same email - should fail result = Membership.create_member(%{ first_name: "Jane", last_name: "Smith", email: "duplicate@example.com" }) assert {:error, %Ash.Error.Invalid{}} = result # Members DO have a unique email constraint at database level end end end