Merge remote-tracking branch 'origin/main' into sidebar
This commit is contained in:
commit
e7515b5450
83 changed files with 8084 additions and 1276 deletions
|
|
@ -13,14 +13,17 @@ defmodule Mv.Membership.MemberFieldVisibilityTest do
|
|||
alias Mv.Membership.Member
|
||||
|
||||
describe "show_in_overview?/1" do
|
||||
test "returns true for all member fields by default" do
|
||||
test "returns true for all member fields by default, except exit_date" do
|
||||
# When no settings exist or member_field_visibility is not configured
|
||||
# Test with fields from constants
|
||||
# Note: exit_date defaults to false (hidden) by design
|
||||
member_fields = Mv.Constants.member_fields()
|
||||
|
||||
Enum.each(member_fields, fn field ->
|
||||
assert Member.show_in_overview?(field) == true,
|
||||
"Field #{field} should be visible by default"
|
||||
expected_visibility = if field == :exit_date, do: false, else: true
|
||||
|
||||
assert Member.show_in_overview?(field) == expected_visibility,
|
||||
"Field #{field} should be #{if expected_visibility, do: "visible", else: "hidden"} by default"
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -77,4 +80,72 @@ defmodule Mv.Membership.MemberFieldVisibilityTest do
|
|||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_single_member_field_visibility/3" do
|
||||
test "atomically updates a single field in member_field_visibility" do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
field_string = "street"
|
||||
|
||||
# Update single field
|
||||
{:ok, updated_settings} =
|
||||
Mv.Membership.update_single_member_field_visibility(
|
||||
settings,
|
||||
field: field_string,
|
||||
show_in_overview: false
|
||||
)
|
||||
|
||||
# Verify the field was updated
|
||||
assert updated_settings.member_field_visibility[field_string] == false
|
||||
|
||||
# Verify other fields are not affected
|
||||
other_fields =
|
||||
Mv.Constants.member_fields()
|
||||
|> Enum.reject(&(&1 == String.to_existing_atom(field_string)))
|
||||
|
||||
Enum.each(other_fields, fn field ->
|
||||
field_string = Atom.to_string(field)
|
||||
# Fields not explicitly set should default to true (except exit_date)
|
||||
expected = if field == :exit_date, do: false, else: true
|
||||
|
||||
assert Map.get(updated_settings.member_field_visibility, field_string, expected) ==
|
||||
expected
|
||||
end)
|
||||
end
|
||||
|
||||
test "returns error for invalid field name" do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: [%{field: :member_field_visibility}]}} =
|
||||
Mv.Membership.update_single_member_field_visibility(
|
||||
settings,
|
||||
field: "invalid_field",
|
||||
show_in_overview: false
|
||||
)
|
||||
end
|
||||
|
||||
test "handles concurrent updates atomically" do
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
field1 = "street"
|
||||
field2 = "house_number"
|
||||
|
||||
# Simulate concurrent updates by updating different fields
|
||||
{:ok, updated1} =
|
||||
Mv.Membership.update_single_member_field_visibility(
|
||||
settings,
|
||||
field: field1,
|
||||
show_in_overview: false
|
||||
)
|
||||
|
||||
{:ok, updated2} =
|
||||
Mv.Membership.update_single_member_field_visibility(
|
||||
updated1,
|
||||
field: field2,
|
||||
show_in_overview: true
|
||||
)
|
||||
|
||||
# Both fields should be correctly updated
|
||||
assert updated2.member_field_visibility[field1] == false
|
||||
assert updated2.member_field_visibility[field2] == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ defmodule Mv.Membership.MemberTest do
|
|||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com",
|
||||
phone_number: "+49123456789",
|
||||
join_date: ~D[2020-01-01],
|
||||
exit_date: nil,
|
||||
notes: "Test note",
|
||||
|
|
@ -17,16 +16,14 @@ defmodule Mv.Membership.MemberTest do
|
|||
postal_code: "12345"
|
||||
}
|
||||
|
||||
test "First name is required and must not be empty" do
|
||||
attrs = Map.put(@valid_attrs, :first_name, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :first_name) =~ "must be present"
|
||||
test "First name is optional" do
|
||||
attrs = Map.delete(@valid_attrs, :first_name)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
end
|
||||
|
||||
test "Last name is required and must not be empty" do
|
||||
attrs = Map.put(@valid_attrs, :last_name, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :last_name) =~ "must be present"
|
||||
test "Last name is optional" do
|
||||
attrs = Map.delete(@valid_attrs, :last_name)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
end
|
||||
|
||||
test "Email is required" do
|
||||
|
|
@ -41,14 +38,6 @@ defmodule Mv.Membership.MemberTest do
|
|||
assert error_message(errors, :email) =~ "is not a valid email"
|
||||
end
|
||||
|
||||
test "Phone number is optional but must have a valid format if specified" do
|
||||
attrs = Map.put(@valid_attrs, :phone_number, "abc")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :phone_number) =~ "is not a valid phone number"
|
||||
attrs2 = Map.delete(@valid_attrs, :phone_number)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
|
||||
test "Join date cannot be in the future" do
|
||||
attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1))
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
defmodule Mv.Authorization.Checks.HasPermissionIntegrationTest do
|
||||
@moduledoc """
|
||||
Integration tests for HasPermission policy check.
|
||||
|
||||
These tests verify that the filter expressions generated by HasPermission
|
||||
have the correct structure for relationship-based filtering.
|
||||
|
||||
Note: Full integration tests with real queries require resources to have
|
||||
policies that use HasPermission. These tests validate filter expression
|
||||
structure and ensure the relationship paths are correct.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Mv.Authorization.Checks.HasPermission
|
||||
|
||||
# Helper to create mock actor with role
|
||||
defp create_actor_with_role(permission_set_name) do
|
||||
%{
|
||||
id: "user-#{System.unique_integer([:positive])}",
|
||||
role: %{permission_set_name: permission_set_name}
|
||||
}
|
||||
end
|
||||
|
||||
describe "Filter Expression Structure - :linked scope" do
|
||||
test "Member filter uses user.id relationship path" do
|
||||
actor = create_actor_with_role("own_data")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(actor, authorizer, [])
|
||||
|
||||
# Verify filter is not nil (should return a filter for :linked scope)
|
||||
assert not is_nil(filter)
|
||||
|
||||
# The filter should be a valid expression (keyword list or Ash.Expr)
|
||||
# We verify it's not nil and can be used in queries
|
||||
assert is_list(filter) or is_map(filter)
|
||||
end
|
||||
|
||||
test "CustomFieldValue filter uses member.user.id relationship path" do
|
||||
actor = create_actor_with_role("own_data")
|
||||
authorizer = create_authorizer(Mv.Membership.CustomFieldValue, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(actor, authorizer, [])
|
||||
|
||||
# Verify filter is not nil
|
||||
assert not is_nil(filter)
|
||||
|
||||
# The filter should be a valid expression
|
||||
assert is_list(filter) or is_map(filter)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Filter Expression Structure - :own scope" do
|
||||
test "User filter uses id == actor.id" do
|
||||
actor = create_actor_with_role("own_data")
|
||||
authorizer = create_authorizer(Mv.Accounts.User, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(actor, authorizer, [])
|
||||
|
||||
# Verify filter is not nil (should return a filter for :own scope)
|
||||
assert not is_nil(filter)
|
||||
|
||||
# The filter should be a valid expression
|
||||
assert is_list(filter) or is_map(filter)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Filter Expression Structure - :all scope" do
|
||||
test "Admin can read all members without filter" do
|
||||
actor = create_actor_with_role("admin")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(actor, authorizer, [])
|
||||
|
||||
# :all scope should return nil (no filter needed)
|
||||
assert is_nil(filter)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to create a mock authorizer
|
||||
defp create_authorizer(resource, action) do
|
||||
%Ash.Policy.Authorizer{
|
||||
resource: resource,
|
||||
subject: %{action: %{name: action}}
|
||||
}
|
||||
end
|
||||
end
|
||||
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
|
||||
584
test/mv/authorization/permission_sets_test.exs
Normal file
584
test/mv/authorization/permission_sets_test.exs
Normal file
|
|
@ -0,0 +1,584 @@
|
|||
defmodule Mv.Authorization.PermissionSetsTest do
|
||||
@moduledoc """
|
||||
Tests for the PermissionSets module that defines hardcoded permission sets.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Mv.Authorization.PermissionSets
|
||||
|
||||
describe "all_permission_sets/0" do
|
||||
test "returns all four permission sets" do
|
||||
sets = PermissionSets.all_permission_sets()
|
||||
|
||||
assert length(sets) == 4
|
||||
assert :own_data in sets
|
||||
assert :read_only in sets
|
||||
assert :normal_user in sets
|
||||
assert :admin in sets
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_permissions/1" do
|
||||
test "all permission sets return map with :resources and :pages keys" do
|
||||
for set <- PermissionSets.all_permission_sets() do
|
||||
permissions = PermissionSets.get_permissions(set)
|
||||
|
||||
assert Map.has_key?(permissions, :resources),
|
||||
"#{set} missing :resources key"
|
||||
|
||||
assert Map.has_key?(permissions, :pages),
|
||||
"#{set} missing :pages key"
|
||||
|
||||
assert is_list(permissions.resources),
|
||||
"#{set} :resources must be a list"
|
||||
|
||||
assert is_list(permissions.pages),
|
||||
"#{set} :pages must be a list"
|
||||
end
|
||||
end
|
||||
|
||||
test "each resource permission has required keys" do
|
||||
permissions = PermissionSets.get_permissions(:own_data)
|
||||
|
||||
Enum.each(permissions.resources, fn perm ->
|
||||
assert Map.has_key?(perm, :resource)
|
||||
assert Map.has_key?(perm, :action)
|
||||
assert Map.has_key?(perm, :scope)
|
||||
assert Map.has_key?(perm, :granted)
|
||||
assert is_binary(perm.resource)
|
||||
assert perm.action in [:read, :create, :update, :destroy]
|
||||
assert perm.scope in [:own, :linked, :all]
|
||||
assert is_boolean(perm.granted)
|
||||
end)
|
||||
end
|
||||
|
||||
test "pages lists are non-empty for all permission sets" do
|
||||
for set <- [:own_data, :read_only, :normal_user, :admin] do
|
||||
permissions = PermissionSets.get_permissions(set)
|
||||
|
||||
assert not Enum.empty?(permissions.pages),
|
||||
"Permission set #{set} should have at least one page"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_permissions/1 - :own_data permission content" do
|
||||
test "allows User read/update with scope :own" do
|
||||
permissions = PermissionSets.get_permissions(:own_data)
|
||||
|
||||
user_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :read end)
|
||||
|
||||
user_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :update end)
|
||||
|
||||
assert user_read.scope == :own
|
||||
assert user_read.granted == true
|
||||
assert user_update.scope == :own
|
||||
assert user_update.granted == true
|
||||
end
|
||||
|
||||
test "allows Member read/update with scope :linked" do
|
||||
permissions = PermissionSets.get_permissions(:own_data)
|
||||
|
||||
member_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :read end)
|
||||
|
||||
member_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :update end)
|
||||
|
||||
assert member_read.scope == :linked
|
||||
assert member_read.granted == true
|
||||
assert member_update.scope == :linked
|
||||
assert member_update.granted == true
|
||||
end
|
||||
|
||||
test "allows CustomFieldValue read/update with scope :linked" do
|
||||
permissions = PermissionSets.get_permissions(:own_data)
|
||||
|
||||
cfv_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :read
|
||||
end)
|
||||
|
||||
cfv_update =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :update
|
||||
end)
|
||||
|
||||
assert cfv_read.scope == :linked
|
||||
assert cfv_read.granted == true
|
||||
assert cfv_update.scope == :linked
|
||||
assert cfv_update.granted == true
|
||||
end
|
||||
|
||||
test "allows CustomField read with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:own_data)
|
||||
|
||||
cf_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomField" && p.action == :read
|
||||
end)
|
||||
|
||||
assert cf_read.scope == :all
|
||||
assert cf_read.granted == true
|
||||
end
|
||||
|
||||
test "includes correct pages" do
|
||||
permissions = PermissionSets.get_permissions(:own_data)
|
||||
|
||||
assert "/" in permissions.pages
|
||||
assert "/profile" in permissions.pages
|
||||
assert "/members/:id" in permissions.pages
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_permissions/1 - :read_only permission content" do
|
||||
test "allows User read/update with scope :own" do
|
||||
permissions = PermissionSets.get_permissions(:read_only)
|
||||
|
||||
user_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :read end)
|
||||
|
||||
user_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :update end)
|
||||
|
||||
assert user_read.scope == :own
|
||||
assert user_read.granted == true
|
||||
assert user_update.scope == :own
|
||||
assert user_update.granted == true
|
||||
end
|
||||
|
||||
test "allows Member read with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:read_only)
|
||||
|
||||
member_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :read end)
|
||||
|
||||
assert member_read.scope == :all
|
||||
assert member_read.granted == true
|
||||
end
|
||||
|
||||
test "does NOT allow Member create/update/destroy" do
|
||||
permissions = PermissionSets.get_permissions(:read_only)
|
||||
|
||||
member_create =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :create end)
|
||||
|
||||
member_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :update end)
|
||||
|
||||
member_destroy =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "Member" && p.action == :destroy
|
||||
end)
|
||||
|
||||
assert member_create == nil || member_create.granted == false
|
||||
assert member_update == nil || member_update.granted == false
|
||||
assert member_destroy == nil || member_destroy.granted == false
|
||||
end
|
||||
|
||||
test "allows CustomFieldValue read with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:read_only)
|
||||
|
||||
cfv_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :read
|
||||
end)
|
||||
|
||||
assert cfv_read.scope == :all
|
||||
assert cfv_read.granted == true
|
||||
end
|
||||
|
||||
test "does NOT allow CustomFieldValue create/update/destroy" do
|
||||
permissions = PermissionSets.get_permissions(:read_only)
|
||||
|
||||
cfv_create =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :create
|
||||
end)
|
||||
|
||||
cfv_update =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :update
|
||||
end)
|
||||
|
||||
cfv_destroy =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :destroy
|
||||
end)
|
||||
|
||||
assert cfv_create == nil || cfv_create.granted == false
|
||||
assert cfv_update == nil || cfv_update.granted == false
|
||||
assert cfv_destroy == nil || cfv_destroy.granted == false
|
||||
end
|
||||
|
||||
test "allows CustomField read with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:read_only)
|
||||
|
||||
cf_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomField" && p.action == :read
|
||||
end)
|
||||
|
||||
assert cf_read.scope == :all
|
||||
assert cf_read.granted == true
|
||||
end
|
||||
|
||||
test "includes correct pages" do
|
||||
permissions = PermissionSets.get_permissions(:read_only)
|
||||
|
||||
assert "/" in permissions.pages
|
||||
assert "/profile" in permissions.pages
|
||||
assert "/members" in permissions.pages
|
||||
assert "/members/:id" in permissions.pages
|
||||
assert "/custom_field_values" in permissions.pages
|
||||
assert "/custom_field_values/:id" in permissions.pages
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_permissions/1 - :normal_user permission content" do
|
||||
test "allows User read/update with scope :own" do
|
||||
permissions = PermissionSets.get_permissions(:normal_user)
|
||||
|
||||
user_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :read end)
|
||||
|
||||
user_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :update end)
|
||||
|
||||
assert user_read.scope == :own
|
||||
assert user_read.granted == true
|
||||
assert user_update.scope == :own
|
||||
assert user_update.granted == true
|
||||
end
|
||||
|
||||
test "allows Member read/create/update with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:normal_user)
|
||||
|
||||
member_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :read end)
|
||||
|
||||
member_create =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :create end)
|
||||
|
||||
member_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :update end)
|
||||
|
||||
assert member_read.scope == :all
|
||||
assert member_read.granted == true
|
||||
assert member_create.scope == :all
|
||||
assert member_create.granted == true
|
||||
assert member_update.scope == :all
|
||||
assert member_update.granted == true
|
||||
end
|
||||
|
||||
test "does NOT allow Member destroy (safety)" do
|
||||
permissions = PermissionSets.get_permissions(:normal_user)
|
||||
|
||||
member_destroy =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "Member" && p.action == :destroy
|
||||
end)
|
||||
|
||||
assert member_destroy == nil || member_destroy.granted == false
|
||||
end
|
||||
|
||||
test "allows CustomFieldValue full CRUD with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:normal_user)
|
||||
|
||||
cfv_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :read
|
||||
end)
|
||||
|
||||
cfv_create =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :create
|
||||
end)
|
||||
|
||||
cfv_update =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :update
|
||||
end)
|
||||
|
||||
cfv_destroy =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :destroy
|
||||
end)
|
||||
|
||||
assert cfv_read.scope == :all
|
||||
assert cfv_read.granted == true
|
||||
assert cfv_create.scope == :all
|
||||
assert cfv_create.granted == true
|
||||
assert cfv_update.scope == :all
|
||||
assert cfv_update.granted == true
|
||||
assert cfv_destroy.scope == :all
|
||||
assert cfv_destroy.granted == true
|
||||
end
|
||||
|
||||
test "allows CustomField read with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:normal_user)
|
||||
|
||||
cf_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomField" && p.action == :read
|
||||
end)
|
||||
|
||||
assert cf_read.scope == :all
|
||||
assert cf_read.granted == true
|
||||
end
|
||||
|
||||
test "includes correct pages" do
|
||||
permissions = PermissionSets.get_permissions(:normal_user)
|
||||
|
||||
assert "/" in permissions.pages
|
||||
assert "/profile" in permissions.pages
|
||||
assert "/members" in permissions.pages
|
||||
assert "/members/new" in permissions.pages
|
||||
assert "/members/:id" in permissions.pages
|
||||
assert "/members/:id/edit" in permissions.pages
|
||||
assert "/custom_field_values" in permissions.pages
|
||||
assert "/custom_field_values/:id" in permissions.pages
|
||||
assert "/custom_field_values/new" in permissions.pages
|
||||
assert "/custom_field_values/:id/edit" in permissions.pages
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_permissions/1 - :admin permission content" do
|
||||
test "allows User full CRUD with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:admin)
|
||||
|
||||
user_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :read end)
|
||||
|
||||
user_create =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :create end)
|
||||
|
||||
user_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :update end)
|
||||
|
||||
user_destroy =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "User" && p.action == :destroy end)
|
||||
|
||||
assert user_read.scope == :all
|
||||
assert user_read.granted == true
|
||||
assert user_create.scope == :all
|
||||
assert user_create.granted == true
|
||||
assert user_update.scope == :all
|
||||
assert user_update.granted == true
|
||||
assert user_destroy.scope == :all
|
||||
assert user_destroy.granted == true
|
||||
end
|
||||
|
||||
test "allows Member full CRUD with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:admin)
|
||||
|
||||
member_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :read end)
|
||||
|
||||
member_create =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :create end)
|
||||
|
||||
member_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Member" && p.action == :update end)
|
||||
|
||||
member_destroy =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "Member" && p.action == :destroy
|
||||
end)
|
||||
|
||||
assert member_read.scope == :all
|
||||
assert member_read.granted == true
|
||||
assert member_create.scope == :all
|
||||
assert member_create.granted == true
|
||||
assert member_update.scope == :all
|
||||
assert member_update.granted == true
|
||||
assert member_destroy.scope == :all
|
||||
assert member_destroy.granted == true
|
||||
end
|
||||
|
||||
test "allows CustomFieldValue full CRUD with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:admin)
|
||||
|
||||
cfv_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :read
|
||||
end)
|
||||
|
||||
cfv_create =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :create
|
||||
end)
|
||||
|
||||
cfv_update =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :update
|
||||
end)
|
||||
|
||||
cfv_destroy =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomFieldValue" && p.action == :destroy
|
||||
end)
|
||||
|
||||
assert cfv_read.scope == :all
|
||||
assert cfv_read.granted == true
|
||||
assert cfv_create.scope == :all
|
||||
assert cfv_create.granted == true
|
||||
assert cfv_update.scope == :all
|
||||
assert cfv_update.granted == true
|
||||
assert cfv_destroy.scope == :all
|
||||
assert cfv_destroy.granted == true
|
||||
end
|
||||
|
||||
test "allows CustomField full CRUD with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:admin)
|
||||
|
||||
cf_read =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomField" && p.action == :read
|
||||
end)
|
||||
|
||||
cf_create =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomField" && p.action == :create
|
||||
end)
|
||||
|
||||
cf_update =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomField" && p.action == :update
|
||||
end)
|
||||
|
||||
cf_destroy =
|
||||
Enum.find(permissions.resources, fn p ->
|
||||
p.resource == "CustomField" && p.action == :destroy
|
||||
end)
|
||||
|
||||
assert cf_read.scope == :all
|
||||
assert cf_read.granted == true
|
||||
assert cf_create.scope == :all
|
||||
assert cf_create.granted == true
|
||||
assert cf_update.scope == :all
|
||||
assert cf_update.granted == true
|
||||
assert cf_destroy.scope == :all
|
||||
assert cf_destroy.granted == true
|
||||
end
|
||||
|
||||
test "allows Role full CRUD with scope :all" do
|
||||
permissions = PermissionSets.get_permissions(:admin)
|
||||
|
||||
role_read =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Role" && p.action == :read end)
|
||||
|
||||
role_create =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Role" && p.action == :create end)
|
||||
|
||||
role_update =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Role" && p.action == :update end)
|
||||
|
||||
role_destroy =
|
||||
Enum.find(permissions.resources, fn p -> p.resource == "Role" && p.action == :destroy end)
|
||||
|
||||
assert role_read.scope == :all
|
||||
assert role_read.granted == true
|
||||
assert role_create.scope == :all
|
||||
assert role_create.granted == true
|
||||
assert role_update.scope == :all
|
||||
assert role_update.granted == true
|
||||
assert role_destroy.scope == :all
|
||||
assert role_destroy.granted == true
|
||||
end
|
||||
|
||||
test "has wildcard page permission" do
|
||||
permissions = PermissionSets.get_permissions(:admin)
|
||||
|
||||
assert "*" in permissions.pages
|
||||
end
|
||||
end
|
||||
|
||||
describe "valid_permission_set?/1" do
|
||||
test "returns true for valid permission set string" do
|
||||
assert PermissionSets.valid_permission_set?("own_data") == true
|
||||
assert PermissionSets.valid_permission_set?("read_only") == true
|
||||
assert PermissionSets.valid_permission_set?("normal_user") == true
|
||||
assert PermissionSets.valid_permission_set?("admin") == true
|
||||
end
|
||||
|
||||
test "returns true for valid permission set atom" do
|
||||
assert PermissionSets.valid_permission_set?(:own_data) == true
|
||||
assert PermissionSets.valid_permission_set?(:read_only) == true
|
||||
assert PermissionSets.valid_permission_set?(:normal_user) == true
|
||||
assert PermissionSets.valid_permission_set?(:admin) == true
|
||||
end
|
||||
|
||||
test "returns false for invalid permission set string" do
|
||||
assert PermissionSets.valid_permission_set?("invalid") == false
|
||||
assert PermissionSets.valid_permission_set?("") == false
|
||||
assert PermissionSets.valid_permission_set?("admin_user") == false
|
||||
end
|
||||
|
||||
test "returns false for invalid permission set atom" do
|
||||
assert PermissionSets.valid_permission_set?(:invalid) == false
|
||||
assert PermissionSets.valid_permission_set?(:unknown) == false
|
||||
end
|
||||
|
||||
test "returns false for nil input" do
|
||||
assert PermissionSets.valid_permission_set?(nil) == false
|
||||
end
|
||||
|
||||
test "returns false for invalid types" do
|
||||
assert PermissionSets.valid_permission_set?(123) == false
|
||||
assert PermissionSets.valid_permission_set?([]) == false
|
||||
assert PermissionSets.valid_permission_set?(%{}) == false
|
||||
assert PermissionSets.valid_permission_set?("") == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "permission_set_name_to_atom/1" do
|
||||
test "returns {:ok, atom} for valid permission set name" do
|
||||
assert PermissionSets.permission_set_name_to_atom("own_data") == {:ok, :own_data}
|
||||
assert PermissionSets.permission_set_name_to_atom("read_only") == {:ok, :read_only}
|
||||
assert PermissionSets.permission_set_name_to_atom("normal_user") == {:ok, :normal_user}
|
||||
assert PermissionSets.permission_set_name_to_atom("admin") == {:ok, :admin}
|
||||
end
|
||||
|
||||
test "returns {:error, :invalid_permission_set} for invalid permission set name" do
|
||||
assert PermissionSets.permission_set_name_to_atom("invalid") ==
|
||||
{:error, :invalid_permission_set}
|
||||
|
||||
assert PermissionSets.permission_set_name_to_atom("") == {:error, :invalid_permission_set}
|
||||
|
||||
assert PermissionSets.permission_set_name_to_atom("admin_user") ==
|
||||
{:error, :invalid_permission_set}
|
||||
end
|
||||
|
||||
test "handles non-existent atom gracefully" do
|
||||
# String.to_existing_atom will raise ArgumentError for non-existent atoms
|
||||
assert PermissionSets.permission_set_name_to_atom("nonexistent_atom_12345") ==
|
||||
{:error, :invalid_permission_set}
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_permissions/1 - error handling" do
|
||||
test "raises ArgumentError for invalid permission set with helpful message" do
|
||||
assert_raise ArgumentError,
|
||||
~r/invalid permission set: :invalid\. Must be one of:/,
|
||||
fn ->
|
||||
PermissionSets.get_permissions(:invalid)
|
||||
end
|
||||
end
|
||||
|
||||
test "error message includes all valid permission sets" do
|
||||
error =
|
||||
assert_raise ArgumentError, fn ->
|
||||
PermissionSets.get_permissions(:unknown)
|
||||
end
|
||||
|
||||
error_message = Exception.message(error)
|
||||
assert error_message =~ "own_data"
|
||||
assert error_message =~ "read_only"
|
||||
assert error_message =~ "normal_user"
|
||||
assert error_message =~ "admin"
|
||||
end
|
||||
end
|
||||
end
|
||||
97
test/mv/authorization/role_test.exs
Normal file
97
test/mv/authorization/role_test.exs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
defmodule Mv.Authorization.RoleTest do
|
||||
@moduledoc """
|
||||
Unit tests for Role resource validations and constraints.
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Authorization
|
||||
|
||||
describe "permission_set_name validation" do
|
||||
test "accepts valid permission set names" do
|
||||
attrs = %{
|
||||
name: "Test Role",
|
||||
permission_set_name: "own_data"
|
||||
}
|
||||
|
||||
assert {:ok, role} = Authorization.create_role(attrs)
|
||||
assert role.permission_set_name == "own_data"
|
||||
end
|
||||
|
||||
test "rejects invalid permission set names" do
|
||||
attrs = %{
|
||||
name: "Test Role",
|
||||
permission_set_name: "invalid_set"
|
||||
}
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Authorization.create_role(attrs)
|
||||
assert error_message(errors, :permission_set_name) =~ "must be one of"
|
||||
end
|
||||
|
||||
test "accepts all four valid permission sets" do
|
||||
valid_sets = ["own_data", "read_only", "normal_user", "admin"]
|
||||
|
||||
for permission_set <- valid_sets do
|
||||
attrs = %{
|
||||
name: "Role #{permission_set}",
|
||||
permission_set_name: permission_set
|
||||
}
|
||||
|
||||
assert {:ok, _role} = Authorization.create_role(attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "system role deletion protection" do
|
||||
test "prevents deletion of system roles" do
|
||||
# is_system_role is not settable via public API, so we use Ash.Changeset directly
|
||||
changeset =
|
||||
Mv.Authorization.Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
name: "System Role",
|
||||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|
||||
{:ok, system_role} = Ash.create(changeset)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Authorization.destroy_role(system_role)
|
||||
|
||||
message = error_message(errors, :is_system_role)
|
||||
assert message =~ "Cannot delete system role"
|
||||
end
|
||||
|
||||
test "allows deletion of non-system roles" do
|
||||
# is_system_role defaults to false, so regular create works
|
||||
{:ok, regular_role} =
|
||||
Authorization.create_role(%{
|
||||
name: "Regular Role",
|
||||
permission_set_name: "read_only"
|
||||
})
|
||||
|
||||
assert :ok = Authorization.destroy_role(regular_role)
|
||||
end
|
||||
end
|
||||
|
||||
describe "name uniqueness" do
|
||||
test "enforces unique role names" do
|
||||
attrs = %{
|
||||
name: "Unique Role",
|
||||
permission_set_name: "own_data"
|
||||
}
|
||||
|
||||
assert {:ok, _} = Authorization.create_role(attrs)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Authorization.create_role(attrs)
|
||||
assert error_message(errors, :name) =~ "has already been taken"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function for error evaluation
|
||||
defp error_message(errors, field) when is_atom(field) do
|
||||
errors
|
||||
|> Enum.filter(fn err -> Map.get(err, :field) == field end)
|
||||
|> Enum.map(&Map.get(&1, :message, ""))
|
||||
|> List.first() || ""
|
||||
end
|
||||
end
|
||||
219
test/mv_web/authorization_test.exs
Normal file
219
test/mv_web/authorization_test.exs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
defmodule MvWeb.AuthorizationTest do
|
||||
@moduledoc """
|
||||
Tests for UI-level authorization helpers.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias MvWeb.Authorization
|
||||
alias Mv.Membership.Member
|
||||
alias Mv.Accounts.User
|
||||
|
||||
describe "can?/3 with resource atom" do
|
||||
test "returns true when user has permission for resource+action" do
|
||||
admin = %{
|
||||
id: "admin-123",
|
||||
role: %{permission_set_name: "admin"}
|
||||
}
|
||||
|
||||
assert Authorization.can?(admin, :create, Mv.Membership.Member) == true
|
||||
assert Authorization.can?(admin, :read, Mv.Membership.Member) == true
|
||||
assert Authorization.can?(admin, :update, Mv.Membership.Member) == true
|
||||
assert Authorization.can?(admin, :destroy, Mv.Membership.Member) == true
|
||||
end
|
||||
|
||||
test "returns false when user lacks permission" do
|
||||
read_only_user = %{
|
||||
id: "read-only-123",
|
||||
role: %{permission_set_name: "read_only"}
|
||||
}
|
||||
|
||||
assert Authorization.can?(read_only_user, :create, Mv.Membership.Member) == false
|
||||
assert Authorization.can?(read_only_user, :read, Mv.Membership.Member) == true
|
||||
assert Authorization.can?(read_only_user, :update, Mv.Membership.Member) == false
|
||||
assert Authorization.can?(read_only_user, :destroy, Mv.Membership.Member) == false
|
||||
end
|
||||
|
||||
test "returns false for nil user" do
|
||||
assert Authorization.can?(nil, :create, Mv.Membership.Member) == false
|
||||
assert Authorization.can?(nil, :read, Mv.Membership.Member) == false
|
||||
end
|
||||
|
||||
test "admin can manage roles" do
|
||||
admin = %{
|
||||
id: "admin-123",
|
||||
role: %{permission_set_name: "admin"}
|
||||
}
|
||||
|
||||
assert Authorization.can?(admin, :create, Mv.Authorization.Role) == true
|
||||
assert Authorization.can?(admin, :read, Mv.Authorization.Role) == true
|
||||
assert Authorization.can?(admin, :update, Mv.Authorization.Role) == true
|
||||
assert Authorization.can?(admin, :destroy, Mv.Authorization.Role) == true
|
||||
end
|
||||
|
||||
test "non-admin cannot manage roles" do
|
||||
normal_user = %{
|
||||
id: "normal-123",
|
||||
role: %{permission_set_name: "normal_user"}
|
||||
}
|
||||
|
||||
assert Authorization.can?(normal_user, :create, Mv.Authorization.Role) == false
|
||||
assert Authorization.can?(normal_user, :read, Mv.Authorization.Role) == false
|
||||
assert Authorization.can?(normal_user, :update, Mv.Authorization.Role) == false
|
||||
assert Authorization.can?(normal_user, :destroy, Mv.Authorization.Role) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "can?/3 with record struct - scope :all" do
|
||||
test "admin can update any member" do
|
||||
admin = %{
|
||||
id: "admin-123",
|
||||
role: %{permission_set_name: "admin"}
|
||||
}
|
||||
|
||||
member1 = %Member{id: "member-1", user: %User{id: "other-user"}}
|
||||
member2 = %Member{id: "member-2", user: %User{id: "another-user"}}
|
||||
|
||||
assert Authorization.can?(admin, :update, member1) == true
|
||||
assert Authorization.can?(admin, :update, member2) == true
|
||||
end
|
||||
|
||||
test "normal_user can update any member" do
|
||||
normal_user = %{
|
||||
id: "normal-123",
|
||||
role: %{permission_set_name: "normal_user"}
|
||||
}
|
||||
|
||||
member = %Member{id: "member-1", user: %User{id: "other-user"}}
|
||||
|
||||
assert Authorization.can?(normal_user, :update, member) == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "can?/3 with record struct - scope :own" do
|
||||
test "user can update own User record" do
|
||||
user = %{
|
||||
id: "user-123",
|
||||
role: %{permission_set_name: "own_data"}
|
||||
}
|
||||
|
||||
own_user_record = %User{id: "user-123"}
|
||||
other_user_record = %User{id: "other-user"}
|
||||
|
||||
assert Authorization.can?(user, :update, own_user_record) == true
|
||||
assert Authorization.can?(user, :update, other_user_record) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "can?/3 with record struct - scope :linked" do
|
||||
test "user can update linked member" do
|
||||
user = %{
|
||||
id: "user-123",
|
||||
role: %{permission_set_name: "own_data"}
|
||||
}
|
||||
|
||||
# Member has_one :user (inverse relationship)
|
||||
linked_member = %Member{id: "member-1", user: %User{id: "user-123"}}
|
||||
unlinked_member = %Member{id: "member-2", user: nil}
|
||||
unlinked_member_other = %Member{id: "member-3", user: %User{id: "other-user"}}
|
||||
|
||||
assert Authorization.can?(user, :update, linked_member) == true
|
||||
assert Authorization.can?(user, :update, unlinked_member) == false
|
||||
assert Authorization.can?(user, :update, unlinked_member_other) == false
|
||||
end
|
||||
|
||||
test "user can update CustomFieldValue of linked member" do
|
||||
user = %{
|
||||
id: "user-123",
|
||||
role: %{permission_set_name: "own_data"}
|
||||
}
|
||||
|
||||
linked_cfv = %Mv.Membership.CustomFieldValue{
|
||||
id: "cfv-1",
|
||||
member: %Member{id: "member-1", user: %User{id: "user-123"}}
|
||||
}
|
||||
|
||||
unlinked_cfv = %Mv.Membership.CustomFieldValue{
|
||||
id: "cfv-2",
|
||||
member: %Member{id: "member-2", user: nil}
|
||||
}
|
||||
|
||||
unlinked_cfv_other = %Mv.Membership.CustomFieldValue{
|
||||
id: "cfv-3",
|
||||
member: %Member{id: "member-3", user: %User{id: "other-user"}}
|
||||
}
|
||||
|
||||
assert Authorization.can?(user, :update, linked_cfv) == true
|
||||
assert Authorization.can?(user, :update, unlinked_cfv) == false
|
||||
assert Authorization.can?(user, :update, unlinked_cfv_other) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "can_access_page?/2" do
|
||||
test "admin can access all pages via wildcard" do
|
||||
admin = %{
|
||||
id: "admin-123",
|
||||
role: %{permission_set_name: "admin"}
|
||||
}
|
||||
|
||||
assert Authorization.can_access_page?(admin, "/admin/roles") == true
|
||||
assert Authorization.can_access_page?(admin, "/members") == true
|
||||
assert Authorization.can_access_page?(admin, "/any/page") == true
|
||||
end
|
||||
|
||||
test "read_only user can access allowed pages" do
|
||||
read_only_user = %{
|
||||
id: "read-only-123",
|
||||
role: %{permission_set_name: "read_only"}
|
||||
}
|
||||
|
||||
assert Authorization.can_access_page?(read_only_user, "/") == true
|
||||
assert Authorization.can_access_page?(read_only_user, "/members") == true
|
||||
assert Authorization.can_access_page?(read_only_user, "/members/123") == true
|
||||
assert Authorization.can_access_page?(read_only_user, "/admin/roles") == false
|
||||
end
|
||||
|
||||
test "matches dynamic routes correctly" do
|
||||
read_only_user = %{
|
||||
id: "read-only-123",
|
||||
role: %{permission_set_name: "read_only"}
|
||||
}
|
||||
|
||||
assert Authorization.can_access_page?(read_only_user, "/members/123") == true
|
||||
assert Authorization.can_access_page?(read_only_user, "/members/abc") == true
|
||||
assert Authorization.can_access_page?(read_only_user, "/members/123/edit") == false
|
||||
end
|
||||
|
||||
test "returns false for nil user" do
|
||||
assert Authorization.can_access_page?(nil, "/members") == false
|
||||
assert Authorization.can_access_page?(nil, "/admin/roles") == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "error handling" do
|
||||
test "user without role returns false" do
|
||||
user_without_role = %{id: "user-123", role: nil}
|
||||
|
||||
assert Authorization.can?(user_without_role, :create, Mv.Membership.Member) == false
|
||||
assert Authorization.can_access_page?(user_without_role, "/members") == false
|
||||
end
|
||||
|
||||
test "user with invalid permission_set_name returns false" do
|
||||
user_with_invalid_permission = %{
|
||||
id: "user-123",
|
||||
role: %{permission_set_name: "invalid_set"}
|
||||
}
|
||||
|
||||
assert Authorization.can?(user_with_invalid_permission, :create, Mv.Membership.Member) ==
|
||||
false
|
||||
|
||||
assert Authorization.can_access_page?(user_with_invalid_permission, "/members") == false
|
||||
end
|
||||
|
||||
test "handles missing fields gracefully" do
|
||||
user_missing_role = %{id: "user-123"}
|
||||
|
||||
assert Authorization.can?(user_missing_role, :create, Mv.Membership.Member) == false
|
||||
assert Authorization.can_access_page?(user_missing_role, "/members") == false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -24,7 +24,6 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
:house_number,
|
||||
:postal_code,
|
||||
:city,
|
||||
:phone_number,
|
||||
:join_date
|
||||
]
|
||||
|
||||
|
|
@ -101,7 +100,6 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
assert has_element?(view, "[data-testid='street'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='house_number'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='postal_code'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='phone_number'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='join_date'] .opacity-40")
|
||||
end
|
||||
|
||||
|
|
|
|||
141
test/mv_web/helpers/member_helpers_test.exs
Normal file
141
test/mv_web/helpers/member_helpers_test.exs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
defmodule MvWeb.Helpers.MemberHelpersTest do
|
||||
@moduledoc """
|
||||
Tests for the display_name/1 helper function in MemberHelpers.
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Membership.Member
|
||||
alias MvWeb.Helpers.MemberHelpers
|
||||
|
||||
describe "display_name/1" do
|
||||
test "returns full name when both first_name and last_name are present" do
|
||||
member = %Member{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John Doe"
|
||||
end
|
||||
|
||||
test "returns email when both first_name and last_name are nil" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "returns first_name only when last_name is nil" do
|
||||
member = %Member{
|
||||
first_name: "John",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John"
|
||||
end
|
||||
|
||||
test "returns last_name only when first_name is nil" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "Doe"
|
||||
end
|
||||
|
||||
test "returns email when first_name and last_name are empty strings" do
|
||||
member = %Member{
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "returns email when first_name and last_name are whitespace only" do
|
||||
member = %Member{
|
||||
first_name: " ",
|
||||
last_name: " \t ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "trims whitespace from name parts" do
|
||||
member = %Member{
|
||||
first_name: " John ",
|
||||
last_name: " Doe ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John Doe"
|
||||
end
|
||||
|
||||
test "handles one empty string and one nil" do
|
||||
member = %Member{
|
||||
first_name: "",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "handles one nil and one empty string" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: "",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "handles one whitespace and one nil" do
|
||||
member = %Member{
|
||||
first_name: " ",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "handles one valid name and one whitespace" do
|
||||
member = %Member{
|
||||
first_name: "John",
|
||||
last_name: " ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John"
|
||||
end
|
||||
|
||||
test "handles member with only first_name containing whitespace" do
|
||||
member = %Member{
|
||||
first_name: " John ",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John"
|
||||
end
|
||||
|
||||
test "handles member with only last_name containing whitespace" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: " Doe ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "Doe"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -154,7 +154,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
|> render_click()
|
||||
|
||||
# Should show success message
|
||||
assert render(view) =~ "Custom field deleted successfully"
|
||||
assert render(view) =~ "Data field deleted successfully"
|
||||
|
||||
# Custom field should be gone from database
|
||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
||||
|
|
|
|||
|
|
@ -64,5 +64,21 @@ defmodule MvWeb.GlobalSettingsLiveTest do
|
|||
|
||||
assert html =~ "must be present"
|
||||
end
|
||||
|
||||
test "displays Memberdata section", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
assert html =~ "Memberdata" or html =~ "Member Data"
|
||||
end
|
||||
|
||||
test "displays flash message after member field visibility update", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
# Simulate member field visibility update
|
||||
send(view.pid, {:member_field_visibility_updated})
|
||||
|
||||
# Check for flash message
|
||||
assert render(view) =~ "updated" or render(view) =~ "success"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
124
test/mv_web/live/member_field_live/index_component_test.exs
Normal file
124
test/mv_web/live/member_field_live/index_component_test.exs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
defmodule MvWeb.MemberFieldLive.IndexComponentTest do
|
||||
@moduledoc """
|
||||
Tests for MemberFieldLive.IndexComponent.
|
||||
|
||||
Tests cover:
|
||||
- Rendering all member fields from Mv.Constants.member_fields()
|
||||
- Displaying show_in_overview status as badge (Yes/No)
|
||||
- Displaying required status for required fields (first_name, last_name, email)
|
||||
- Current status is displayed based on settings.member_field_visibility
|
||||
- Default status is "Yes" (visible) when not configured in settings
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Membership
|
||||
|
||||
setup %{conn: conn} do
|
||||
user = create_test_user(%{email: "admin@example.com"})
|
||||
conn = conn_with_oidc_user(conn, user)
|
||||
{:ok, conn: conn, user: user}
|
||||
end
|
||||
|
||||
describe "rendering" do
|
||||
test "renders all member fields from Constants", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# Check that all member fields are displayed
|
||||
member_fields = Mv.Constants.member_fields()
|
||||
|
||||
for field <- member_fields do
|
||||
field_name = String.replace(Atom.to_string(field), "_", " ") |> String.capitalize()
|
||||
# Field name should appear in the table (either as label or in some form)
|
||||
assert html =~ field_name or html =~ Atom.to_string(field)
|
||||
end
|
||||
end
|
||||
|
||||
test "displays show_in_overview status as badge", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# Should have "Show in overview" column header
|
||||
assert html =~ "Show in overview" or html =~ "Show in Overview"
|
||||
|
||||
# Should have badge elements (Yes/No)
|
||||
assert html =~ "badge" or html =~ "Yes" or html =~ "No"
|
||||
end
|
||||
|
||||
test "displays required status for required fields", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# Required fields: first_name, last_name, email
|
||||
# Should have "Required" column or indicator
|
||||
assert html =~ "Required" or html =~ "required"
|
||||
end
|
||||
|
||||
test "shows default status as Yes when not configured", %{conn: conn} do
|
||||
# Ensure settings have no member_field_visibility configured
|
||||
{:ok, settings} = Membership.get_settings()
|
||||
|
||||
{:ok, _updated} =
|
||||
Membership.update_settings(settings, %{member_field_visibility: %{}})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# All fields should show as visible (Yes) by default
|
||||
# Check for "Yes" badge or similar indicator
|
||||
assert html =~ "Yes" or html =~ "badge-success"
|
||||
end
|
||||
|
||||
test "shows configured visibility status from settings", %{conn: conn} do
|
||||
# Configure some fields as hidden
|
||||
{:ok, settings} = Membership.get_settings()
|
||||
visibility_config = %{"street" => false, "house_number" => false}
|
||||
|
||||
{:ok, _updated} =
|
||||
Membership.update_member_field_visibility(settings, visibility_config)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# Street and house_number should show as hidden (No)
|
||||
# Other fields should show as visible (Yes)
|
||||
assert html =~ "street" or html =~ "Street"
|
||||
assert html =~ "house_number" or html =~ "House number"
|
||||
end
|
||||
end
|
||||
|
||||
describe "required fields" do
|
||||
test "marks first_name as required", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# first_name should be marked as required
|
||||
assert html =~ "first_name" or html =~ "First name"
|
||||
# Should have required indicator
|
||||
assert html =~ "required" or html =~ "Required"
|
||||
end
|
||||
|
||||
test "marks last_name as required", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# last_name should be marked as required
|
||||
assert html =~ "last_name" or html =~ "Last name"
|
||||
# Should have required indicator
|
||||
assert html =~ "required" or html =~ "Required"
|
||||
end
|
||||
|
||||
test "marks email as required", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# email should be marked as required
|
||||
assert html =~ "email" or html =~ "Email"
|
||||
# Should have required indicator
|
||||
assert html =~ "required" or html =~ "Required"
|
||||
end
|
||||
|
||||
test "does not mark optional fields as required", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/settings")
|
||||
|
||||
# Optional fields should not have required indicator
|
||||
# Check that street (optional) doesn't have required badge
|
||||
# This test verifies that only required fields show the indicator
|
||||
assert html =~ "street" or html =~ "Street"
|
||||
end
|
||||
end
|
||||
end
|
||||
452
test/mv_web/live/role_live_test.exs
Normal file
452
test/mv_web/live/role_live_test.exs
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
defmodule MvWeb.RoleLiveTest do
|
||||
@moduledoc """
|
||||
Tests for role management LiveViews.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Authorization
|
||||
alias Mv.Authorization.Role
|
||||
|
||||
# Helper to create a role
|
||||
defp create_role(attrs \\ %{}) do
|
||||
default_attrs = %{
|
||||
name: "Test Role #{System.unique_integer([:positive])}",
|
||||
description: "Test description",
|
||||
permission_set_name: "read_only"
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
case Authorization.create_role(attrs) do
|
||||
{:ok, role} -> role
|
||||
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to create admin user with admin role
|
||||
defp create_admin_user(conn) do
|
||||
# Create admin role
|
||||
admin_role =
|
||||
case Authorization.list_roles() do
|
||||
{:ok, roles} ->
|
||||
case Enum.find(roles, &(&1.name == "Admin")) do
|
||||
nil ->
|
||||
# Create admin role if it doesn't exist
|
||||
create_role(%{
|
||||
name: "Admin",
|
||||
description: "Administrator with full access",
|
||||
permission_set_name: "admin"
|
||||
})
|
||||
|
||||
role ->
|
||||
role
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Create admin role if list_roles fails
|
||||
create_role(%{
|
||||
name: "Admin",
|
||||
description: "Administrator with full access",
|
||||
permission_set_name: "admin"
|
||||
})
|
||||
end
|
||||
|
||||
# Create user
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Assign admin role using manage_relationship
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|
||||
# Load role for authorization checks (must be loaded for can?/3 to work)
|
||||
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts)
|
||||
|
||||
# Store user with role in session for LiveView
|
||||
conn = conn_with_password_user(conn, user_with_role)
|
||||
{conn, user_with_role, admin_role}
|
||||
end
|
||||
|
||||
# Helper to create non-admin user
|
||||
defp create_non_admin_user(conn) do
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||
email: "user#{System.unique_integer([:positive])}@mv.local",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
conn = conn_with_password_user(conn, user)
|
||||
{conn, user}
|
||||
end
|
||||
|
||||
describe "index page" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
end
|
||||
|
||||
test "mounts successfully", %{conn: conn} do
|
||||
{:ok, _view, _html} = live(conn, "/admin/roles")
|
||||
end
|
||||
|
||||
test "loads all roles from database", %{conn: conn} do
|
||||
role1 = create_role(%{name: "Role 1"})
|
||||
role2 = create_role(%{name: "Role 2"})
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles")
|
||||
|
||||
assert html =~ role1.name
|
||||
assert html =~ role2.name
|
||||
end
|
||||
|
||||
test "shows table with role names", %{conn: conn} do
|
||||
role = create_role(%{name: "Test Role"})
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles")
|
||||
|
||||
assert html =~ role.name
|
||||
assert html =~ role.description
|
||||
assert html =~ role.permission_set_name
|
||||
end
|
||||
|
||||
test "shows system role badge", %{conn: conn} do
|
||||
_system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
name: "System Role",
|
||||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles")
|
||||
|
||||
assert html =~ "System Role" || html =~ "system"
|
||||
end
|
||||
|
||||
test "delete button disabled for system roles", %{conn: conn} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
name: "System Role",
|
||||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/admin/roles")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"button[phx-click='delete'][phx-value-id='#{system_role.id}'][disabled]"
|
||||
) ||
|
||||
not has_element?(
|
||||
view,
|
||||
"button[phx-click='delete'][phx-value-id='#{system_role.id}']"
|
||||
)
|
||||
end
|
||||
|
||||
test "delete button enabled for non-system roles", %{conn: conn} do
|
||||
role = create_role()
|
||||
|
||||
{:ok, view, html} = live(conn, "/admin/roles")
|
||||
|
||||
# Delete is a link with phx-click containing delete event
|
||||
# Check if delete link exists in HTML (phx-click contains delete and role id)
|
||||
assert (html =~ "phx-click" && html =~ "delete" && html =~ role.id) ||
|
||||
has_element?(view, "a[phx-click*='delete'][phx-value-id='#{role.id}']") ||
|
||||
has_element?(view, "a[aria-label='Delete role']")
|
||||
end
|
||||
|
||||
test "new role button navigates to form", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, "/admin/roles")
|
||||
|
||||
# Check if button exists (admin should see it)
|
||||
if html =~ "New Role" do
|
||||
{:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> element("a[href='/admin/roles/new'], button[href='/admin/roles/new']")
|
||||
|> render_click()
|
||||
|
||||
assert to == "/admin/roles/new"
|
||||
else
|
||||
# If button not visible, user doesn't have permission (expected for non-admin)
|
||||
# This test assumes admin user, so button should be visible
|
||||
flunk("New Role button not found - user may not have admin role loaded")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "show page" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
end
|
||||
|
||||
test "mounts with valid role ID", %{conn: conn} do
|
||||
role = create_role()
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||
|
||||
assert html =~ role.name
|
||||
assert html =~ role.description
|
||||
assert html =~ role.permission_set_name
|
||||
end
|
||||
|
||||
test "returns 404 for invalid role ID", %{conn: conn} do
|
||||
invalid_id = Ecto.UUID.generate()
|
||||
|
||||
# Should redirect to index with error message
|
||||
# redirect in mount returns {:error, {:redirect, ...}}
|
||||
result = live(conn, "/admin/roles/#{invalid_id}")
|
||||
|
||||
assert match?({:error, {:redirect, %{to: "/admin/roles"}}}, result)
|
||||
end
|
||||
|
||||
test "shows system role badge if is_system_role is true", %{conn: conn} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
name: "System Role",
|
||||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|
||||
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
|
||||
|
||||
assert html =~ "System Role" || html =~ "system"
|
||||
end
|
||||
end
|
||||
|
||||
describe "form - create" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
end
|
||||
|
||||
test "mounts successfully", %{conn: conn} do
|
||||
{:ok, _view, _html} = live(conn, "/admin/roles/new")
|
||||
end
|
||||
|
||||
test "form dropdown shows all 4 permission sets", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, "/admin/roles/new")
|
||||
|
||||
assert html =~ "own_data"
|
||||
assert html =~ "read_only"
|
||||
assert html =~ "normal_user"
|
||||
assert html =~ "admin"
|
||||
end
|
||||
|
||||
test "creates new role with valid data", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/admin/roles/new")
|
||||
|
||||
attrs = %{
|
||||
"name" => "New Role",
|
||||
"description" => "New description",
|
||||
"permission_set_name" => "read_only"
|
||||
}
|
||||
|
||||
view
|
||||
|> form("#role-form", role: attrs)
|
||||
|> render_submit()
|
||||
|
||||
# Should redirect to index or show page
|
||||
assert_redirect(view, "/admin/roles")
|
||||
end
|
||||
|
||||
test "shows error with invalid permission_set_name", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/admin/roles/new")
|
||||
|
||||
# Try to submit with empty permission_set_name (invalid)
|
||||
attrs = %{
|
||||
"name" => "New Role",
|
||||
"description" => "New description",
|
||||
"permission_set_name" => ""
|
||||
}
|
||||
|
||||
view
|
||||
|> form("#role-form", role: attrs)
|
||||
|> render_submit()
|
||||
|
||||
# Should show validation error
|
||||
html = render(view)
|
||||
assert html =~ "error" || html =~ "required" || html =~ "Permission Set"
|
||||
end
|
||||
|
||||
test "shows flash message after successful creation", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/admin/roles/new")
|
||||
|
||||
attrs = %{
|
||||
"name" => "New Role #{System.unique_integer([:positive])}",
|
||||
"description" => "New description",
|
||||
"permission_set_name" => "read_only"
|
||||
}
|
||||
|
||||
view
|
||||
|> form("#role-form", role: attrs)
|
||||
|> render_submit()
|
||||
|
||||
# Should redirect to index
|
||||
assert_redirect(view, "/admin/roles")
|
||||
end
|
||||
end
|
||||
|
||||
describe "form - edit" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
role = create_role()
|
||||
%{conn: conn, user: user, role: role}
|
||||
end
|
||||
|
||||
test "mounts with valid role ID", %{conn: conn, role: role} do
|
||||
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}/edit")
|
||||
|
||||
assert html =~ role.name
|
||||
end
|
||||
|
||||
test "returns 404 for invalid role ID in edit", %{conn: conn} do
|
||||
invalid_id = Ecto.UUID.generate()
|
||||
|
||||
# Should redirect to index with error message
|
||||
# redirect in mount returns {:error, {:redirect, ...}}
|
||||
result = live(conn, "/admin/roles/#{invalid_id}/edit")
|
||||
|
||||
assert match?({:error, {:redirect, %{to: "/admin/roles"}}}, result)
|
||||
end
|
||||
|
||||
test "updates role name", %{conn: conn, role: role} do
|
||||
{:ok, view, _html} = live(conn, "/admin/roles/#{role.id}/edit?return_to=show")
|
||||
|
||||
attrs = %{
|
||||
"name" => "Updated Role Name",
|
||||
"description" => role.description,
|
||||
"permission_set_name" => role.permission_set_name
|
||||
}
|
||||
|
||||
view
|
||||
|> form("#role-form", role: attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert_redirect(view, "/admin/roles/#{role.id}")
|
||||
|
||||
# Verify update
|
||||
{:ok, updated_role} = Authorization.get_role(role.id)
|
||||
assert updated_role.name == "Updated Role Name"
|
||||
end
|
||||
|
||||
test "updates system role's permission_set_name", %{conn: conn} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
name: "System Role",
|
||||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/admin/roles/#{system_role.id}/edit?return_to=show")
|
||||
|
||||
attrs = %{
|
||||
"name" => system_role.name,
|
||||
"description" => system_role.description,
|
||||
"permission_set_name" => "read_only"
|
||||
}
|
||||
|
||||
view
|
||||
|> form("#role-form", role: attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert_redirect(view, "/admin/roles/#{system_role.id}")
|
||||
|
||||
# Verify update
|
||||
{:ok, updated_role} = Authorization.get_role(system_role.id)
|
||||
assert updated_role.permission_set_name == "read_only"
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete functionality" do
|
||||
setup %{conn: conn} do
|
||||
{conn, user, _admin_role} = create_admin_user(conn)
|
||||
%{conn: conn, user: user}
|
||||
end
|
||||
|
||||
test "deletes non-system role", %{conn: conn} do
|
||||
role = create_role()
|
||||
|
||||
{:ok, view, html} = live(conn, "/admin/roles")
|
||||
|
||||
# Delete is a link - JS.push creates phx-click with value containing id
|
||||
# Verify the role id is in the HTML (in phx-click value)
|
||||
assert html =~ role.id
|
||||
|
||||
# Send delete event directly to avoid selector issues with multiple delete buttons
|
||||
render_click(view, "delete", %{"id" => role.id})
|
||||
|
||||
# Verify deletion by checking database
|
||||
assert {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{}]}} =
|
||||
Authorization.get_role(role.id)
|
||||
end
|
||||
|
||||
test "fails to delete system role with error message", %{conn: conn} do
|
||||
system_role =
|
||||
Role
|
||||
|> Ash.Changeset.for_create(:create_role, %{
|
||||
name: "System Role",
|
||||
permission_set_name: "own_data"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||
|> Ash.create!()
|
||||
|
||||
{:ok, view, html} = live(conn, "/admin/roles")
|
||||
|
||||
# System role delete button should be disabled
|
||||
assert html =~ "disabled" || html =~ "cursor-not-allowed" ||
|
||||
html =~ "System roles cannot be deleted"
|
||||
|
||||
# Try to delete via event (backend check)
|
||||
render_click(view, "delete", %{"id" => system_role.id})
|
||||
|
||||
# Should show error message
|
||||
assert render(view) =~ "System roles cannot be deleted"
|
||||
|
||||
# Role should still exist
|
||||
{:ok, _role} = Authorization.get_role(system_role.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "authorization" do
|
||||
test "only admin can access /admin/roles", %{conn: conn} do
|
||||
{conn, _user} = create_non_admin_user(conn)
|
||||
|
||||
# Non-admin should be redirected or see error
|
||||
# Note: Authorization is checked via can_access_page? which returns false
|
||||
# The page might still mount but show no content or redirect
|
||||
# For now, we just verify the page doesn't work as expected for non-admin
|
||||
{:ok, _view, html} = live(conn, "/admin/roles")
|
||||
|
||||
# Non-admin should not see "New Role" button (can? returns false)
|
||||
# But the button might still be in HTML, just hidden or disabled
|
||||
# We verify that the page loads but admin features are restricted
|
||||
assert html =~ "Listing Roles" || html =~ "Roles"
|
||||
end
|
||||
|
||||
test "admin can access /admin/roles", %{conn: conn} do
|
||||
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||
|
||||
{:ok, _view, _html} = live(conn, "/admin/roles")
|
||||
end
|
||||
end
|
||||
end
|
||||
141
test/mv_web/member_live/index_display_name_test.exs
Normal file
141
test/mv_web/member_live/index_display_name_test.exs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
defmodule MvWeb.Helpers.MemberHelpersTest do
|
||||
@moduledoc """
|
||||
Tests for the display_name/1 helper function in MemberHelpers.
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Membership.Member
|
||||
alias MvWeb.Helpers.MemberHelpers
|
||||
|
||||
describe "display_name/1" do
|
||||
test "returns full name when both first_name and last_name are present" do
|
||||
member = %Member{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John Doe"
|
||||
end
|
||||
|
||||
test "returns email when both first_name and last_name are nil" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "returns first_name only when last_name is nil" do
|
||||
member = %Member{
|
||||
first_name: "John",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John"
|
||||
end
|
||||
|
||||
test "returns last_name only when first_name is nil" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: "Doe",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "Doe"
|
||||
end
|
||||
|
||||
test "returns email when first_name and last_name are empty strings" do
|
||||
member = %Member{
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "returns email when first_name and last_name are whitespace only" do
|
||||
member = %Member{
|
||||
first_name: " ",
|
||||
last_name: " \t ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "trims whitespace from name parts" do
|
||||
member = %Member{
|
||||
first_name: " John ",
|
||||
last_name: " Doe ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John Doe"
|
||||
end
|
||||
|
||||
test "handles one empty string and one nil" do
|
||||
member = %Member{
|
||||
first_name: "",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "handles one nil and one empty string" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: "",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "handles one whitespace and one nil" do
|
||||
member = %Member{
|
||||
first_name: " ",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "john@example.com"
|
||||
end
|
||||
|
||||
test "handles one valid name and one whitespace" do
|
||||
member = %Member{
|
||||
first_name: "John",
|
||||
last_name: " ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John"
|
||||
end
|
||||
|
||||
test "handles member with only first_name containing whitespace" do
|
||||
member = %Member{
|
||||
first_name: " John ",
|
||||
last_name: nil,
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "John"
|
||||
end
|
||||
|
||||
test "handles member with only last_name containing whitespace" do
|
||||
member = %Member{
|
||||
first_name: nil,
|
||||
last_name: " Doe ",
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
assert MemberHelpers.display_name(member) == "Doe"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -16,7 +16,6 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
|
|||
house_number: "123",
|
||||
postal_code: "12345",
|
||||
city: "Berlin",
|
||||
phone_number: "+49123456789",
|
||||
join_date: ~D[2020-01-15]
|
||||
})
|
||||
|> Ash.create()
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
:house_number,
|
||||
:postal_code,
|
||||
:city,
|
||||
:phone_number,
|
||||
:join_date
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue