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.
This commit is contained in:
parent
05b611d880
commit
cba471dcac
1 changed files with 264 additions and 0 deletions
264
test/mv/authorization/checks/has_permission_test.exs
Normal file
264
test/mv/authorization/checks/has_permission_test.exs
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue