user_policies_test: data-driven tests for own_data, read_only, normal_user
Single describe with @tag permission_set and for-loop; one setup per permission set.
This commit is contained in:
parent
085b6be769
commit
3a92398d54
1 changed files with 72 additions and 252 deletions
|
|
@ -10,7 +10,6 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
||||||
use Mv.DataCase, async: false
|
use Mv.DataCase, async: false
|
||||||
|
|
||||||
alias Mv.Accounts
|
alias Mv.Accounts
|
||||||
alias Mv.Authorization
|
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
|
@ -19,59 +18,10 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
||||||
%{actor: system_actor}
|
%{actor: system_actor}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a role with a specific permission set
|
|
||||||
defp create_role_with_permission_set(permission_set_name, actor) do
|
|
||||||
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
|
||||||
|
|
||||||
case Authorization.create_role(
|
|
||||||
%{
|
|
||||||
name: role_name,
|
|
||||||
description: "Test role for #{permission_set_name}",
|
|
||||||
permission_set_name: permission_set_name
|
|
||||||
},
|
|
||||||
actor: actor
|
|
||||||
) do
|
|
||||||
{:ok, role} -> role
|
|
||||||
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper to create a user with a specific permission set
|
|
||||||
# Returns user with role preloaded (required for authorization)
|
|
||||||
defp create_user_with_permission_set(permission_set_name, actor) do
|
|
||||||
# Create role with permission set
|
|
||||||
role = create_role_with_permission_set(permission_set_name, actor)
|
|
||||||
|
|
||||||
# Create user
|
|
||||||
{:ok, user} =
|
|
||||||
Accounts.User
|
|
||||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
|
||||||
email: "user#{System.unique_integer([:positive])}@example.com",
|
|
||||||
password: "testpassword123"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: actor)
|
|
||||||
|
|
||||||
# Assign role to user
|
|
||||||
{:ok, user} =
|
|
||||||
user
|
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|
||||||
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
# Reload user with role preloaded (critical for authorization!)
|
|
||||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
|
|
||||||
user_with_role
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper to create another user (for testing access to other users)
|
|
||||||
defp create_other_user(actor) do
|
|
||||||
create_user_with_permission_set("own_data", actor)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Shared test setup for permission sets with scope :own access
|
# Shared test setup for permission sets with scope :own access
|
||||||
defp setup_user_with_own_access(permission_set, actor) do
|
defp setup_user_with_own_access(permission_set, actor) do
|
||||||
user = create_user_with_permission_set(permission_set, actor)
|
user = Mv.Fixtures.user_with_role_fixture(permission_set)
|
||||||
other_user = create_other_user(actor)
|
other_user = Mv.Fixtures.user_with_role_fixture("own_data")
|
||||||
|
|
||||||
# Reload user to ensure role is preloaded
|
# Reload user to ensure role is preloaded
|
||||||
{:ok, user} =
|
{:ok, user} =
|
||||||
|
|
@ -80,217 +30,88 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
||||||
%{user: user, other_user: other_user}
|
%{user: user, other_user: other_user}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "own_data permission set (Mitglied)" do
|
# Data-driven: same behaviour for own_data, read_only, normal_user (scope :own for User)
|
||||||
setup %{actor: actor} do
|
describe "non-admin permission sets (own_data, read_only, normal_user)" do
|
||||||
setup_user_with_own_access("own_data", actor)
|
setup %{actor: actor} = context do
|
||||||
|
permission_set = context[:permission_set] || "own_data"
|
||||||
|
setup_user_with_own_access(permission_set, actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can read own user record", %{user: user} do
|
for permission_set <- ["own_data", "read_only", "normal_user"] do
|
||||||
{:ok, fetched_user} =
|
@tag permission_set: permission_set
|
||||||
Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
|
test "can read own user record (#{permission_set})", %{user: user} do
|
||||||
|
{:ok, fetched_user} =
|
||||||
|
Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
|
||||||
|
|
||||||
assert fetched_user.id == user.id
|
assert fetched_user.id == user.id
|
||||||
end
|
|
||||||
|
|
||||||
test "can update own email", %{user: user} do
|
|
||||||
new_email = "updated#{System.unique_integer([:positive])}@example.com"
|
|
||||||
|
|
||||||
# Non-admins use :update (email only); :update_user is admin-only (member link/unlink).
|
|
||||||
{:ok, updated_user} =
|
|
||||||
user
|
|
||||||
|> Ash.Changeset.for_update(:update, %{email: new_email})
|
|
||||||
|> Ash.update(actor: user)
|
|
||||||
|
|
||||||
assert updated_user.email == Ash.CiString.new(new_email)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot read other users (returns not found due to auto_filter)", %{
|
|
||||||
user: user,
|
|
||||||
other_user: other_user
|
|
||||||
} do
|
|
||||||
# Note: With auto_filter policies, when a user tries to read a user that doesn't
|
|
||||||
# match the filter (id == actor.id), Ash returns NotFound, not Forbidden.
|
|
||||||
# This is the expected behavior - the filter makes the record "invisible" to the user.
|
|
||||||
assert_raise Ash.Error.Invalid, fn ->
|
|
||||||
Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot update other users (returns forbidden)", %{user: user, other_user: other_user} do
|
@tag permission_set: permission_set
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
test "can update own email (#{permission_set})", %{user: user} do
|
||||||
other_user
|
new_email = "updated#{System.unique_integer([:positive])}@example.com"
|
||||||
|> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
|
|
||||||
|> Ash.update!(actor: user)
|
{:ok, updated_user} =
|
||||||
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{email: new_email})
|
||||||
|
|> Ash.update(actor: user)
|
||||||
|
|
||||||
|
assert updated_user.email == Ash.CiString.new(new_email)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "list users returns only own user", %{user: user} do
|
@tag permission_set: permission_set
|
||||||
{:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
|
test "cannot read other users - not found due to auto_filter (#{permission_set})", %{
|
||||||
|
user: user,
|
||||||
# Should only return the own user (scope :own filters)
|
other_user: other_user
|
||||||
assert length(users) == 1
|
} do
|
||||||
assert hd(users).id == user.id
|
assert_raise Ash.Error.Invalid, fn ->
|
||||||
end
|
Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
|
||||||
|
end
|
||||||
test "cannot create user (returns forbidden)", %{user: user} do
|
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
|
||||||
Accounts.User
|
|
||||||
|> Ash.Changeset.for_create(:create_user, %{
|
|
||||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create!(actor: user)
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot destroy user (returns forbidden)", %{user: user} do
|
@tag permission_set: permission_set
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
test "cannot update other users - forbidden (#{permission_set})", %{
|
||||||
Ash.destroy!(user, actor: user)
|
user: user,
|
||||||
|
other_user: other_user
|
||||||
|
} do
|
||||||
|
assert_raise Ash.Error.Forbidden, fn ->
|
||||||
|
other_user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
|
||||||
|
|> Ash.update!(actor: user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "read_only permission set (Vorstand/Buchhaltung)" do
|
@tag permission_set: permission_set
|
||||||
setup %{actor: actor} do
|
test "list users returns only own user (#{permission_set})", %{user: user} do
|
||||||
setup_user_with_own_access("read_only", actor)
|
{:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
|
||||||
end
|
|
||||||
|
|
||||||
test "can read own user record", %{user: user} do
|
assert length(users) == 1
|
||||||
{:ok, fetched_user} =
|
assert hd(users).id == user.id
|
||||||
Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
|
|
||||||
|
|
||||||
assert fetched_user.id == user.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "can update own email", %{user: user} do
|
|
||||||
new_email = "updated#{System.unique_integer([:positive])}@example.com"
|
|
||||||
|
|
||||||
# Non-admins use :update (email only); :update_user is admin-only (member link/unlink).
|
|
||||||
{:ok, updated_user} =
|
|
||||||
user
|
|
||||||
|> Ash.Changeset.for_update(:update, %{email: new_email})
|
|
||||||
|> Ash.update(actor: user)
|
|
||||||
|
|
||||||
assert updated_user.email == Ash.CiString.new(new_email)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot read other users (returns not found due to auto_filter)", %{
|
|
||||||
user: user,
|
|
||||||
other_user: other_user
|
|
||||||
} do
|
|
||||||
# Note: With auto_filter policies, when a user tries to read a user that doesn't
|
|
||||||
# match the filter (id == actor.id), Ash returns NotFound, not Forbidden.
|
|
||||||
# This is the expected behavior - the filter makes the record "invisible" to the user.
|
|
||||||
assert_raise Ash.Error.Invalid, fn ->
|
|
||||||
Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot update other users (returns forbidden)", %{user: user, other_user: other_user} do
|
@tag permission_set: permission_set
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
test "cannot create user - forbidden (#{permission_set})", %{user: user} do
|
||||||
other_user
|
assert_raise Ash.Error.Forbidden, fn ->
|
||||||
|> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
|
Accounts.User
|
||||||
|> Ash.update!(actor: user)
|
|> Ash.Changeset.for_create(:create_user, %{
|
||||||
|
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||||
|
})
|
||||||
|
|> Ash.create!(actor: user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "list users returns only own user", %{user: user} do
|
@tag permission_set: permission_set
|
||||||
{:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
|
test "cannot destroy user - forbidden (#{permission_set})", %{user: user} do
|
||||||
|
assert_raise Ash.Error.Forbidden, fn ->
|
||||||
# Should only return the own user (scope :own filters)
|
Ash.destroy!(user, actor: user)
|
||||||
assert length(users) == 1
|
end
|
||||||
assert hd(users).id == user.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot create user (returns forbidden)", %{user: user} do
|
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
|
||||||
Accounts.User
|
|
||||||
|> Ash.Changeset.for_create(:create_user, %{
|
|
||||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create!(actor: user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot destroy user (returns forbidden)", %{user: user} do
|
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
|
||||||
Ash.destroy!(user, actor: user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "normal_user permission set (Kassenwart)" do
|
|
||||||
setup %{actor: actor} do
|
|
||||||
setup_user_with_own_access("normal_user", actor)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "can read own user record", %{user: user} do
|
|
||||||
{:ok, fetched_user} =
|
|
||||||
Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
|
|
||||||
|
|
||||||
assert fetched_user.id == user.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "can update own email", %{user: user} do
|
|
||||||
new_email = "updated#{System.unique_integer([:positive])}@example.com"
|
|
||||||
|
|
||||||
# Non-admins use :update (email only); :update_user is admin-only (member link/unlink).
|
|
||||||
{:ok, updated_user} =
|
|
||||||
user
|
|
||||||
|> Ash.Changeset.for_update(:update, %{email: new_email})
|
|
||||||
|> Ash.update(actor: user)
|
|
||||||
|
|
||||||
assert updated_user.email == Ash.CiString.new(new_email)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot read other users (returns not found due to auto_filter)", %{
|
|
||||||
user: user,
|
|
||||||
other_user: other_user
|
|
||||||
} do
|
|
||||||
# Note: With auto_filter policies, when a user tries to read a user that doesn't
|
|
||||||
# match the filter (id == actor.id), Ash returns NotFound, not Forbidden.
|
|
||||||
# This is the expected behavior - the filter makes the record "invisible" to the user.
|
|
||||||
assert_raise Ash.Error.Invalid, fn ->
|
|
||||||
Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot update other users (returns forbidden)", %{user: user, other_user: other_user} do
|
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
|
||||||
other_user
|
|
||||||
|> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
|
|
||||||
|> Ash.update!(actor: user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "list users returns only own user", %{user: user} do
|
|
||||||
{:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
|
|
||||||
|
|
||||||
# Should only return the own user (scope :own filters)
|
|
||||||
assert length(users) == 1
|
|
||||||
assert hd(users).id == user.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot create user (returns forbidden)", %{user: user} do
|
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
|
||||||
Accounts.User
|
|
||||||
|> Ash.Changeset.for_create(:create_user, %{
|
|
||||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create!(actor: user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cannot destroy user (returns forbidden)", %{user: user} do
|
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
|
||||||
Ash.destroy!(user, actor: user)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "admin permission set" do
|
describe "admin permission set" do
|
||||||
setup %{actor: actor} do
|
setup %{actor: actor} do
|
||||||
user = create_user_with_permission_set("admin", actor)
|
user = Mv.Fixtures.user_with_role_fixture("admin")
|
||||||
other_user = create_other_user(actor)
|
other_user = Mv.Fixtures.user_with_role_fixture("own_data")
|
||||||
|
|
||||||
# Reload user to ensure role is preloaded
|
# Reload user to ensure role is preloaded
|
||||||
{:ok, user} =
|
{:ok, user} =
|
||||||
|
|
@ -345,11 +166,10 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "admin can assign role to another user via update_user", %{
|
test "admin can assign role to another user via update_user", %{
|
||||||
actor: actor,
|
|
||||||
other_user: other_user
|
other_user: other_user
|
||||||
} do
|
} do
|
||||||
admin = create_user_with_permission_set("admin", actor)
|
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
||||||
normal_user_role = create_role_with_permission_set("normal_user", actor)
|
normal_user_role = Mv.Fixtures.role_fixture("normal_user")
|
||||||
|
|
||||||
{:ok, updated} =
|
{:ok, updated} =
|
||||||
other_user
|
other_user
|
||||||
|
|
@ -362,13 +182,13 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
||||||
|
|
||||||
describe "admin role assignment and last-admin validation" do
|
describe "admin role assignment and last-admin validation" do
|
||||||
test "two admins: one can change own role to normal_user (other remains admin)", %{
|
test "two admins: one can change own role to normal_user (other remains admin)", %{
|
||||||
actor: actor
|
actor: _actor
|
||||||
} do
|
} do
|
||||||
_admin_role = create_role_with_permission_set("admin", actor)
|
_admin_role = Mv.Fixtures.role_fixture("admin")
|
||||||
normal_user_role = create_role_with_permission_set("normal_user", actor)
|
normal_user_role = Mv.Fixtures.role_fixture("normal_user")
|
||||||
|
|
||||||
admin_a = create_user_with_permission_set("admin", actor)
|
admin_a = Mv.Fixtures.user_with_role_fixture("admin")
|
||||||
_admin_b = create_user_with_permission_set("admin", actor)
|
_admin_b = Mv.Fixtures.user_with_role_fixture("admin")
|
||||||
|
|
||||||
{:ok, updated} =
|
{:ok, updated} =
|
||||||
admin_a
|
admin_a
|
||||||
|
|
@ -379,10 +199,10 @@ defmodule Mv.Accounts.UserPoliciesTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "single admin: changing own role to normal_user returns validation error", %{
|
test "single admin: changing own role to normal_user returns validation error", %{
|
||||||
actor: actor
|
actor: _actor
|
||||||
} do
|
} do
|
||||||
normal_user_role = create_role_with_permission_set("normal_user", actor)
|
normal_user_role = Mv.Fixtures.role_fixture("normal_user")
|
||||||
single_admin = create_user_with_permission_set("admin", actor)
|
single_admin = Mv.Fixtures.user_with_role_fixture("admin")
|
||||||
|
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||||
single_admin
|
single_admin
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue