Add actor parameter to all tests requiring authorization

This commit adds actor: system_actor to all Ash operations in tests that
require authorization.
This commit is contained in:
Moritz 2026-01-23 20:00:24 +01:00
parent 686f69c9e9
commit 0f48a9b15a
Signed by: moritz
GPG key ID: 1020A035E5DD0824
75 changed files with 4686 additions and 2859 deletions

View file

@ -7,6 +7,11 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do
alias Mv.Accounts
alias Mv.Membership
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
describe "Email sync edge cases" do
@valid_user_attrs %{
email: "user@example.com"
@ -18,15 +23,15 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do
email: "member@example.com"
}
test "simultaneous email updates use user email as source of truth" do
test "simultaneous email updates use user email as source of truth", %{actor: actor} do
# Create linked user and member
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
{:ok, user} =
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
# Verify link and initial sync
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id)
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert synced_member.email == "user@example.com"
# Scenario: Both emails are updated "simultaneously"
@ -35,58 +40,60 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do
# Update member email first
{:ok, _updated_member} =
Membership.update_member(member, %{email: "member-new@example.com"})
Membership.update_member(member, %{email: "member-new@example.com"}, actor: actor)
# Verify it synced to user
{:ok, user_after_member_update} = Ash.get(Mv.Accounts.User, user.id)
{:ok, user_after_member_update} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
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"})
Accounts.update_user(user_after_member_update, %{email: "user-final@example.com"},
actor: actor
)
# Reload both
{:ok, final_user} = Ash.get(Mv.Accounts.User, user.id)
{:ok, final_member} = Ash.get(Mv.Membership.Member, member.id)
{:ok, final_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
{:ok, final_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
# 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 "email validation works for both user and member", %{actor: actor} do
# Test that invalid emails are rejected for both resources
# Invalid email for user
invalid_user_result = Accounts.create_user(%{email: "not-an-email"})
invalid_user_result = Accounts.create_user(%{email: "not-an-email"}, actor: actor)
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)
invalid_member_result = Membership.create_member(invalid_member_attrs, actor: actor)
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)
{:ok, _user} = Accounts.create_user(@valid_user_attrs, actor: actor)
{:ok, _member} = Membership.create_member(@valid_member_attrs, actor: actor)
end
test "identity constraints prevent duplicate emails" do
test "identity constraints prevent duplicate emails", %{actor: actor} do
# Create first user with an email
{:ok, user1} = Accounts.create_user(%{email: "duplicate@example.com"})
{:ok, user1} = Accounts.create_user(%{email: "duplicate@example.com"}, actor: actor)
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"})
result = Accounts.create_user(%{email: "duplicate@example.com"}, actor: actor)
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)
{:ok, member1} = Membership.create_member(member_attrs, actor: actor)
assert member1.email == "member-dup@example.com"
# Try to create second member with same email - should fail
result2 = Membership.create_member(member_attrs)
result2 = Membership.create_member(member_attrs, actor: actor)
assert {:error, %Ash.Error.Invalid{}} = result2
end
end

View file

