- Policy tests: use Fixtures where applicable; create_custom_field() fix in custom_field_value. - Replace unused actor with _actor, remove unused alias Accounts in policy tests. - profile_navigation_test: disable Credo for intentional TODO comment.
194 lines
6.8 KiB
Elixir
194 lines
6.8 KiB
Elixir
defmodule Mv.Membership.MemberEmailValidationTest do
|
||
@moduledoc """
|
||
Tests for Member email-change permission validation.
|
||
|
||
When a member is linked to a user, only admins or the linked user may change
|
||
that member's email. Unlinked members and non-email updates are unaffected.
|
||
"""
|
||
use Mv.DataCase, async: false
|
||
|
||
alias Mv.Accounts
|
||
alias Mv.Helpers.SystemActor
|
||
alias Mv.Membership
|
||
|
||
setup do
|
||
system_actor = SystemActor.get_system_actor()
|
||
%{actor: system_actor}
|
||
end
|
||
|
||
defp create_linked_member_for_user(user, _actor) do
|
||
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
||
|
||
{:ok, member} =
|
||
Membership.create_member(
|
||
%{
|
||
first_name: "Linked",
|
||
last_name: "Member",
|
||
email: "linked#{System.unique_integer([:positive])}@example.com"
|
||
},
|
||
actor: admin
|
||
)
|
||
|
||
user
|
||
|> Ash.Changeset.for_update(:update, %{})
|
||
|> Ash.Changeset.force_change_attribute(:member_id, member.id)
|
||
|> Ash.update(actor: admin, domain: Mv.Accounts, return_notifications?: false)
|
||
|
||
member
|
||
end
|
||
|
||
defp create_unlinked_member(_actor) do
|
||
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
||
|
||
{:ok, member} =
|
||
Membership.create_member(
|
||
%{
|
||
first_name: "Unlinked",
|
||
last_name: "Member",
|
||
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
||
},
|
||
actor: admin
|
||
)
|
||
|
||
member
|
||
end
|
||
|
||
describe "unlinked member" do
|
||
test "normal_user can update email of unlinked member", %{actor: actor} do
|
||
normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
|
||
unlinked_member = create_unlinked_member(actor)
|
||
|
||
new_email = "new#{System.unique_integer([:positive])}@example.com"
|
||
|
||
assert {:ok, updated} =
|
||
Membership.update_member(unlinked_member, %{email: new_email}, actor: normal_user)
|
||
|
||
assert updated.email == new_email
|
||
end
|
||
|
||
test "validation does not block when member has no linked user", %{actor: actor} do
|
||
normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
|
||
unlinked_member = create_unlinked_member(actor)
|
||
|
||
new_email = "other#{System.unique_integer([:positive])}@example.com"
|
||
|
||
assert {:ok, _} =
|
||
Membership.update_member(unlinked_member, %{email: new_email}, actor: normal_user)
|
||
end
|
||
end
|
||
|
||
describe "linked member – another user's member" do
|
||
test "normal_user cannot update email of another user's linked member", %{actor: actor} do
|
||
user_a = Mv.Fixtures.user_with_role_fixture("own_data")
|
||
linked_member = create_linked_member_for_user(user_a, actor)
|
||
|
||
normal_user_b = Mv.Fixtures.user_with_role_fixture("normal_user")
|
||
new_email = "other#{System.unique_integer([:positive])}@example.com"
|
||
|
||
assert {:error, %Ash.Error.Invalid{} = error} =
|
||
Membership.update_member(linked_member, %{email: new_email}, actor: normal_user_b)
|
||
|
||
assert Enum.any?(error.errors, &(&1.field == :email)),
|
||
"expected an error for field :email, got: #{inspect(error.errors)}"
|
||
end
|
||
|
||
test "admin can update email of linked member", %{actor: actor} do
|
||
user_a = Mv.Fixtures.user_with_role_fixture("own_data")
|
||
linked_member = create_linked_member_for_user(user_a, actor)
|
||
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
||
|
||
new_email = "admin_changed#{System.unique_integer([:positive])}@example.com"
|
||
|
||
assert {:ok, updated} =
|
||
Membership.update_member(linked_member, %{email: new_email}, actor: admin)
|
||
|
||
assert updated.email == new_email
|
||
end
|
||
end
|
||
|
||
describe "linked member – own member" do
|
||
test "own_data user can update email of their own linked member", %{actor: actor} do
|
||
own_data_user = Mv.Fixtures.user_with_role_fixture("own_data")
|
||
linked_member = create_linked_member_for_user(own_data_user, actor)
|
||
|
||
{:ok, own_data_user} =
|
||
Ash.get(Accounts.User, own_data_user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||
|
||
{:ok, own_data_user} =
|
||
Ash.load(own_data_user, :member, domain: Mv.Accounts, actor: actor)
|
||
|
||
new_email = "own_updated#{System.unique_integer([:positive])}@example.com"
|
||
|
||
assert {:ok, updated} =
|
||
Membership.update_member(linked_member, %{email: new_email}, actor: own_data_user)
|
||
|
||
assert updated.email == new_email
|
||
end
|
||
|
||
test "normal_user with linked member can update email of that same member", %{actor: actor} do
|
||
normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
|
||
linked_member = create_linked_member_for_user(normal_user, actor)
|
||
|
||
{:ok, normal_user} =
|
||
Ash.get(Accounts.User, normal_user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||
|
||
{:ok, normal_user} = Ash.load(normal_user, :member, domain: Mv.Accounts, actor: actor)
|
||
|
||
new_email = "normal_own#{System.unique_integer([:positive])}@example.com"
|
||
|
||
assert {:ok, updated} =
|
||
Membership.update_member(linked_member, %{email: new_email}, actor: normal_user)
|
||
|
||
assert updated.email == new_email
|
||
end
|
||
end
|
||
|
||
describe "no-op / other fields" do
|
||
test "updating only other attributes on linked member as normal_user does not trigger validation error",
|
||
%{actor: actor} do
|
||
user_a = Mv.Fixtures.user_with_role_fixture("own_data")
|
||
linked_member = create_linked_member_for_user(user_a, actor)
|
||
normal_user_b = Mv.Fixtures.user_with_role_fixture("normal_user")
|
||
|
||
assert {:ok, updated} =
|
||
Membership.update_member(linked_member, %{first_name: "UpdatedName"},
|
||
actor: normal_user_b
|
||
)
|
||
|
||
assert updated.first_name == "UpdatedName"
|
||
assert updated.email == linked_member.email
|
||
end
|
||
|
||
test "updating email of linked member as admin succeeds", %{actor: actor} do
|
||
user_a = Mv.Fixtures.user_with_role_fixture("own_data")
|
||
linked_member = create_linked_member_for_user(user_a, actor)
|
||
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
||
|
||
new_email = "admin_ok#{System.unique_integer([:positive])}@example.com"
|
||
|
||
assert {:ok, updated} =
|
||
Membership.update_member(linked_member, %{email: new_email}, actor: admin)
|
||
|
||
assert updated.email == new_email
|
||
end
|
||
end
|
||
|
||
describe "read_only" do
|
||
test "read_only cannot update any member (policy rejects before validation)", %{actor: actor} do
|
||
read_only_user = Mv.Fixtures.user_with_role_fixture("read_only")
|
||
linked_member = create_linked_member_for_user(read_only_user, actor)
|
||
|
||
{:ok, read_only_user} =
|
||
Ash.get(Accounts.User, read_only_user.id,
|
||
domain: Mv.Accounts,
|
||
load: [:role],
|
||
actor: actor
|
||
)
|
||
|
||
assert {:error, %Ash.Error.Forbidden{}} =
|
||
Membership.update_member(linked_member, %{email: "changed@example.com"},
|
||
actor: read_only_user
|
||
)
|
||
end
|
||
end
|
||
end
|