diff --git a/lib/mv/authorization/permission_sets.ex b/lib/mv/authorization/permission_sets.ex index d01e285..22b1648 100644 --- a/lib/mv/authorization/permission_sets.ex +++ b/lib/mv/authorization/permission_sets.ex @@ -2,23 +2,60 @@ defmodule Mv.Authorization.PermissionSets do @moduledoc """ Defines the four hardcoded permission sets for the application. - This is a minimal stub implementation. The full implementation - with all permission details will be added in a subsequent issue. + Each permission set specifies: + - Resource permissions (what CRUD operations on which resources) + - Page permissions (which LiveView pages can be accessed) + - Scopes (own, linked, all) ## Permission Sets 1. **own_data** - Default for "Mitglied" role + - Can only access own user data and linked member/custom field values + - Cannot create new members or manage system + 2. **read_only** - For "Vorstand" and "Buchhaltung" roles + - Can read all member data + - Cannot create, update, or delete + 3. **normal_user** - For "Kassenwart" role + - Create/Read/Update members (no delete for safety), full CRUD on custom field values + - Cannot manage custom fields or users + 4. **admin** - For "Admin" role + - Unrestricted access to all resources + - Can manage users, roles, custom fields ## Usage - # Get list of all valid permission set names - PermissionSets.all_permission_sets() - # => [:own_data, :read_only, :normal_user, :admin] + # Get permissions for a role's permission set + permissions = PermissionSets.get_permissions(:admin) + + # Check if a permission set name is valid + PermissionSets.valid_permission_set?("read_only") # => true + + # Convert string to atom safely + {:ok, atom} = PermissionSets.permission_set_name_to_atom("own_data") + + ## Performance + + All functions are pure and compile-time. Permission lookups are < 1 microsecond. """ + @type scope :: :own | :linked | :all + @type action :: :read | :create | :update | :destroy + + @type resource_permission :: %{ + resource: String.t(), + action: action(), + scope: scope(), + granted: boolean() + } + + @type permission_set :: %{ + resources: [resource_permission()], + pages: [String.t()] + } + @doc """ Returns the list of all valid permission set names. @@ -31,4 +68,207 @@ defmodule Mv.Authorization.PermissionSets do def all_permission_sets do [:own_data, :read_only, :normal_user, :admin] end + + @doc """ + Returns permissions for the given permission set. + + ## Examples + + iex> permissions = PermissionSets.get_permissions(:admin) + iex> Enum.any?(permissions.resources, fn p -> + ...> p.resource == "User" and p.action == :destroy + ...> end) + true + + iex> PermissionSets.get_permissions(:invalid) + ** (FunctionClauseError) no function clause matching + """ + @spec get_permissions(atom()) :: permission_set() + + def get_permissions(:own_data) do + %{ + resources: [ + # User: Can always read/update own credentials + %{resource: "User", action: :read, scope: :own, granted: true}, + %{resource: "User", action: :update, scope: :own, granted: true}, + + # Member: Can read/update linked member + %{resource: "Member", action: :read, scope: :linked, granted: true}, + %{resource: "Member", action: :update, scope: :linked, granted: true}, + + # CustomFieldValue: Can read/update custom field values of linked member + %{resource: "CustomFieldValue", action: :read, scope: :linked, granted: true}, + %{resource: "CustomFieldValue", action: :update, scope: :linked, granted: true}, + + # CustomField: Can read all (needed for forms) + %{resource: "CustomField", action: :read, scope: :all, granted: true} + ], + pages: [ + # Home page + "/", + # Own profile + "/profile", + # Linked member detail (filtered by policy) + "/members/:id" + ] + } + end + + def get_permissions(:read_only) do + %{ + resources: [ + # User: Can read/update own credentials only + %{resource: "User", action: :read, scope: :own, granted: true}, + %{resource: "User", action: :update, scope: :own, granted: true}, + + # Member: Can read all members, no modifications + %{resource: "Member", action: :read, scope: :all, granted: true}, + + # CustomFieldValue: Can read all custom field values + %{resource: "CustomFieldValue", action: :read, scope: :all, granted: true}, + + # CustomField: Can read all + %{resource: "CustomField", action: :read, scope: :all, granted: true} + ], + pages: [ + "/", + # Member list + "/members", + # Member detail + "/members/:id", + # Custom field values overview + "/custom_field_values" + ] + } + end + + def get_permissions(:normal_user) do + %{ + resources: [ + # User: Can read/update own credentials only + %{resource: "User", action: :read, scope: :own, granted: true}, + %{resource: "User", action: :update, scope: :own, granted: true}, + + # Member: Full CRUD except destroy (safety) + %{resource: "Member", action: :read, scope: :all, granted: true}, + %{resource: "Member", action: :create, scope: :all, granted: true}, + %{resource: "Member", action: :update, scope: :all, granted: true}, + # Note: destroy intentionally omitted for safety + + # CustomFieldValue: Full CRUD + %{resource: "CustomFieldValue", action: :read, scope: :all, granted: true}, + %{resource: "CustomFieldValue", action: :create, scope: :all, granted: true}, + %{resource: "CustomFieldValue", action: :update, scope: :all, granted: true}, + %{resource: "CustomFieldValue", action: :destroy, scope: :all, granted: true}, + + # CustomField: Read only (admin manages definitions) + %{resource: "CustomField", action: :read, scope: :all, granted: true} + ], + pages: [ + "/", + "/members", + # Create member + "/members/new", + "/members/:id", + # Edit member + "/members/:id/edit", + "/custom_field_values", + "/custom_field_values/new", + "/custom_field_values/:id/edit" + ] + } + end + + def get_permissions(:admin) do + %{ + resources: [ + # User: Full management including other users + %{resource: "User", action: :read, scope: :all, granted: true}, + %{resource: "User", action: :create, scope: :all, granted: true}, + %{resource: "User", action: :update, scope: :all, granted: true}, + %{resource: "User", action: :destroy, scope: :all, granted: true}, + + # Member: Full CRUD + %{resource: "Member", action: :read, scope: :all, granted: true}, + %{resource: "Member", action: :create, scope: :all, granted: true}, + %{resource: "Member", action: :update, scope: :all, granted: true}, + %{resource: "Member", action: :destroy, scope: :all, granted: true}, + + # CustomFieldValue: Full CRUD + %{resource: "CustomFieldValue", action: :read, scope: :all, granted: true}, + %{resource: "CustomFieldValue", action: :create, scope: :all, granted: true}, + %{resource: "CustomFieldValue", action: :update, scope: :all, granted: true}, + %{resource: "CustomFieldValue", action: :destroy, scope: :all, granted: true}, + + # CustomField: Full CRUD (admin manages custom field definitions) + %{resource: "CustomField", action: :read, scope: :all, granted: true}, + %{resource: "CustomField", action: :create, scope: :all, granted: true}, + %{resource: "CustomField", action: :update, scope: :all, granted: true}, + %{resource: "CustomField", action: :destroy, scope: :all, granted: true}, + + # Role: Full CRUD (admin manages roles) + %{resource: "Role", action: :read, scope: :all, granted: true}, + %{resource: "Role", action: :create, scope: :all, granted: true}, + %{resource: "Role", action: :update, scope: :all, granted: true}, + %{resource: "Role", action: :destroy, scope: :all, granted: true} + ], + pages: [ + # Wildcard: Admin can access all pages + "*" + ] + } + end + + @doc """ + Checks if a permission set name (string or atom) is valid. + + ## Examples + + iex> PermissionSets.valid_permission_set?("admin") + true + + iex> PermissionSets.valid_permission_set?(:read_only) + true + + iex> PermissionSets.valid_permission_set?("invalid") + false + """ + @spec valid_permission_set?(String.t() | atom()) :: boolean() + def valid_permission_set?(name) when is_binary(name) do + case permission_set_name_to_atom(name) do + {:ok, _atom} -> true + {:error, _} -> false + end + end + + def valid_permission_set?(name) when is_atom(name) do + name in all_permission_sets() + end + + def valid_permission_set?(_), do: false + + @doc """ + Converts a permission set name string to atom safely. + + ## Examples + + iex> PermissionSets.permission_set_name_to_atom("admin") + {:ok, :admin} + + iex> PermissionSets.permission_set_name_to_atom("invalid") + {:error, :invalid_permission_set} + """ + @spec permission_set_name_to_atom(String.t()) :: + {:ok, atom()} | {:error, :invalid_permission_set} + def permission_set_name_to_atom(name) when is_binary(name) do + atom = String.to_existing_atom(name) + + if valid_permission_set?(atom) do + {:ok, atom} + else + {:error, :invalid_permission_set} + end + rescue + ArgumentError -> {:error, :invalid_permission_set} + end end diff --git a/test/mv/authorization/permission_sets_test.exs b/test/mv/authorization/permission_sets_test.exs new file mode 100644 index 0000000..51dc797 --- /dev/null +++ b/test/mv/authorization/permission_sets_test.exs @@ -0,0 +1,568 @@ +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 "returns map with :resources and :pages keys for :own_data" do + permissions = PermissionSets.get_permissions(:own_data) + + assert Map.has_key?(permissions, :resources) + assert Map.has_key?(permissions, :pages) + assert is_list(permissions.resources) + assert is_list(permissions.pages) + end + + test "returns map with :resources and :pages keys for :read_only" do + permissions = PermissionSets.get_permissions(:read_only) + + assert Map.has_key?(permissions, :resources) + assert Map.has_key?(permissions, :pages) + assert is_list(permissions.resources) + assert is_list(permissions.pages) + end + + test "returns map with :resources and :pages keys for :normal_user" do + permissions = PermissionSets.get_permissions(:normal_user) + + assert Map.has_key?(permissions, :resources) + assert Map.has_key?(permissions, :pages) + assert is_list(permissions.resources) + assert is_list(permissions.pages) + end + + test "returns map with :resources and :pages keys for :admin" do + permissions = PermissionSets.get_permissions(:admin) + + assert Map.has_key?(permissions, :resources) + assert Map.has_key?(permissions, :pages) + assert is_list(permissions.resources) + assert is_list(permissions.pages) + 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 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 "/members" in permissions.pages + assert "/members/:id" in permissions.pages + assert "/custom_field_values" 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 "/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/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 + 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 +end