@ -4,121 +4,177 @@ defmodule Mv.Accounts.EmailUniquenessTest do
alias Mv.Accounts
alias Mv.Membership
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
describe "Email uniqueness validation - Creation" do
test "CAN create member with existing unlinked user email" do
test "CAN create member with existing unlinked user email", %{actor: actor} do
# Create a user with email
{:ok, _user} =
Accounts.create_user(%{
email: "existing@example.com"
})
Accounts.create_user(
%{
email: "existing@example.com"
},
actor: actor
)
# Create member with same email - should succeed
{:ok, member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "existing@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "existing@example.com"
},
actor: actor
)
assert to_string(member.email) == "existing@example.com"
end
test "CAN create user with existing unlinked member email" do
test "CAN create user with existing unlinked member email", %{actor: actor} do
# Create a member with email
{:ok, _member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "existing@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "existing@example.com"
},
actor: actor
)
# Create user with same email - should succeed
{:ok, user} =
Accounts.create_user(%{
email: "existing@example.com"
})
Accounts.create_user(
%{
email: "existing@example.com"
},
actor: actor
)
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
test "unlinked member email CAN be changed to an existing unlinked user email", %{
actor: actor
} do
# Create a user with email
{:ok, _user} =
Accounts.create_user(%{
email: "existing_user@example.com"
})
Accounts.create_user(
%{
email: "existing_user@example.com"
},
actor: actor
)
# Create an unlinked member with different email
{:ok, member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
},
actor: actor
)
# Change member email to existing user email - should succeed (member is unlinked)
{:ok, updated_member} =
Membership.update_member(member, %{
email: "existing_user@example.com"
})
Membership.update_member(
member,
%{
email: "existing_user@example.com"
},
actor: actor
)
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
test "unlinked user email CAN be changed to an existing unlinked member email", %{
actor: actor
} do
# Create a member with email
{:ok, _member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "existing_member@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "existing_member@example.com"
},
actor: actor
)
# Create an unlinked user with different email
{:ok, user} =
Accounts.create_user(%{
email: "user@example.com"
})
Accounts.create_user(
%{
email: "user@example.com"
},
actor: actor
)
# Change user email to existing member email - should succeed (user is unlinked)
{:ok, updated_user} =
Accounts.update_user(user, %{
email: "existing_member@example.com"
})
Accounts.update_user(
user,
%{
email: "existing_member@example.com"
},
actor: actor
)
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
test "unlinked member email CANNOT be changed to an existing linked user email", %{
actor: actor
} 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"
})
Accounts.create_user(
%{
email: "linked_user@example.com"
},
actor: actor
)
{:ok, _member_a} =
Membership.create_member(%{
first_name: "Member",
last_name: "A",
email: "temp@example.com",
user: %{id: user.id}
})
Membership.create_member(
%{
first_name: "Member",
last_name: "A",
email: "temp@example.com",
user: %{id: user.id}
},
actor: actor
)
# Create an unlinked member with different email
{:ok, member_b} =
Membership.create_member(%{
first_name: "Member",
last_name: "B",
email: "member_b@example.com"
})
Membership.create_member(
%{
first_name: "Member",
last_name: "B",
email: "member_b@example.com"
},
actor: actor
)
# 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"
})
Membership.update_member(
member_b,
%{
email: "linked_user@example.com"
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{} = error} = result
@ -129,37 +185,52 @@ defmodule Mv.Accounts.EmailUniquenessTest do
end)
end
test "unlinked user email CANNOT be changed to an existing linked member email" do
test "unlinked user email CANNOT be changed to an existing linked member email", %{
actor: actor
} 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"
})
Accounts.create_user(
%{
email: "user_a@example.com"
},
actor: actor
)
{:ok, _member_a} =
Membership.create_member(%{
first_name: "Member",
last_name: "A",
email: "temp@example.com",
user: %{id: user_a.id}
})
Membership.create_member(
%{
first_name: "Member",
last_name: "A",
email: "temp@example.com",
user: %{id: user_a.id}
},
actor: actor
)
# 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)
{:ok, user_a_reloaded} = Ash.get(Mv.Accounts.User, user_a.id, actor: actor)
{:ok, user_a_with_member} = Ash.load(user_a_reloaded, :member, actor: actor)
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"
})
Accounts.create_user(
%{
email: "user_b@example.com"
},
actor: actor
)
# Try to change unlinked user's email to linked member's email - should fail
result =
Accounts.update_user(user_b, %{
email: linked_member_email
})
Accounts.update_user(
user_b,
%{
email: linked_member_email
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{} = error} = result
@ -172,28 +243,37 @@ defmodule Mv.Accounts.EmailUniquenessTest do
end
describe "Email uniqueness validation - Creating with linked emails" do
test "CANNOT create member with existing linked user email" do
test "CANNOT create member with existing linked user email", %{actor: actor} do
# Create a user and link it to a member
{:ok, user} =
Accounts.create_user(%{
email: "linked@example.com"
})
Accounts.create_user(
%{
email: "linked@example.com"
},
actor: actor
)
{:ok, _member} =
Membership.create_member(%{
first_name: "First",
last_name: "Member",
email: "temp@example.com",
user: %{id: user.id}
})
Membership.create_member(
%{
first_name: "First",
last_name: "Member",
email: "temp@example.com",
user: %{id: user.id}
},
actor: actor
)
# 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"
})
Membership.create_member(
%{
first_name: "Second",
last_name: "Member",
email: "linked@example.com"
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{} = error} = result
@ -204,31 +284,40 @@ defmodule Mv.Accounts.EmailUniquenessTest do
end)
end
test "CANNOT create user with existing linked member email" do
test "CANNOT create user with existing linked member email", %{actor: actor} do
# Create a user and link it to a member
{:ok, user} =
Accounts.create_user(%{
email: "user@example.com"
})
Accounts.create_user(
%{
email: "user@example.com"
},
actor: actor
)
{:ok, _member} =
Membership.create_member(%{
first_name: "Member",
last_name: "One",
email: "temp@example.com",
user: %{id: user.id}
})
Membership.create_member(
%{
first_name: "Member",
last_name: "One",
email: "temp@example.com",
user: %{id: user.id}
},
actor: actor
)
# 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)
{:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
{:ok, user_with_member} = Ash.load(user_reloaded, :member, actor: actor)
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
})
Accounts.create_user(
%{
email: linked_member_email
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{} = error} = result
@ -241,32 +330,45 @@ defmodule Mv.Accounts.EmailUniquenessTest do
end
describe "Email uniqueness validation - Updating linked entities" do
test "linked member email CANNOT be changed to an existing user email" do
test "linked member email CANNOT be changed to an existing user email", %{actor: actor} do
# Create a user with email
{:ok, _other_user} =
Accounts.create_user(%{
email: "other_user@example.com"
})
Accounts.create_user(
%{
email: "other_user@example.com"
},
actor: actor
)
# Create a user and link it to a member
{:ok, user} =
Accounts.create_user(%{
email: "user@example.com"
})
Accounts.create_user(
%{
email: "user@example.com"
},
actor: actor
)
{:ok, member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "temp@example.com",
user: %{id: user.id}
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "temp@example.com",
user: %{id: user.id}
},
actor: actor
)
# Try to change linked member's email to other user's email - should fail
result =
Membership.update_member(member, %{
email: "other_user@example.com"
})
Membership.update_member(
member,
%{
email: "other_user@example.com"
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{} = error} = result
@ -277,37 +379,50 @@ defmodule Mv.Accounts.EmailUniquenessTest do
end)
end
test "linked user email CANNOT be changed to an existing member email" do
test "linked user email CANNOT be changed to an existing member email", %{actor: actor} do
# Create a member with email
{:ok, _other_member} =
Membership.create_member(%{
first_name: "Jane",
last_name: "Doe",
email: "other_member@example.com"
})
Membership.create_member(
%{
first_name: "Jane",
last_name: "Doe",
email: "other_member@example.com"
},
actor: actor
)
# Create a user and link it to a member
{:ok, user} =
Accounts.create_user(%{
email: "user@example.com"
})
Accounts.create_user(
%{
email: "user@example.com"
},
actor: actor
)
{:ok, _member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "temp@example.com",
user: %{id: user.id}
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "temp@example.com",
user: %{id: user.id}
},
actor: actor
)
# Reload user to get updated member_id
{:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id)
{:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
# 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"
})
Accounts.update_user(
user_reloaded,
%{
email: "other_member@example.com"
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{} = error} = result
@ -320,34 +435,49 @@ defmodule Mv.Accounts.EmailUniquenessTest do
end
describe "Email uniqueness validation - Linking" do
test "CANNOT link user to member if user email is already used by another unlinked member" do
test "CANNOT link user to member if user email is already used by another unlinked member", %{
actor: actor
} do
# Create a member with email
{:ok, _other_member} =
Membership.create_member(%{
first_name: "Jane",
last_name: "Doe",
email: "duplicate@example.com"
})
Membership.create_member(
%{
first_name: "Jane",
last_name: "Doe",
email: "duplicate@example.com"
},
actor: actor
)
# Create a user with same email
{:ok, user} =
Accounts.create_user(%{
email: "duplicate@example.com"
})
Accounts.create_user(
%{
email: "duplicate@example.com"
},
actor: actor
)
# Create a member to link with the user
{:ok, member} =
Membership.create_member(%{
first_name: "John",
last_name: "Smith",
email: "john@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Smith",
email: "john@example.com"
},
actor: actor
)
# 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}
})
Accounts.update_user(
user,
%{
member: %{id: member.id}
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{} = error} = result
@ -358,120 +488,160 @@ defmodule Mv.Accounts.EmailUniquenessTest do
end)
end
test "CAN link member to user even if member email is used by another user (member email gets overridden)" do
test "CAN link member to user even if member email is used by another user (member email gets overridden)",
%{actor: actor} do
# Create a user with email
{:ok, _other_user} =
Accounts.create_user(%{
email: "duplicate@example.com"
})
Accounts.create_user(
%{
email: "duplicate@example.com"
},
actor: actor
)
# Create a member with same email
{:ok, member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "duplicate@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "duplicate@example.com"
},
actor: actor
)
# Create a user to link with the member
{:ok, user} =
Accounts.create_user(%{
email: "user@example.com"
})
Accounts.create_user(
%{
email: "user@example.com"
},
actor: actor
)
# Link member to user - should succeed because member.email will be overridden
{:ok, updated_member} =
Membership.update_member(member, %{
user: %{id: user.id}
})
Membership.update_member(
member,
%{
user: %{id: user.id}
},
actor: actor
)
# Member email should now be the same as user email
{:ok, member_reloaded} = Ash.get(Mv.Membership.Member, updated_member.id)
{:ok, member_reloaded} = Ash.get(Mv.Membership.Member, updated_member.id, actor: actor)
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
test "member email syncs to linked user email without validation error", %{actor: actor} do
# Create a user
{:ok, user} =
Accounts.create_user(%{
email: "user@example.com"
})
Accounts.create_user(
%{
email: "user@example.com"
},
actor: actor
)
# 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}
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "member@example.com",
user: %{id: user.id}
},
actor: actor
)
# 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)
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert member_after_link.email == "user@example.com"
end
test "user email syncs to linked member without validation error" do
test "user email syncs to linked member without validation error", %{actor: actor} do
# Create a member
{:ok, member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
},
actor: actor
)
# 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}
})
Accounts.create_user(
%{
email: "user@example.com",
member: %{id: member.id}
},
actor: actor
)
# 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)
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert member_after_link.email == "user@example.com"
end
test "two unlinked users cannot have the same email" do
test "two unlinked users cannot have the same email", %{actor: actor} do
# Create first user
{:ok, _user1} =
Accounts.create_user(%{
email: "duplicate@example.com"
})
Accounts.create_user(
%{
email: "duplicate@example.com"
},
actor: actor
)
# Try to create second user with same email
result =
Accounts.create_user(%{
email: "duplicate@example.com"
})
Accounts.create_user(
%{
email: "duplicate@example.com"
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{}} = result
end
test "two unlinked members cannot have the same email (members have unique constraint)" do
test "two unlinked members cannot have the same email (members have unique constraint)", %{
actor: actor
} do
# Create first member
{:ok, _member1} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "duplicate@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "duplicate@example.com"
},
actor: actor
)
# Try to create second member with same email - should fail
result =
Membership.create_member(%{
first_name: "Jane",
last_name: "Smith",
email: "duplicate@example.com"
})
Membership.create_member(
%{
first_name: "Jane",
last_name: "Smith",
email: "duplicate@example.com"
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{}} = result
# Members DO have a unique email constraint at database level

View file

@ -10,6 +10,11 @@ defmodule Mv.Accounts.UserAuthenticationTest do
use MvWeb.ConnCase, async: true
require Ash.Query
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
describe "Password authentication user identification" do
@tag :test_proposal
test "password login uses email as identifier" do
@ -27,7 +32,7 @@ defmodule Mv.Accounts.UserAuthenticationTest do
{:ok, users} =
Mv.Accounts.User
|> Ash.Query.filter(email == ^email_to_find)
|> Ash.read()
|> Ash.read(actor: user)
assert length(users) == 1
found_user = List.first(users)
@ -113,11 +118,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
# Use sign_in_with_rauthy to find user by oidc_id
# Note: This test will FAIL until we implement the security fix
# that changes the filter from email to oidc_id
system_actor = Mv.Helpers.SystemActor.get_system_actor()
result =
Mv.Accounts.read_sign_in_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
case result do
{:ok, [found_user]} ->
@ -141,11 +151,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
}
# Should create via register_with_rauthy
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, new_user} =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
assert to_string(new_user.email) == "newuser@example.com"
assert new_user.oidc_id == "brand_new_oidc_789"
@ -170,12 +185,12 @@ defmodule Mv.Accounts.UserAuthenticationTest do
{:ok, users1} =
Mv.Accounts.User
|> Ash.Query.filter(oidc_id == "oidc_unique_1")
|> Ash.read()
|> Ash.read(actor: user1)
{:ok, users2} =
Mv.Accounts.User
|> Ash.Query.filter(oidc_id == "oidc_unique_2")
|> Ash.read()
|> Ash.read(actor: user2)
assert length(users1) == 1
assert length(users2) == 1
@ -205,11 +220,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
}
# Should NOT find the user (security requirement)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
result =
Mv.Accounts.read_sign_in_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
# Either returns empty list OR authentication error - both mean "user not found"
case result do
@ -241,11 +261,16 @@ defmodule Mv.Accounts.UserAuthenticationTest do
}
# Should NOT find the user because oidc_id is nil
system_actor = Mv.Helpers.SystemActor.get_system_actor()
result =
Mv.Accounts.read_sign_in_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
# Either returns empty list OR authentication error - both mean "user not found"
case result do

View file

@ -8,6 +8,11 @@ defmodule Mv.Accounts.UserEmailSyncTest do
alias Mv.Accounts
alias Mv.Membership
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
describe "User email synchronization to linked Member" do
@valid_user_attrs %{
email: "user@example.com"
@ -19,96 +24,100 @@ defmodule Mv.Accounts.UserEmailSyncTest do
email: "member@example.com"
}
test "updating user email syncs to linked member" do
test "updating user email syncs to linked member", %{actor: actor} do
# Create a member
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
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}))
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
# Verify initial state - member email should be overridden by user email
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id)
{:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert member_after_link.email == "user@example.com"
# Update user email
{:ok, updated_user} = Accounts.update_user(user, %{email: "newemail@example.com"})
{:ok, updated_user} =
Accounts.update_user(user, %{email: "newemail@example.com"}, actor: actor)
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)
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert synced_member.email == "newemail@example.com"
end
test "creating user linked to member overrides member email" do
test "creating user linked to member overrides member email", %{actor: actor} do
# Create a member with their own email
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
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}))
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
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)
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert updated_member.email == "user@example.com"
end
test "linking user to existing member syncs user email to member" do
test "linking user to existing member syncs user email to member", %{actor: actor} do
# Create a standalone member
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
assert member.email == "member@example.com"
# Create a standalone user
{:ok, user} = Accounts.create_user(@valid_user_attrs)
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
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}})
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
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)
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert synced_member.email == "user@example.com"
end
test "updating user email when no member linked does not error" do
test "updating user email when no member linked does not error", %{actor: actor} do
# Create a standalone user without member link
{:ok, user} = Accounts.create_user(@valid_user_attrs)
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
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"})
{:ok, updated_user} =
Accounts.update_user(user, %{email: "newemail@example.com"}, actor: actor)
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
test "unlinking user from member does not sync email", %{actor: actor} do
# Create member
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
# Create user linked to member
{:ok, user} =
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
assert user.member_id == member.id
# Verify member email was synced
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id)
{:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert synced_member.email == "user@example.com"
# Unlink user from member
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil})
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}, actor: actor)
assert unlinked_user.member_id == nil
# Member email should remain unchanged after unlinking
{:ok, member_after_unlink} = Ash.get(Mv.Membership.Member, member.id)
{:ok, member_after_unlink} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
assert member_after_unlink.email == "user@example.com"
end
end
@ -119,6 +128,8 @@ defmodule Mv.Accounts.UserEmailSyncTest do
email = "test@example.com"
password = "securepassword123"
system_actor = Mv.Helpers.SystemActor.get_system_actor()
# Create user with password strategy (simulating registration)
{:ok, user} =
Mv.Accounts.User
@ -126,7 +137,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do
email: email,
password: password
})
|> Ash.create()
|> Ash.create(actor: system_actor)
assert to_string(user.email) == email
assert user.hashed_password != nil
@ -138,7 +149,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do
email: email,
password: password
})
|> Ash.read_one()
|> Ash.read_one(actor: system_actor)
assert signed_in_user.id == user.id
assert to_string(signed_in_user.email) == email
@ -153,6 +164,8 @@ defmodule Mv.Accounts.UserEmailSyncTest do
oauth_tokens = %{"access_token" => "mock_token"}
system_actor = Mv.Helpers.SystemActor.get_system_actor()
# Simulate OIDC registration
{:ok, user} =
Mv.Accounts.User
@ -160,7 +173,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do
user_info: user_info,
oauth_tokens: oauth_tokens
})
|> Ash.create()
|> Ash.create(actor: system_actor)
assert to_string(user.email) == "oidc@example.com"
assert user.oidc_id == "oidc-user-123"

View file

@ -18,71 +18,86 @@ defmodule Mv.Accounts.UserMemberDeletionTest do
email: "john@example.com"
}
test "deleting a member sets the user's member_id to NULL" do
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
test "deleting a member sets the user's member_id to NULL", %{actor: actor} do
# Create a member
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
# Create a user linked to the member
{:ok, user} =
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor)
# Verify the relationship is established
{:ok, user_before_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
{:ok, user_before_delete} =
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
assert user_before_delete.member_id == member.id
assert user_before_delete.member.id == member.id
# Delete the member
:ok = Membership.destroy_member(member)
:ok = Membership.destroy_member(member, actor: actor)
# Verify the user still exists but member_id is NULL
{:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
{:ok, user_after_delete} =
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
assert user_after_delete.id == user.id
assert user_after_delete.member_id == nil
assert user_after_delete.member == nil
end
test "user can be linked to a new member after old member is deleted" do
test "user can be linked to a new member after old member is deleted", %{actor: actor} do
# Create first member
{:ok, member1} = Membership.create_member(@valid_member_attrs)
{:ok, member1} = Membership.create_member(@valid_member_attrs, actor: actor)
# Create user linked to first member
{:ok, user} =
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member1.id}))
Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member1.id}), actor: actor)
assert user.member_id == member1.id
# Delete first member
:ok = Membership.destroy_member(member1)
:ok = Membership.destroy_member(member1, actor: actor)
# Reload user from database to get updated member_id (should be NULL)
{:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id)
{:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
assert user_after_delete.member_id == nil
# Create second member
{:ok, member2} =
Membership.create_member(%{
first_name: "Jane",
last_name: "Smith",
email: "jane@example.com"
})
Membership.create_member(
%{
first_name: "Jane",
last_name: "Smith",
email: "jane@example.com"
},
actor: actor
)
# Link user to second member (use reloaded user)
{:ok, updated_user} = Accounts.update_user(user_after_delete, %{member: %{id: member2.id}})
{:ok, updated_user} =
Accounts.update_user(user_after_delete, %{member: %{id: member2.id}}, actor: actor)
# Verify new relationship
{:ok, final_user} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member])
{:ok, final_user} =
Ash.get(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member])
assert final_user.member_id == member2.id
assert final_user.member.id == member2.id
end
test "member without linked user can be deleted normally" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "member without linked user can be deleted normally", %{actor: actor} do
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
# Delete member (no users linked)
assert :ok = Membership.destroy_member(member)
assert :ok = Membership.destroy_member(member, actor: actor)
# Verify member is deleted
assert {:error, _} = Ash.get(Mv.Membership.Member, member.id)
assert {:error, _} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
end
end
end

View file

@ -10,51 +10,70 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do
alias Mv.Accounts
alias Mv.Membership
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
describe "link with same email" do
test "succeeds when user.email == member.email" do
test "succeeds when user.email == member.email", %{actor: actor} do
# Create member with specific email
{:ok, member} =
Membership.create_member(%{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com"
})
Membership.create_member(
%{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com"
},
actor: actor
)
# Create user with same email and link to member
result =
Accounts.create_user(%{
email: "alice@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "alice@example.com",
member: %{id: member.id}
},
actor: actor
)
# 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)
user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor)
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
test "no validation error triggered when updating linked pair with same email", %{
actor: actor
} do
# Create member
{:ok, member} =
Membership.create_member(%{
first_name: "Bob",
last_name: "Smith",
email: "bob@example.com"
})
Membership.create_member(
%{
first_name: "Bob",
last_name: "Smith",
email: "bob@example.com"
},
actor: actor
)
# Create user and link
{:ok, user} =
Accounts.create_user(%{
email: "bob@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "bob@example.com",
member: %{id: member.id}
},
actor: actor
)
# Update user (should not trigger email validation error)
result = Accounts.update_user(user, %{email: "bob@example.com"})
result = Accounts.update_user(user, %{email: "bob@example.com"}, actor: actor)
assert {:ok, updated_user} = result
assert to_string(updated_user.email) == "bob@example.com"
@ -62,70 +81,88 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do
end
describe "link with different emails" do
test "fails if member.email is used by a DIFFERENT linked user" do
test "fails if member.email is used by a DIFFERENT linked user", %{actor: actor} 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"
})
Membership.create_member(
%{
first_name: "Other",
last_name: "Member",
email: "other@example.com"
},
actor: actor
)
{:ok, _user1} =
Accounts.create_user(%{
email: "user1@example.com",
member: %{id: other_member.id}
})
Accounts.create_user(
%{
email: "user1@example.com",
member: %{id: other_member.id}
},
actor: actor
)
# Reload to ensure email sync happened
_other_member = Ash.reload!(other_member)
_other_member = Ash.reload!(other_member, actor: actor)
# Create a NEW member with different email
{:ok, member} =
Membership.create_member(%{
first_name: "Charlie",
last_name: "Brown",
email: "charlie@example.com"
})
Membership.create_member(
%{
first_name: "Charlie",
last_name: "Brown",
email: "charlie@example.com"
},
actor: actor
)
# Try to create user2 with email that matches the linked other_member
result =
Accounts.create_user(%{
email: "user1@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "user1@example.com",
member: %{id: member.id}
},
actor: actor
)
# 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
test "succeeds for unique emails", %{actor: actor} do
# Create member
{:ok, member} =
Membership.create_member(%{
first_name: "David",
last_name: "Wilson",
email: "david@example.com"
})
Membership.create_member(
%{
first_name: "David",
last_name: "Wilson",
email: "david@example.com"
},
actor: actor
)
# Create user with different but unique email
result =
Accounts.create_user(%{
email: "user@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "user@example.com",
member: %{id: member.id}
},
actor: actor
)
# 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)
user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor)
assert user.member.email == "user@example.com"
end
end
describe "edge cases" do
test "unlinking and relinking with same email works (Problem #4)" do
test "unlinking and relinking with same email works (Problem #4)", %{actor: actor} 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)
@ -133,34 +170,40 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do
# Create member
{:ok, member} =
Membership.create_member(%{
first_name: "Emma",
last_name: "Davis",
email: "emma@example.com"
})
Membership.create_member(
%{
first_name: "Emma",
last_name: "Davis",
email: "emma@example.com"
},
actor: actor
)
# Create user and link
{:ok, user} =
Accounts.create_user(%{
email: "emma@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "emma@example.com",
member: %{id: member.id}
},
actor: actor
)
# Verify they are linked
user = Ash.load!(user, [:member], domain: Mv.Accounts)
user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor)
assert user.member.id == member.id
assert user.member.email == "emma@example.com"
# Unlink
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil})
{:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}, actor: actor)
assert is_nil(unlinked_user.member_id)
# Member still has the email after unlink
member = Ash.reload!(member)
member = Ash.reload!(member, actor: actor)
assert member.email == "emma@example.com"
# Relink (should work - this is Problem #4)
result = Accounts.update_user(unlinked_user, %{member: %{id: member.id}})
result = Accounts.update_user(unlinked_user, %{member: %{id: member.id}}, actor: actor)
assert {:ok, relinked_user} = result
assert relinked_user.member_id == member.id

