From 7df34ce5eab0104e85be064f2dea9436b90c2082 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 16 Oct 2025 16:57:34 +0200 Subject: [PATCH] email sync tests --- test/accounts/email_sync_edge_cases_test.exs | 93 ++++++++++ test/accounts/user_email_sync_test.exs | 169 +++++++++++++++++++ test/membership/member_email_sync_test.exs | 127 ++++++++++++++ 3 files changed, 389 insertions(+) create mode 100644 test/accounts/email_sync_edge_cases_test.exs create mode 100644 test/accounts/user_email_sync_test.exs create mode 100644 test/membership/member_email_sync_test.exs diff --git a/test/accounts/email_sync_edge_cases_test.exs b/test/accounts/email_sync_edge_cases_test.exs new file mode 100644 index 0000000..b872235 --- /dev/null +++ b/test/accounts/email_sync_edge_cases_test.exs @@ -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 diff --git a/test/accounts/user_email_sync_test.exs b/test/accounts/user_email_sync_test.exs new file mode 100644 index 0000000..6d08d61 --- /dev/null +++ b/test/accounts/user_email_sync_test.exs @@ -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 diff --git a/test/membership/member_email_sync_test.exs b/test/membership/member_email_sync_test.exs new file mode 100644 index 0000000..eeef210 --- /dev/null +++ b/test/membership/member_email_sync_test.exs @@ -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