diff --git a/test/mv/accounts/user_policies_test.exs b/test/mv/accounts/user_policies_test.exs index 97eea78..9678a0e 100644 --- a/test/mv/accounts/user_policies_test.exs +++ b/test/mv/accounts/user_policies_test.exs @@ -10,7 +10,6 @@ defmodule Mv.Accounts.UserPoliciesTest do use Mv.DataCase, async: false alias Mv.Accounts - alias Mv.Authorization require Ash.Query @@ -19,59 +18,10 @@ defmodule Mv.Accounts.UserPoliciesTest do %{actor: system_actor} 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 defp setup_user_with_own_access(permission_set, actor) do - user = create_user_with_permission_set(permission_set, actor) - other_user = create_other_user(actor) + user = Mv.Fixtures.user_with_role_fixture(permission_set) + other_user = Mv.Fixtures.user_with_role_fixture("own_data") # Reload user to ensure role is preloaded {:ok, user} = @@ -80,217 +30,88 @@ defmodule Mv.Accounts.UserPoliciesTest do %{user: user, other_user: other_user} end - describe "own_data permission set (Mitglied)" do - setup %{actor: actor} do - setup_user_with_own_access("own_data", actor) + # Data-driven: same behaviour for own_data, read_only, normal_user (scope :own for User) + describe "non-admin permission sets (own_data, read_only, normal_user)" do + setup %{actor: actor} = context do + permission_set = context[:permission_set] || "own_data" + setup_user_with_own_access(permission_set, 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) + for permission_set <- ["own_data", "read_only", "normal_user"] do + @tag permission_set: permission_set + 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 - 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) + assert fetched_user.id == user.id 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) + @tag permission_set: permission_set + test "can update own email (#{permission_set})", %{user: user} do + new_email = "updated#{System.unique_integer([:positive])}@example.com" + + {: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 - 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) + @tag permission_set: permission_set + test "cannot read other users - not found due to auto_filter (#{permission_set})", %{ + user: user, + other_user: other_user + } do + assert_raise Ash.Error.Invalid, fn -> + Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts) + end end - end - test "cannot destroy user (returns forbidden)", %{user: user} do - assert_raise Ash.Error.Forbidden, fn -> - Ash.destroy!(user, actor: user) + @tag permission_set: permission_set + test "cannot update other users - forbidden (#{permission_set})", %{ + 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 - describe "read_only permission set (Vorstand/Buchhaltung)" do - setup %{actor: actor} do - setup_user_with_own_access("read_only", actor) - end + @tag permission_set: permission_set + test "list users returns only own user (#{permission_set})", %{user: user} do + {:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts) - 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) + assert length(users) == 1 + assert hd(users).id == user.id 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) + @tag permission_set: permission_set + test "cannot create user - forbidden (#{permission_set})", %{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 "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 - - 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) + @tag permission_set: permission_set + test "cannot destroy user - forbidden (#{permission_set})", %{user: user} do + assert_raise Ash.Error.Forbidden, fn -> + Ash.destroy!(user, actor: user) + end end end end describe "admin permission set" do setup %{actor: actor} do - user = create_user_with_permission_set("admin", actor) - other_user = create_other_user(actor) + user = Mv.Fixtures.user_with_role_fixture("admin") + other_user = Mv.Fixtures.user_with_role_fixture("own_data") # Reload user to ensure role is preloaded {:ok, user} = @@ -345,11 +166,10 @@ defmodule Mv.Accounts.UserPoliciesTest do 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) + admin = Mv.Fixtures.user_with_role_fixture("admin") + normal_user_role = Mv.Fixtures.role_fixture("normal_user") {:ok, updated} = other_user @@ -362,13 +182,13 @@ defmodule Mv.Accounts.UserPoliciesTest do 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 + actor: _actor } do - _admin_role = create_role_with_permission_set("admin", actor) - normal_user_role = create_role_with_permission_set("normal_user", actor) + _admin_role = Mv.Fixtures.role_fixture("admin") + normal_user_role = Mv.Fixtures.role_fixture("normal_user") - admin_a = create_user_with_permission_set("admin", actor) - _admin_b = create_user_with_permission_set("admin", actor) + admin_a = Mv.Fixtures.user_with_role_fixture("admin") + _admin_b = Mv.Fixtures.user_with_role_fixture("admin") {:ok, updated} = admin_a @@ -379,10 +199,10 @@ defmodule Mv.Accounts.UserPoliciesTest do end test "single admin: changing own role to normal_user returns validation error", %{ - actor: actor + actor: _actor } do - normal_user_role = create_role_with_permission_set("normal_user", actor) - single_admin = create_user_with_permission_set("admin", actor) + normal_user_role = Mv.Fixtures.role_fixture("normal_user") + single_admin = Mv.Fixtures.user_with_role_fixture("admin") assert {:error, %Ash.Error.Invalid{errors: errors}} = single_admin