View file

@ -9,121 +9,150 @@ defmodule Mv.Accounts.UserMemberLinkingTest do
alias Mv.Accounts
alias Mv.Membership
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
describe "User-Member Linking with Email Sync" do
test "link user to member with different email syncs member email" do
test "link user to member with different email syncs member email", %{actor: actor} do
# Create user with one email
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
{:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor)
# Create member with different email
{:ok, member} =
Membership.create_member(%{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
})
Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "member@example.com"
},
actor: actor
)
# Link user to member
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}})
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
# Verify link exists
user_with_member = Ash.get!(Mv.Accounts.User, updated_user.id, load: [:member])
user_with_member =
Ash.get!(Mv.Accounts.User, updated_user.id, actor: actor, 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)
synced_member = Ash.get!(Mv.Membership.Member, member.id, actor: actor)
assert synced_member.email == "user@example.com"
end
test "unlink member from user sets member to nil" do
test "unlink member from user sets member to nil", %{actor: actor} do
# Create and link user and member
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
{:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor)
{:ok, member} =
Membership.create_member(%{
first_name: "Jane",
last_name: "Smith",
email: "jane@example.com"
})
Membership.create_member(
%{
first_name: "Jane",
last_name: "Smith",
email: "jane@example.com"
},
actor: actor
)
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}})
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
# Verify link exists
user_with_member = Ash.get!(Mv.Accounts.User, linked_user.id, load: [:member])
user_with_member = Ash.get!(Mv.Accounts.User, linked_user.id, actor: actor, 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})
{:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}, actor: actor)
# Verify link is removed
user_without_member = Ash.get!(Mv.Accounts.User, unlinked_user.id, load: [:member])
user_without_member =
Ash.get!(Mv.Accounts.User, unlinked_user.id, actor: actor, load: [:member])
assert is_nil(user_without_member.member)
# Verify member still exists independently
member_still_exists = Ash.get!(Mv.Membership.Member, member.id)
member_still_exists = Ash.get!(Mv.Membership.Member, member.id, actor: actor)
assert member_still_exists.id == member.id
end
test "cannot link member already linked to another user" do
test "cannot link member already linked to another user", %{actor: actor} do
# Create first user and link to member
{:ok, user1} = Accounts.create_user(%{email: "user1@example.com"})
{:ok, user1} = Accounts.create_user(%{email: "user1@example.com"}, actor: actor)
{:ok, member} =
Membership.create_member(%{
first_name: "Bob",
last_name: "Wilson",
email: "bob@example.com"
})
Membership.create_member(
%{
first_name: "Bob",
last_name: "Wilson",
email: "bob@example.com"
},
actor: actor
)
{:ok, _linked_user1} = Accounts.update_user(user1, %{member: %{id: member.id}})
{:ok, _linked_user1} =
Accounts.update_user(user1, %{member: %{id: member.id}}, actor: actor)
# Create second user and try to link to same member
{:ok, user2} = Accounts.create_user(%{email: "user2@example.com"})
{:ok, user2} = Accounts.create_user(%{email: "user2@example.com"}, actor: actor)
# Should fail because member is already linked
assert {:error, %Ash.Error.Invalid{}} =
Accounts.update_user(user2, %{member: %{id: member.id}})
Accounts.update_user(user2, %{member: %{id: member.id}}, actor: actor)
end
test "cannot change member link directly, must unlink first" do
test "cannot change member link directly, must unlink first", %{actor: actor} do
# Create user and link to first member
{:ok, user} = Accounts.create_user(%{email: "user@example.com"})
{:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor)
{:ok, member1} =
Membership.create_member(%{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com"
})
Membership.create_member(
%{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com"
},
actor: actor
)
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member1.id}})
{:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member1.id}}, actor: actor)
# Create second member
{:ok, member2} =
Membership.create_member(%{
first_name: "Charlie",
last_name: "Brown",
email: "charlie@example.com"
})
Membership.create_member(
%{
first_name: "Charlie",
last_name: "Brown",
email: "charlie@example.com"
},
actor: actor
)
# Try to directly change member link (should fail)
assert {:error, %Ash.Error.Invalid{errors: errors}} =
Accounts.update_user(linked_user, %{member: %{id: member2.id}})
Accounts.update_user(linked_user, %{member: %{id: member2.id}}, actor: actor)
# 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})
{:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}, actor: actor)
# 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, _} =
Membership.update_member(member1, %{email: "alice_changed@example.com"}, actor: actor)
{:ok, relinked_user} = Accounts.update_user(unlinked_user, %{member: %{id: member2.id}})
{:ok, relinked_user} =
Accounts.update_user(unlinked_user, %{member: %{id: member2.id}}, actor: actor)
# Verify new link is established
user_with_new_member = Ash.get!(Mv.Accounts.User, relinked_user.id, load: [:member])
user_with_new_member =
Ash.get!(Mv.Accounts.User, relinked_user.id, actor: actor, load: [:member])
assert user_with_new_member.member.id == member2.id
end
end

