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:
Moritz 2026-01-08 16:48:42 +01:00
parent 05b611d880
commit cba471dcac

View 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