User form: persist role, member linking, Forbidden handling

- User resource: update_user accepts role_id, manage_relationship :member
- user_live/form: touch role_id, params_with_member_if_unchanged to avoid unlink
- Handle Forbidden in form, extract error message for display
- user_policies_test and form_test coverage
This commit is contained in:
Moritz 2026-02-03 23:52:20 +01:00
parent 5ed41555e9
commit 8ec4a07103
4 changed files with 196 additions and 8 deletions

View file

@ -343,6 +343,64 @@ defmodule Mv.Accounts.UserPoliciesTest do
# Verify user is deleted
assert {:error, _} = Ash.get(Accounts.User, other_user.id, domain: Mv.Accounts)
end
test "admin can assign role to another user via update_user", %{
actor: actor,
other_user: other_user
} do
admin = create_user_with_permission_set("admin", actor)
normal_user_role = create_role_with_permission_set("normal_user", actor)
{:ok, updated} =
other_user
|> Ash.Changeset.for_update(:update_user, %{role_id: normal_user_role.id})
|> Ash.update(actor: admin)
assert updated.role_id == normal_user_role.id
end
end
describe "admin role assignment and last-admin validation" do
test "two admins: one can change own role to normal_user (other remains admin)", %{
actor: actor
} do
_admin_role = create_role_with_permission_set("admin", actor)
normal_user_role = create_role_with_permission_set("normal_user", actor)
admin_a = create_user_with_permission_set("admin", actor)
_admin_b = create_user_with_permission_set("admin", actor)
{:ok, updated} =
admin_a
|> Ash.Changeset.for_update(:update_user, %{role_id: normal_user_role.id})
|> Ash.update(actor: admin_a)
assert updated.role_id == normal_user_role.id
end
test "single admin: changing own role to normal_user returns validation error", %{
actor: actor
} do
normal_user_role = create_role_with_permission_set("normal_user", actor)
single_admin = create_user_with_permission_set("admin", actor)
assert {:error, %Ash.Error.Invalid{errors: errors}} =
single_admin
|> Ash.Changeset.for_update(:update_user, %{role_id: normal_user_role.id})
|> Ash.update(actor: single_admin)
error_messages =
Enum.flat_map(errors, fn
%Ash.Error.Changes.InvalidAttribute{message: msg} when is_binary(msg) -> [msg]
%{message: msg} when is_binary(msg) -> [msg]
_ -> []
end)
assert Enum.any?(error_messages, fn msg ->
msg =~ "least one user must keep the Admin role" or msg =~ "Admin role"
end),
"Expected last-admin validation message, got: #{inspect(error_messages)}"
end
end
describe "AshAuthentication bypass" do

View file

@ -213,6 +213,35 @@ defmodule MvWeb.UserLive.FormTest do
assert not is_nil(updated_user.hashed_password)
assert updated_user.hashed_password != ""
end
test "admin can change user role and change persists", %{conn: conn} do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
role_a = Mv.Fixtures.role_fixture("normal_user")
role_b = Mv.Fixtures.role_fixture("read_only")
user = create_test_user(%{email: "rolechange@example.com"})
{:ok, user} = Mv.Accounts.update_user(user, %{role_id: role_a.id}, actor: system_actor)
assert user.role_id == role_a.id
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
view
|> form("#user-form",
user: %{
email: "rolechange@example.com",
role_id: role_b.id
}
)
|> render_submit()
assert_redirected(view, "/users")
updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor)
assert updated_user.role_id == role_b.id,
"Expected role_id to persist as #{role_b.id}, got #{inspect(updated_user.role_id)}"
end
end
describe "edit user form - validation" do