View file

@ -5,6 +5,11 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
alias Mv.Accounts
alias Mv.Membership
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
describe "User-Member Relationship - Basic Tests" do
@valid_user_attrs %{
email: "test@example.com"
@ -16,22 +21,26 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
email: "john@example.com"
}
test "user can exist without member" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
test "user can exist without member", %{actor: actor} do
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
assert user.member_id == nil
# Load the relationship to test it
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
{:ok, user_with_member} =
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
assert user_with_member.member == nil
end
test "member can exist without user" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "member can exist without user", %{actor: actor} do
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
assert member.id != nil
assert member.first_name == "John"
# Load the relationship to test it
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
{:ok, member_with_user} =
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
assert member_with_user.user == nil
end
end
@ -47,47 +56,58 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
email: "alice@example.com"
}
test "user can be linked to member during user creation" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "user can be linked to member during user creation", %{actor: actor} do
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
{:ok, user} = Accounts.create_user(user_attrs)
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
# Load the relationship to test it
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
{:ok, user_with_member} =
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
assert user_with_member.member.id == member.id
end
test "member can be linked to user during member creation using manage_relationship" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
test "member can be linked to user during member creation using manage_relationship", %{
actor: actor
} do
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
member_attrs = Map.put(@valid_member_attrs, :user, %{id: user.id})
{:ok, member} = Membership.create_member(member_attrs)
{:ok, member} = Membership.create_member(member_attrs, actor: actor)
# Load the relationship to test it
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
{:ok, member_with_user} =
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
assert member_with_user.user.id == user.id
end
test "user can be linked to member during update" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "user can be linked to member during update", %{actor: actor} do
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}})
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor)
# Load the relationship to test it
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member])
{:ok, user_with_member} =
Ash.get(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member], actor: actor)
assert user_with_member.member.id == member.id
end
test "member can be linked to user during update using manage_relationship" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "member can be linked to user during update using manage_relationship", %{actor: actor} do
{:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
{:ok, _updated_member} = Membership.update_member(member, %{user: %{id: user.id}})
{:ok, _updated_member} =
Membership.update_member(member, %{user: %{id: user.id}}, actor: actor)
# Load the relationship to test it
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
{:ok, member_with_user} =
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
assert member_with_user.user.id == user.id
end
end
@ -103,25 +123,39 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
email: "bob@example.com"
}
test "ash resolves inverse relationship automatically" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
test "ash resolves inverse relationship automatically", %{actor: actor} do
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
{:ok, user} = Accounts.create_user(user_attrs)
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
# Load relationships
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
{:ok, user_with_member} =
Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member], actor: actor)
{:ok, member_with_user} =
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
assert user_with_member.member.id == member.id
assert member_with_user.user.id == user.id
end
test "member can find associated user" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "member can find associated user", %{actor: actor} do
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
{:ok, user} =
Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}},
actor: actor
)
{:ok, member_with_user} =
Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user], actor: actor)
{:ok, user} = Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}})
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert member_with_user.user.id == user.id
end
end
@ -137,61 +171,77 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
email: "charlie@example.com"
}
test "prevents overwriting a member of already linked user on update" do
{:ok, existing_member} = Membership.create_member(@valid_member_attrs)
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
test "prevents overwriting a member of already linked user on update", %{actor: actor} do
{:ok, existing_member} = Membership.create_member(@valid_member_attrs, actor: actor)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
{:ok, user} = Accounts.create_user(user_attrs)
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
{:ok, member2} =
Membership.create_member(%{
first_name: "Dave",
last_name: "Wilson",
email: "dave@example.com"
})
Membership.create_member(
%{
first_name: "Dave",
last_name: "Wilson",
email: "dave@example.com"
},
actor: actor
)
assert {:error, %Ash.Error.Invalid{}} =
Accounts.update_user(user, %{member: %{id: member2.id}})
Accounts.update_user(user, %{member: %{id: member2.id}}, actor: actor)
end
test "prevents linking user to already linked member on update" do
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "prevents linking user to already linked member on update", %{actor: actor} do
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs, actor: actor)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
{:ok, _updated_user} =
Accounts.update_user(existing_user, %{member: %{id: member.id}}, actor: actor)
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"}, actor: actor)
assert {:error, %Ash.Error.Invalid{}} =
Accounts.update_user(user2, %{member: %{id: member.id}})
Accounts.update_user(user2, %{member: %{id: member.id}}, actor: actor)
end
test "prevents linking member to already linked user on creation" do
{:ok, existing_member} = Membership.create_member(@valid_member_attrs)
test "prevents linking member to already linked user on creation", %{actor: actor} do
{:ok, existing_member} = Membership.create_member(@valid_member_attrs, actor: actor)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
{:ok, user} = Accounts.create_user(user_attrs)
{:ok, user} = Accounts.create_user(user_attrs, actor: actor)
assert {:error, %Ash.Error.Invalid{}} =
Membership.create_member(%{
first_name: "Dave",
last_name: "Wilson",
email: "dave@example.com",
user: %{id: user.id}
})
Membership.create_member(
%{
first_name: "Dave",
last_name: "Wilson",
email: "dave@example.com",
user: %{id: user.id}
},
actor: actor
)
end
test "prevents linking user to already linked member on creation" do
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
test "prevents linking user to already linked member on creation", %{actor: actor} do
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs, actor: actor)
{:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor)
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
{:ok, _updated_user} =
Accounts.update_user(existing_user, %{member: %{id: member.id}}, actor: actor)
assert {:error, %Ash.Error.Invalid{}} =
Accounts.create_user(%{
email: "test5@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "test5@example.com",
member: %{id: member.id}
},
actor: actor
)
end
end
end