email sync tests

This commit is contained in:
Moritz 2025-10-16 16:57:34 +02:00
parent b94a4a65d3
commit 91c5e17994
Signed by: moritz
GPG key ID: 1020A035E5DD0824
3 changed files with 389 additions and 0 deletions

View file

@ -0,0 +1,93 @@
defmodule Mv.Accounts.EmailSyncEdgeCasesTest do
@moduledoc """
Edge case tests for email synchronization between User and Member.
Tests various boundary conditions and validation scenarios.
"""
use Mv.DataCase, async: false
alias Mv.Accounts
alias Mv.Membership
describe "Email sync edge cases" do
@valid_user_attrs %{
email: "user@example.com"
}
@valid_member_attrs %{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
}
test "simultaneous email updates use user email as source of truth" do
# Create linked user and member
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, user} =
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
# Verify link and initial sync
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id)
assert synced_member.email == "user@example.com"
# Scenario: Both emails are updated "simultaneously"
# In practice, this tests that when a member email is updated,
# it syncs to user, and user remains the source of truth
# Update member email first
{:ok, _updated_member} =
Membership.update_member(member, %{email: "member-new@example.com"})
# Verify it synced to user
{:ok, user_after_member_update} = Ash.get(Mv.Accounts.User, user.id)
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"})
# Reload both
{:ok, final_user} = Ash.get(Mv.Accounts.User, user.id)
{:ok, final_member} = Ash.get(Mv.Membership.Member, member.id)
# 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 that invalid emails are rejected for both resources
# Invalid email for user
invalid_user_result = Accounts.create_user(%{email: "not-an-email"})
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)
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)
end
test "identity constraints prevent duplicate emails" do
# Create first user with an email
{:ok, user1} = Accounts.create_user(%{email: "duplicate@example.com"})
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"})
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)
assert member1.email == "member-dup@example.com"
# Try to create second member with same email - should fail
result2 = Membership.create_member(member_attrs)
assert {:error, %Ash.Error.Invalid{}} = result2
end
end
end

View file

@ -0,0 +1,169 @@
defmodule Mv.Accounts.UserEmailSyncTest do
@moduledoc """
Tests for email synchronization from User to Member.
When a user and member are linked, email changes should sync bidirectionally.
User.email is the source of truth when linking occurs.
"""
use Mv.DataCase, async: false
alias Mv.Accounts
alias Mv.Membership
describe "User email synchronization to linked Member" do
@valid_user_attrs %{
email: "user@example.com"
}
@valid_member_attrs %{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
}
test "updating user email syncs to linked member" do
# Create a member
{:ok, member} = Membership.create_member(@valid_member_attrs)
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}))
# Verify initial state - member email should be overridden by user email
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id)
assert member_after_link.email == "user@example.com"
# Update user email
{:ok, updated_user} = Accounts.update_user(user, %{email: "newemail@example.com"})
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)
assert synced_member.email == "newemail@example.com"
end
test "creating user linked to member overrides member email" do
# Create a member with their own email
{:ok, member} = Membership.create_member(@valid_member_attrs)
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}))
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)
assert updated_member.email == "user@example.com"
end
test "linking user to existing member syncs user email to member" do
# Create a standalone member
{:ok, member} = Membership.create_member(@valid_member_attrs)
assert member.email == "member@example.com"
# Create a standalone user
{:ok, user} = Accounts.create_user(@valid_user_attrs)
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}})
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)
assert synced_member.email == "user@example.com"
end
test "updating user email when no member linked does not error" do
# Create a standalone user without member link
{:ok, user} = Accounts.create_user(@valid_user_attrs)
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"})
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
# Create member
{:ok, member} = Membership.create_member(@valid_member_attrs)
# Create user linked to member
{:ok, user} =
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
assert user.member_id == member.id
# Verify member email was synced
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id)
assert synced_member.email == "user@example.com"
# Unlink user from member
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil})
assert unlinked_user.member_id == nil
# Member email should remain unchanged after unlinking
{:ok, member_after_unlink} = Ash.get(Mv.Membership.Member, member.id)
assert member_after_unlink.email == "user@example.com"
end
end
describe "AshAuthentication compatibility" do
test "AshAuthentication password strategy still works with email" do
# This test ensures that the email field remains accessible for password auth
email = "test@example.com"
password = "securepassword123"
# Create user with password strategy (simulating registration)
{:ok, user} =
Mv.Accounts.User
|> Ash.Changeset.for_create(:register_with_password, %{
email: email,
password: password
})
|> Ash.create()
assert to_string(user.email) == email
assert user.hashed_password != nil
# Verify we can sign in with email
{:ok, signed_in_user} =
Mv.Accounts.User
|> Ash.Query.for_read(:sign_in_with_password, %{
email: email,
password: password
})
|> Ash.read_one()
assert signed_in_user.id == user.id
assert to_string(signed_in_user.email) == email
end
test "AshAuthentication OIDC strategy still works with email" do
# This test ensures the OIDC flow can still set email
user_info = %{
"preferred_username" => "oidc@example.com",
"sub" => "oidc-user-123"
}
oauth_tokens = %{"access_token" => "mock_token"}
# Simulate OIDC registration
{:ok, user} =
Mv.Accounts.User
|> Ash.Changeset.for_create(:register_with_rauthy, %{
user_info: user_info,
oauth_tokens: oauth_tokens
})
|> Ash.create()
assert to_string(user.email) == "oidc@example.com"
assert user.oidc_id == "oidc-user-123"
end
end
end

View file

@ -0,0 +1,127 @@
defmodule Mv.Membership.MemberEmailSyncTest do
@moduledoc """
Tests for email synchronization from Member to User.
When a member and user are linked, email changes should sync bidirectionally.
User.email is the source of truth when linking occurs.
"""
use Mv.DataCase, async: false
alias Mv.Accounts
alias Mv.Membership
describe "Member email synchronization to linked User" do
@valid_user_attrs %{
email: "user@example.com"
}
@valid_member_attrs %{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
}
test "updating member email syncs to linked user" do
# Create a user
{:ok, user} = Accounts.create_user(@valid_user_attrs)
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}))
# Verify initial state - member email should be overridden by user email
{:ok, member_after_create} = Ash.get(Mv.Membership.Member, member.id)
assert member_after_create.email == "user@example.com"
# Update member email
{:ok, updated_member} =
Membership.update_member(member, %{email: "newmember@example.com"})
assert updated_member.email == "newmember@example.com"
# Verify user email was also updated
{:ok, synced_user} = Ash.get(Mv.Accounts.User, user.id)
assert to_string(synced_user.email) == "newmember@example.com"
end
test "creating member linked to user syncs user email to member" do
# Create a user with their own email
{:ok, user} = Accounts.create_user(@valid_user_attrs)
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}))
# 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])
assert loaded_member.user.id == user.id
end
test "linking member to existing user syncs user email to member" do
# Create a standalone user
{:ok, user} = Accounts.create_user(@valid_user_attrs)
assert to_string(user.email) == "user@example.com"
# Create a standalone member
{:ok, member} = Membership.create_member(@valid_member_attrs)
assert member.email == "member@example.com"
# Link the member to the user
{:ok, linked_member} = Membership.update_member(member, %{user: %{id: user.id}})
# Verify the link
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, linked_member.id, load: [:user])
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
# Create a standalone member without user link
{:ok, member} = Membership.create_member(@valid_member_attrs)
assert member.email == "member@example.com"
# Load to verify no user link
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert loaded_member.user == nil
# Update member email - should work fine without error
{:ok, updated_member} =
Membership.update_member(member, %{email: "newemail@example.com"})
assert updated_member.email == "newemail@example.com"
end
test "unlinking member from user does not sync email" do
# Create user
{:ok, user} = Accounts.create_user(@valid_user_attrs)
# Create member linked to user
{:ok, member} =
Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}))
# Verify member email was synced to user email
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id)
assert synced_member.email == "user@example.com"
# Verify link exists
{:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert loaded_member.user != nil
# Unlink member from user
{:ok, unlinked_member} = Membership.update_member(member, %{user: nil})
# Verify unlink
{:ok, loaded_unlinked} = Ash.get(Mv.Membership.Member, unlinked_member.id, load: [:user])
assert loaded_unlinked.user == nil
# User email should remain unchanged after unlinking
{:ok, user_after_unlink} = Ash.get(Mv.Accounts.User, user.id)
assert to_string(user_after_unlink.email) == "user@example.com"
end
end
end