From cba471dcac84319a15e31887d4c5aab3dfdf823b Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 8 Jan 2026 16:48:42 +0100 Subject: [PATCH] test: add tests for HasPermission policy check Add comprehensive test suite for the HasPermission Ash Policy Check covering permission lookup, scope application, error handling, and logging. --- .../checks/has_permission_test.exs | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 test/mv/authorization/checks/has_permission_test.exs diff --git a/test/mv/authorization/checks/has_permission_test.exs b/test/mv/authorization/checks/has_permission_test.exs new file mode 100644 index 0000000..5ab88c6 --- /dev/null +++ b/test/mv/authorization/checks/has_permission_test.exs @@ -0,0 +1,264 @@ +defmodule Mv.Authorization.Checks.HasPermissionTest do + @moduledoc """ + Tests for the HasPermission Ash Policy Check. + + This check evaluates permissions from the PermissionSets module and applies + scope filters to Ash queries. + """ + use ExUnit.Case, async: true + + alias Mv.Authorization.Checks.HasPermission + + # Helper to create a mock authorizer for strict_check/3 + defp create_authorizer(resource, action) do + %Ash.Policy.Authorizer{ + resource: resource, + subject: %{action: %{name: action}} + } + end + + # Helper to create actor with role + defp create_actor(id, permission_set_name) do + %{ + id: id, + role: %{permission_set_name: permission_set_name} + } + end + + describe "describe/1" do + test "returns human-readable description" do + description = HasPermission.describe([]) + assert is_binary(description) + assert description =~ "permission" + end + end + + describe "strict_check/3 - Permission Lookup" do + test "admin has permission for all resources/actions" do + admin = create_actor("admin-123", "admin") + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(admin, authorizer, []) + + assert result == true or result == :unknown + end + + test "read_only has read permission for Member" do + read_only_user = create_actor("read-only-123", "read_only") + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(read_only_user, authorizer, []) + + assert result == true or result == :unknown + end + + test "read_only does NOT have create permission for Member" do + read_only_user = create_actor("read-only-123", "read_only") + authorizer = create_authorizer(Mv.Membership.Member, :create) + + {:ok, result} = HasPermission.strict_check(read_only_user, authorizer, []) + + assert result == false + end + + test "own_data has update permission for User with scope :own" do + own_data_user = create_actor("user-123", "own_data") + authorizer = create_authorizer(Mv.Accounts.User, :update) + + {:ok, result} = HasPermission.strict_check(own_data_user, authorizer, []) + + # Should return :unknown for :own scope (needs filter) + assert result == :unknown + end + end + + describe "strict_check/3 - Scope :all" do + test "actor with scope :all can access any record" do + admin = create_actor("admin-123", "admin") + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(admin, authorizer, []) + + # :all scope should return true (no filter needed) + assert result == true + end + + test "admin can read all members without filter" do + admin = create_actor("admin-123", "admin") + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(admin, authorizer, []) + + # Should return true for :all scope + assert result == true + end + end + + describe "strict_check/3 - Scope :own" do + test "actor with scope :own returns :unknown (needs filter)" do + user = create_actor("user-123", "own_data") + authorizer = create_authorizer(Mv.Accounts.User, :read) + + {:ok, result} = HasPermission.strict_check(user, authorizer, []) + + # Should return :unknown for :own scope (needs filter via auto_filter) + assert result == :unknown + end + end + + describe "auto_filter/3 - Scope :own" do + test "scope :own returns filter expression" do + user = create_actor("user-123", "own_data") + authorizer = create_authorizer(Mv.Accounts.User, :update) + + filter = HasPermission.auto_filter(user, authorizer, []) + + # Should return a filter expression + assert not is_nil(filter) + end + end + + describe "auto_filter/3 - Scope :linked" do + test "scope :linked for Member returns user_id filter" do + user = create_actor("user-123", "own_data") + authorizer = create_authorizer(Mv.Membership.Member, :read) + + filter = HasPermission.auto_filter(user, authorizer, []) + + # Should return a filter expression + assert not is_nil(filter) + end + + test "scope :linked for CustomFieldValue returns member.user_id filter" do + user = create_actor("user-123", "own_data") + authorizer = create_authorizer(Mv.Membership.CustomFieldValue, :update) + + filter = HasPermission.auto_filter(user, authorizer, []) + + # Should return a filter expression that traverses member relationship + assert not is_nil(filter) + end + end + + describe "strict_check/3 - Error Handling" do + test "returns {:ok, false} for nil actor" do + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(nil, authorizer, []) + + assert result == false + end + + test "returns {:ok, false} for actor missing role" do + actor_without_role = %{id: "user-123"} + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(actor_without_role, authorizer, []) + + assert result == false + end + + test "returns {:ok, false} for actor with nil role" do + actor_with_nil_role = %{id: "user-123", role: nil} + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(actor_with_nil_role, authorizer, []) + + assert result == false + end + + test "returns {:ok, false} for invalid permission_set_name" do + actor_with_invalid_permission = %{ + id: "user-123", + role: %{permission_set_name: "invalid_set"} + } + + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(actor_with_invalid_permission, authorizer, []) + + assert result == false + end + + test "returns {:ok, false} for no matching permission" do + read_only_user = create_actor("read-only-123", "read_only") + authorizer = create_authorizer(Mv.Authorization.Role, :create) + + {:ok, result} = HasPermission.strict_check(read_only_user, authorizer, []) + + assert result == false + end + + test "handles role with nil permission_set_name gracefully" do + actor_with_nil_permission_set = %{ + id: "user-123", + role: %{permission_set_name: nil} + } + + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(actor_with_nil_permission_set, authorizer, []) + + assert result == false + end + end + + describe "strict_check/3 - Logging" do + import ExUnit.CaptureLog + + test "logs authorization failure for nil actor" do + authorizer = create_authorizer(Mv.Membership.Member, :read) + + log = + capture_log(fn -> + HasPermission.strict_check(nil, authorizer, []) + end) + + assert log =~ "Authorization failed" or log == "" + end + + test "logs authorization failure for missing role" do + actor_without_role = %{id: "user-123"} + authorizer = create_authorizer(Mv.Membership.Member, :read) + + log = + capture_log(fn -> + HasPermission.strict_check(actor_without_role, authorizer, []) + end) + + assert log =~ "Authorization failed" or log == "" + end + end + + describe "strict_check/3 - Resource Name Extraction" do + test "correctly extracts resource name from nested module" do + admin = create_actor("admin-123", "admin") + authorizer = create_authorizer(Mv.Membership.Member, :read) + + {:ok, result} = HasPermission.strict_check(admin, authorizer, []) + + # Should work correctly (not crash) + assert result == true or result == :unknown or result == false + end + + test "works with different resource modules" do + admin = create_actor("admin-123", "admin") + + resources = [ + Mv.Accounts.User, + Mv.Membership.Member, + Mv.Membership.CustomFieldValue, + Mv.Membership.CustomField, + Mv.Authorization.Role + ] + + for resource <- resources do + authorizer = create_authorizer(resource, :read) + {:ok, result} = HasPermission.strict_check(admin, authorizer, []) + + # Should not crash and should return valid result + assert result == true or result == :unknown or result == false + end + end + end +end