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