@@ -81,18 +67,6 @@ defmodule MvWeb.UserLive.Form do
<%= if @show_password_fields do %>
- <%= if @user && MvWeb.Helpers.UserHelpers.has_oidc?(@user) do %>
-
-
- {gettext("SSO / OIDC user")}
-
-
- {gettext(
- "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
- )}
-
-
- <% end %>
<.input
field={@form[:password]}
label={gettext("Password")}
@@ -326,9 +300,6 @@ defmodule MvWeb.UserLive.Form do
# Only admins can link/unlink users to members (permission docs; prevents privilege escalation).
can_manage_member_linking = can?(actor, :destroy, Mv.Accounts.User)
- # Only admins can assign user roles (Role update permission).
- can_assign_role = can?(actor, :update, Mv.Authorization.Role)
- roles = if can_assign_role, do: load_roles(actor), else: []
{:ok,
socket
@@ -336,8 +307,6 @@ defmodule MvWeb.UserLive.Form do
|> assign(user: user)
|> assign(:page_title, page_title)
|> assign(:can_manage_member_linking, can_manage_member_linking)
- |> assign(:can_assign_role, can_assign_role)
- |> assign(:roles, roles)
|> assign(:show_password_fields, false)
|> assign(:member_search_query, "")
|> assign(:available_members, [])
@@ -388,10 +357,7 @@ defmodule MvWeb.UserLive.Form do
def handle_event("save", %{"user" => user_params}, socket) do
actor = current_actor(socket)
- # Include current member in params when not linking/unlinking so update_user's
- # manage_relationship(on_missing: :unrelate) does not accidentally unlink.
- user_params = params_with_member_if_unchanged(socket, user_params)
-
+ # First save the user without member changes
case submit_form(socket.assigns.form, user_params, actor) do
{:ok, user} ->
handle_member_linking(socket, user, actor)
@@ -563,20 +529,6 @@ defmodule MvWeb.UserLive.Form do
defp get_action_name(:update), do: gettext("updated")
defp get_action_name(other), do: to_string(other)
- # When user has a linked member and we are not linking/unlinking, include current member in params
- # so update_user's manage_relationship(on_missing: :unrelate) does not unlink the member.
- defp params_with_member_if_unchanged(socket, params) do
- user = socket.assigns.user
- linking = socket.assigns.selected_member_id
- unlinking = socket.assigns[:unlink_member]
-
- if user && user.member_id && !linking && !unlinking do
- Map.put(params, "member", %{"id" => user.member_id})
- else
- params
- end
- end
-
defp handle_member_link_error(socket, error) do
error_message = extract_error_message(error)
@@ -620,8 +572,7 @@ defmodule MvWeb.UserLive.Form do
assigns: %{
user: user,
show_password_fields: show_password_fields,
- can_manage_member_linking: can_manage_member_linking,
- can_assign_role: can_assign_role
+ can_manage_member_linking: can_manage_member_linking
}
} = socket
) do
@@ -629,25 +580,16 @@ defmodule MvWeb.UserLive.Form do
form =
if user do
- # For existing users: admin uses update_user (email + member + role_id); non-admin uses update (email only).
+ # For existing users: admin uses update_user (email + member); non-admin uses update (email only).
# Password change uses admin_set_password for both.
action =
cond do
show_password_fields -> :admin_set_password
- can_manage_member_linking or can_assign_role -> :update_user
+ can_manage_member_linking -> :update_user
true -> :update
end
- form =
- AshPhoenix.Form.for_update(user, action, domain: Mv.Accounts, as: "user", actor: actor)
-
- # Ensure role_id is always included on submit when role dropdown is shown (AshPhoenix.Form
- # only submits keys in touched_forms; marking as touched avoids role change being dropped).
- if can_assign_role and action == :update_user do
- AshPhoenix.Form.touch(form, [:role_id])
- else
- form
- end
+ AshPhoenix.Form.for_update(user, action, domain: Mv.Accounts, as: "user", actor: actor)
else
# For new users, use password registration if password fields are shown
action = if show_password_fields, do: :register_with_password, else: :create_user
@@ -726,14 +668,6 @@ defmodule MvWeb.UserLive.Form do
Mv.Membership.Member.filter_by_email_match(members, user_email_str)
end
- @spec load_roles(any()) :: [Mv.Authorization.Role.t()]
- defp load_roles(actor) do
- case Authorization.list_roles(actor: actor) do
- {:ok, roles} -> roles
- {:error, _} -> []
- end
- end
-
# Extract user-friendly error message from Ash.Error
@spec extract_error_message(any()) :: String.t()
defp extract_error_message(%Ash.Error.Invalid{errors: errors}) when is_list(errors) do
diff --git a/lib/mv_web/live/user_live/index.ex b/lib/mv_web/live/user_live/index.ex
index 72cc55c..1eb3e47 100644
--- a/lib/mv_web/live/user_live/index.ex
+++ b/lib/mv_web/live/user_live/index.ex
@@ -35,7 +35,7 @@ defmodule MvWeb.UserLive.Index do
users =
Mv.Accounts.User
|> Ash.Query.filter(email != ^Mv.Helpers.SystemActor.system_user_email())
- |> Ash.read!(domain: Mv.Accounts, load: [:member, :role], actor: actor)
+ |> Ash.read!(domain: Mv.Accounts, load: [:member], actor: actor)
sorted = Enum.sort_by(users, & &1.email)
diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex
index ab13f90..cb945e2 100644
--- a/lib/mv_web/live/user_live/index.html.heex
+++ b/lib/mv_web/live/user_live/index.html.heex
@@ -15,8 +15,6 @@
rows={@users}
row_id={fn user -> "row-#{user.id}" end}
row_click={fn user -> JS.navigate(~p"/users/#{user}") end}
- sort_field={@sort_field}
- sort_order={@sort_order}
>
<:col
:let={user}
@@ -47,7 +45,6 @@
<:col
:let={user}
- sort_field={:email}
label={
sort_button(%{
field: :email,
@@ -59,28 +56,11 @@
>
{user.email}
- <:col :let={user} label={gettext("Role")}>
- {user.role.name}
-
<:col :let={user} label={gettext("Linked Member")}>
<%= if user.member do %>
{MvWeb.Helpers.MemberHelpers.display_name(user.member)}
<% else %>
-
{gettext("No member linked")}
- <% end %>
-
- <:col :let={user} label={gettext("Password")}>
- <%= if MvWeb.Helpers.UserHelpers.has_password?(user) do %>
-
{gettext("Enabled")}
- <% else %>
-
—
- <% end %>
-
- <:col :let={user} label={gettext("OIDC")}>
- <%= if MvWeb.Helpers.UserHelpers.has_oidc?(user) do %>
-
{gettext("Linked")}
- <% else %>
-
—
+
{gettext("No member linked")}
<% end %>
diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex
index 4d803cd..5114b74 100644
--- a/lib/mv_web/live/user_live/show.ex
+++ b/lib/mv_web/live/user_live/show.ex
@@ -55,16 +55,8 @@ defmodule MvWeb.UserLive.Show do
<.list>
<:item title={gettext("Email")}>{@user.email}
- <:item title={gettext("Role")}>{@user.role.name}
<:item title={gettext("Password Authentication")}>
- {if MvWeb.Helpers.UserHelpers.has_password?(@user),
- do: gettext("Enabled"),
- else: gettext("Not enabled")}
-
- <:item title={gettext("OIDC")}>
- {if MvWeb.Helpers.UserHelpers.has_oidc?(@user),
- do: gettext("Linked"),
- else: gettext("Not linked")}
+ {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
<:item title={gettext("Linked Member")}>
<%= if @user.member do %>
@@ -87,9 +79,7 @@ defmodule MvWeb.UserLive.Show do
@impl true
def mount(%{"id" => id}, _session, socket) do
actor = current_actor(socket)
-
- user =
- Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member, :role], actor: actor)
+ user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member], actor: actor)
if Mv.Helpers.SystemActor.system_user?(user) do
{:ok,
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index b732c4a..c4fd57d 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -294,7 +294,6 @@ msgstr "Beschreibung"
msgid "Edit User"
msgstr "Benutzer*in bearbeiten"
-#: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Enabled"
@@ -472,7 +471,6 @@ msgid "Include both letters and numbers"
msgstr "Buchstaben und Zahlen verwenden"
#: lib/mv_web/live/user_live/form.ex
-#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Password"
msgstr "Passwort"
@@ -960,6 +958,7 @@ msgid "Last name"
msgstr "Nachname"
#: lib/mv_web/components/core_components.ex
+#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
msgid "None"
msgstr "Keine"
@@ -1671,9 +1670,6 @@ msgstr "Profil"
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/show.ex
-#: lib/mv_web/live/user_live/form.ex
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role"
msgstr "Rolle"
@@ -1969,6 +1965,11 @@ msgstr "Bezahlstatus"
msgid "Reset"
msgstr "Zurücksetzen"
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#, elixir-autogen, elixir-format
+msgid "Only administrators can regenerate cycles"
+msgstr "Nur Administrator*innen können Zyklen regenerieren"
+
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
@@ -2301,45 +2302,3 @@ msgstr "Unbekannte Spalte '%{header}' wird ignoriert. Falls dies ein Datenfeld i
#, elixir-autogen, elixir-format, fuzzy
msgid "Only administrators or the linked user can change the email for members linked to users"
msgstr "Nur Administrator*innen oder die verknüpfte Benutzer*in können die E-Mail von Mitgliedern ändern, die mit Benutzer*innen verknüpft sind."
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Select role..."
-msgstr "Keine auswählen"
-
-#: lib/mv_web/live/member_live/show/membership_fees_component.ex
-#, elixir-autogen, elixir-format
-msgid "You are not allowed to perform this action."
-msgstr "Du hast keine Berechtigung, diese Aktion auszuführen."
-
-#: lib/mv_web/live/member_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Select a membership fee type"
-msgstr "Mitgliedsbeitragstyp auswählen"
-
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Linked"
-msgstr "Verknüpft"
-
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "OIDC"
-msgstr "OIDC"
-
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Not linked"
-msgstr "Nicht verknüpft"
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "SSO / OIDC user"
-msgstr "SSO-/OIDC-Benutzer*in"
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
-msgstr "Dieser*e Benutzer*in ist per SSO (Single Sign-On) angebunden. Ein hier gesetztes oder geändertes Passwort betrifft nur die Anmeldung mit E-Mail und Passwort in dieser Anwendung. Es ändert nicht das Passwort beim Identity-Provider (z. B. Authentik). Zum Ändern des SSO-Passworts nutzen Sie den Identity-Provider oder die IT Ihrer Organisation."
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 3c147ba..0908fd8 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -295,7 +295,6 @@ msgstr ""
msgid "Edit User"
msgstr ""
-#: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Enabled"
@@ -473,7 +472,6 @@ msgid "Include both letters and numbers"
msgstr ""
#: lib/mv_web/live/user_live/form.ex
-#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Password"
msgstr ""
@@ -961,6 +959,7 @@ msgid "Last name"
msgstr ""
#: lib/mv_web/components/core_components.ex
+#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
msgid "None"
msgstr ""
@@ -1672,9 +1671,6 @@ msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/show.ex
-#: lib/mv_web/live/user_live/form.ex
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role"
msgstr ""
@@ -1970,6 +1966,11 @@ msgstr ""
msgid "Reset"
msgstr ""
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#, elixir-autogen, elixir-format
+msgid "Only administrators can regenerate cycles"
+msgstr ""
+
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
@@ -2302,45 +2303,3 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Only administrators or the linked user can change the email for members linked to users"
msgstr ""
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Select role..."
-msgstr ""
-
-#: lib/mv_web/live/member_live/show/membership_fees_component.ex
-#, elixir-autogen, elixir-format
-msgid "You are not allowed to perform this action."
-msgstr ""
-
-#: lib/mv_web/live/member_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Select a membership fee type"
-msgstr ""
-
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Linked"
-msgstr ""
-
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "OIDC"
-msgstr ""
-
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Not linked"
-msgstr ""
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "SSO / OIDC user"
-msgstr ""
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
-msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 7aad814..6faa102 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -295,7 +295,6 @@ msgstr ""
msgid "Edit User"
msgstr ""
-#: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Enabled"
@@ -473,7 +472,6 @@ msgid "Include both letters and numbers"
msgstr ""
#: lib/mv_web/live/user_live/form.ex
-#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Password"
msgstr ""
@@ -961,6 +959,7 @@ msgid "Last name"
msgstr ""
#: lib/mv_web/components/core_components.ex
+#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "None"
msgstr ""
@@ -1672,9 +1671,6 @@ msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/show.ex
-#: lib/mv_web/live/user_live/form.ex
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role"
msgstr ""
@@ -1970,6 +1966,11 @@ msgstr ""
msgid "Reset"
msgstr ""
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#, elixir-autogen, elixir-format
+msgid "Only administrators can regenerate cycles"
+msgstr ""
+
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
@@ -2302,45 +2303,3 @@ msgstr "Unknown column '%{header}' will be ignored. If this is a custom field, c
#, elixir-autogen, elixir-format, fuzzy
msgid "Only administrators or the linked user can change the email for members linked to users"
msgstr "Only administrators or the linked user can change the email for members linked to users"
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Select role..."
-msgstr ""
-
-#: lib/mv_web/live/member_live/show/membership_fees_component.ex
-#, elixir-autogen, elixir-format
-msgid "You are not allowed to perform this action."
-msgstr ""
-
-#: lib/mv_web/live/member_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Select a membership fee type"
-msgstr ""
-
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Linked"
-msgstr ""
-
-#: lib/mv_web/live/user_live/index.html.heex
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "OIDC"
-msgstr ""
-
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Not linked"
-msgstr ""
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "SSO / OIDC user"
-msgstr ""
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
-msgstr ""
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
index e97e7c2..4240336 100644
--- a/priv/repo/seeds.exs
+++ b/priv/repo/seeds.exs
@@ -10,7 +10,7 @@ alias Mv.MembershipFees.CycleGenerator
require Ash.Query
-# Create example membership fee types (no admin user yet; skip authorization for bootstrap)
+# Create example membership fee types
for fee_type_attrs <- [
%{
name: "Standard (Jährlich)",
@@ -39,12 +39,7 @@ for fee_type_attrs <- [
] do
MembershipFeeType
|> Ash.Changeset.for_create(:create, fee_type_attrs)
- |> Ash.create!(
- upsert?: true,
- upsert_identity: :unique_name,
- authorize?: false,
- domain: Mv.MembershipFees
- )
+ |> Ash.create!(upsert?: true, upsert_identity: :unique_name)
end
for attrs <- [
@@ -228,7 +223,7 @@ case Accounts.User
|> Ash.update!(authorize?: false)
{:ok, nil} ->
- # User doesn't exist - create admin user and set password (so Password column shows "Enabled")
+ # User doesn't exist - create admin user with password
# Use authorize?: false for bootstrap - no admin user exists yet to use as actor
Accounts.create_user!(%{email: admin_email},
upsert?: true,
@@ -304,12 +299,12 @@ case Accounts.User
IO.puts("SystemActor will fall back to admin user (#{admin_email})")
end
-# Load all membership fee types for assignment (admin actor for authorization)
+# Load all membership fee types for assignment
# Sort by name to ensure deterministic order
all_fee_types =
MembershipFeeType
|> Ash.Query.sort(name: :asc)
- |> Ash.read!(actor: admin_user_with_role, domain: Mv.MembershipFees)
+ |> Ash.read!()
|> Enum.to_list()
# Create sample members for testing - use upsert to prevent duplicates
@@ -457,8 +452,7 @@ Enum.each(member_attrs_list, fn member_attrs ->
end
end)
-# Create additional users for user-member linking examples (no password by default)
-# Only admin gets a password (admin_set_password when created); all other users have no password.
+# Create additional users for user-member linking examples
additional_users = [
%{email: "hans.mueller@example.de"},
%{email: "greta.schmidt@example.de"},
@@ -468,12 +462,15 @@ additional_users = [
created_users =
Enum.map(additional_users, fn user_attrs ->
+ # Use admin user as actor for additional user creation (not bootstrap)
user =
Accounts.create_user!(user_attrs,
upsert?: true,
upsert_identity: :unique_email,
actor: admin_user_with_role
)
+ |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
+ |> Ash.update!(actor: admin_user_with_role)
# Reload user to ensure all fields (including member_id) are loaded
Accounts.User
diff --git a/test/membership/membership_fee_settings_test.exs b/test/membership/membership_fee_settings_test.exs
index 2c126d9..744b6bd 100644
--- a/test/membership/membership_fee_settings_test.exs
+++ b/test/membership/membership_fee_settings_test.exs
@@ -54,26 +54,18 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
# Create a valid fee type
{:ok, fee_type} =
- Ash.create(
- MembershipFeeType,
- %{
- name: "Test Fee Type #{System.unique_integer([:positive])}",
- amount: Decimal.new("100.00"),
- interval: :yearly
- },
- actor: actor
- )
+ Ash.create(MembershipFeeType, %{
+ name: "Test Fee Type #{System.unique_integer([:positive])}",
+ amount: Decimal.new("100.00"),
+ interval: :yearly
+ })
# Setting a valid fee type should work
{:ok, updated} =
settings
- |> Ash.Changeset.for_update(
- :update_membership_fee_settings,
- %{
- default_membership_fee_type_id: fee_type.id
- },
- actor: actor
- )
+ |> Ash.Changeset.for_update(:update_membership_fee_settings, %{
+ default_membership_fee_type_id: fee_type.id
+ })
|> Ash.update(actor: actor)
assert updated.default_membership_fee_type_id == fee_type.id
diff --git a/test/membership_fees/changes/validate_same_interval_test.exs b/test/membership_fees/changes/validate_same_interval_test.exs
index 2310537..21287dd 100644
--- a/test/membership_fees/changes/validate_same_interval_test.exs
+++ b/test/membership_fees/changes/validate_same_interval_test.exs
@@ -52,7 +52,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: yearly_type2.id},
actor: actor
)
- |> ValidateSameInterval.change(%{}, %{actor: actor})
+ |> ValidateSameInterval.change(%{}, %{})
assert changeset.valid?
end
@@ -68,7 +68,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: monthly_type.id},
actor: actor
)
- |> ValidateSameInterval.change(%{}, %{actor: actor})
+ |> ValidateSameInterval.change(%{}, %{})
refute changeset.valid?
assert %{errors: errors} = changeset
@@ -90,7 +90,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: yearly_type.id},
actor: actor
)
- |> ValidateSameInterval.change(%{}, %{actor: actor})
+ |> ValidateSameInterval.change(%{}, %{})
assert changeset.valid?
end
@@ -102,7 +102,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: nil}, actor: actor)
- |> ValidateSameInterval.change(%{}, %{actor: actor})
+ |> ValidateSameInterval.change(%{}, %{})
refute changeset.valid?
assert %{errors: errors} = changeset
@@ -120,7 +120,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{first_name: "New Name"}, actor: actor)
- |> ValidateSameInterval.change(%{}, %{actor: actor})
+ |> ValidateSameInterval.change(%{}, %{})
assert changeset.valid?
end
@@ -136,7 +136,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: quarterly_type.id},
actor: actor
)
- |> ValidateSameInterval.change(%{}, %{actor: actor})
+ |> ValidateSameInterval.change(%{}, %{})
error = Enum.find(changeset.errors, &(&1.field == :membership_fee_type_id))
assert error.message =~ "yearly"
@@ -175,7 +175,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: type2.id},
actor: actor
)
- |> ValidateSameInterval.change(%{}, %{actor: actor})
+ |> ValidateSameInterval.change(%{}, %{})
refute changeset.valid?,
"Should prevent change from #{interval1} to #{interval2}"
diff --git a/test/membership_fees/membership_fee_cycle_test.exs b/test/membership_fees/membership_fee_cycle_test.exs
index 8770134..fefc838 100644
--- a/test/membership_fees/membership_fee_cycle_test.exs
+++ b/test/membership_fees/membership_fee_cycle_test.exs
@@ -151,7 +151,7 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
cycle = create_cycle(member, fee_type, %{status: :paid}, actor)
- assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_unpaid)
+ assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid)
assert updated.status == :unpaid
end
@@ -175,7 +175,7 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
cycle = create_cycle(member, fee_type, %{status: :suspended}, actor)
- assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_unpaid)
+ assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid)
assert updated.status == :unpaid
end
end
diff --git a/test/membership_fees/membership_fee_type_integration_test.exs b/test/membership_fees/membership_fee_type_integration_test.exs
index 88f620d..e716b42 100644
--- a/test/membership_fees/membership_fee_type_integration_test.exs
+++ b/test/membership_fees/membership_fee_type_integration_test.exs
@@ -155,13 +155,9 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
{:ok, settings} = Mv.Membership.get_settings()
settings
- |> Ash.Changeset.for_update(
- :update_membership_fee_settings,
- %{
- default_membership_fee_type_id: fee_type.id
- },
- actor: actor
- )
+ |> Ash.Changeset.for_update(:update_membership_fee_settings, %{
+ default_membership_fee_type_id: fee_type.id
+ })
|> Ash.update!(actor: actor)
# Try to delete
@@ -180,13 +176,9 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
{:ok, settings} = Mv.Membership.get_settings()
settings
- |> Ash.Changeset.for_update(
- :update_membership_fee_settings,
- %{
- default_membership_fee_type_id: fee_type.id
- },
- actor: actor
- )
+ |> Ash.Changeset.for_update(:update_membership_fee_settings, %{
+ default_membership_fee_type_id: fee_type.id
+ })
|> Ash.update!(actor: actor)
# Create a member without explicitly setting membership_fee_type_id
diff --git a/test/membership_fees/membership_fee_type_test.exs b/test/membership_fees/membership_fee_type_test.exs
index 7b6c39a..80b7839 100644
--- a/test/membership_fees/membership_fee_type_test.exs
+++ b/test/membership_fees/membership_fee_type_test.exs
@@ -264,13 +264,9 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
{:ok, settings} = Mv.Membership.get_settings()
settings
- |> Ash.Changeset.for_update(
- :update_membership_fee_settings,
- %{
- default_membership_fee_type_id: fee_type.id
- },
- actor: actor
- )
+ |> Ash.Changeset.for_update(:update_membership_fee_settings, %{
+ default_membership_fee_type_id: fee_type.id
+ })
|> Ash.update!(actor: actor)
# Try to delete
diff --git a/test/mv/accounts/user_policies_test.exs b/test/mv/accounts/user_policies_test.exs
index 66b550c..736b336 100644
--- a/test/mv/accounts/user_policies_test.exs
+++ b/test/mv/accounts/user_policies_test.exs
@@ -10,6 +10,7 @@ defmodule Mv.Accounts.UserPoliciesTest do
use Mv.DataCase, async: false
alias Mv.Accounts
+ alias Mv.Authorization
require Ash.Query
@@ -18,10 +19,59 @@ defmodule Mv.Accounts.UserPoliciesTest do
%{actor: system_actor}
end
+ # Helper to create a role with a specific permission set
+ defp create_role_with_permission_set(permission_set_name, actor) do
+ role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
+
+ case Authorization.create_role(
+ %{
+ name: role_name,
+ description: "Test role for #{permission_set_name}",
+ permission_set_name: permission_set_name
+ },
+ actor: actor
+ ) do
+ {:ok, role} -> role
+ {:error, error} -> raise "Failed to create role: #{inspect(error)}"
+ end
+ end
+
+ # Helper to create a user with a specific permission set
+ # Returns user with role preloaded (required for authorization)
+ defp create_user_with_permission_set(permission_set_name, actor) do
+ # Create role with permission set
+ role = create_role_with_permission_set(permission_set_name, actor)
+
+ # Create user
+ {:ok, user} =
+ Accounts.User
+ |> Ash.Changeset.for_create(:register_with_password, %{
+ email: "user#{System.unique_integer([:positive])}@example.com",
+ password: "testpassword123"
+ })
+ |> Ash.create(actor: actor)
+
+ # Assign role to user
+ {:ok, user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{})
+ |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
+ |> Ash.update(actor: actor)
+
+ # Reload user with role preloaded (critical for authorization!)
+ {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
+ user_with_role
+ end
+
+ # Helper to create another user (for testing access to other users)
+ defp create_other_user(actor) do
+ create_user_with_permission_set("own_data", actor)
+ end
+
# Shared test setup for permission sets with scope :own access
defp setup_user_with_own_access(permission_set, actor) do
- user = Mv.Fixtures.user_with_role_fixture(permission_set)
- other_user = Mv.Fixtures.user_with_role_fixture("own_data")
+ user = create_user_with_permission_set(permission_set, actor)
+ other_user = create_other_user(actor)
# Reload user to ensure role is preloaded
{:ok, user} =
@@ -30,101 +80,217 @@ defmodule Mv.Accounts.UserPoliciesTest do
%{user: user, other_user: other_user}
end
- # Data-driven: same behaviour for own_data, read_only, normal_user (scope :own for User)
- describe "non-admin permission sets (own_data, read_only, normal_user)" do
- setup %{actor: actor} = context do
- permission_set = context[:permission_set] || "own_data"
- setup_user_with_own_access(permission_set, actor)
+ describe "own_data permission set (Mitglied)" do
+ setup %{actor: actor} do
+ setup_user_with_own_access("own_data", actor)
end
- for permission_set <- ["own_data", "read_only", "normal_user"] do
- @tag permission_set: permission_set
- test "can read own user record (#{permission_set})", %{user: user} do
- {:ok, fetched_user} =
- Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
+ test "can read own user record", %{user: user} do
+ {:ok, fetched_user} =
+ Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
- assert fetched_user.id == user.id
+ assert fetched_user.id == user.id
+ end
+
+ test "can update own email", %{user: user} do
+ new_email = "updated#{System.unique_integer([:positive])}@example.com"
+
+ # Non-admins use :update (email only); :update_user is admin-only (member link/unlink).
+ {:ok, updated_user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{email: new_email})
+ |> Ash.update(actor: user)
+
+ assert updated_user.email == Ash.CiString.new(new_email)
+ end
+
+ test "cannot read other users (returns not found due to auto_filter)", %{
+ user: user,
+ other_user: other_user
+ } do
+ # Note: With auto_filter policies, when a user tries to read a user that doesn't
+ # match the filter (id == actor.id), Ash returns NotFound, not Forbidden.
+ # This is the expected behavior - the filter makes the record "invisible" to the user.
+ assert_raise Ash.Error.Invalid, fn ->
+ Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
end
+ end
- @tag permission_set: permission_set
- test "can update own email (#{permission_set})", %{user: user} do
- new_email = "updated#{System.unique_integer([:positive])}@example.com"
-
- {:ok, updated_user} =
- user
- |> Ash.Changeset.for_update(:update, %{email: new_email})
- |> Ash.update(actor: user)
-
- assert updated_user.email == Ash.CiString.new(new_email)
+ test "cannot update other users (returns forbidden)", %{user: user, other_user: other_user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ other_user
+ |> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
+ |> Ash.update!(actor: user)
end
+ end
- @tag permission_set: permission_set
- test "cannot read other users - not found due to auto_filter (#{permission_set})", %{
- user: user,
- other_user: other_user
- } do
- assert_raise Ash.Error.Invalid, fn ->
- Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
- end
+ test "list users returns only own user", %{user: user} do
+ {:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
+
+ # Should only return the own user (scope :own filters)
+ assert length(users) == 1
+ assert hd(users).id == user.id
+ end
+
+ test "cannot create user (returns forbidden)", %{user: user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ Accounts.User
+ |> Ash.Changeset.for_create(:create_user, %{
+ email: "new#{System.unique_integer([:positive])}@example.com"
+ })
+ |> Ash.create!(actor: user)
end
+ end
- @tag permission_set: permission_set
- test "cannot update other users - forbidden (#{permission_set})", %{
- user: user,
- other_user: other_user
- } do
- assert_raise Ash.Error.Forbidden, fn ->
- other_user
- |> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
- |> Ash.update!(actor: user)
- end
+ test "cannot destroy user (returns forbidden)", %{user: user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ Ash.destroy!(user, actor: user)
end
+ end
+ end
- @tag permission_set: permission_set
- test "list users returns only own user (#{permission_set})", %{user: user} do
- {:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
+ describe "read_only permission set (Vorstand/Buchhaltung)" do
+ setup %{actor: actor} do
+ setup_user_with_own_access("read_only", actor)
+ end
- assert length(users) == 1
- assert hd(users).id == user.id
+ test "can read own user record", %{user: user} do
+ {:ok, fetched_user} =
+ Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
+
+ assert fetched_user.id == user.id
+ end
+
+ test "can update own email", %{user: user} do
+ new_email = "updated#{System.unique_integer([:positive])}@example.com"
+
+ # Non-admins use :update (email only); :update_user is admin-only (member link/unlink).
+ {:ok, updated_user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{email: new_email})
+ |> Ash.update(actor: user)
+
+ assert updated_user.email == Ash.CiString.new(new_email)
+ end
+
+ test "cannot read other users (returns not found due to auto_filter)", %{
+ user: user,
+ other_user: other_user
+ } do
+ # Note: With auto_filter policies, when a user tries to read a user that doesn't
+ # match the filter (id == actor.id), Ash returns NotFound, not Forbidden.
+ # This is the expected behavior - the filter makes the record "invisible" to the user.
+ assert_raise Ash.Error.Invalid, fn ->
+ Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
end
+ end
- @tag permission_set: permission_set
- test "cannot create user - forbidden (#{permission_set})", %{user: user} do
- assert_raise Ash.Error.Forbidden, fn ->
- Accounts.User
- |> Ash.Changeset.for_create(:create_user, %{
- email: "new#{System.unique_integer([:positive])}@example.com"
- })
- |> Ash.create!(actor: user)
- end
+ test "cannot update other users (returns forbidden)", %{user: user, other_user: other_user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ other_user
+ |> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
+ |> Ash.update!(actor: user)
end
+ end
- @tag permission_set: permission_set
- test "cannot destroy user - forbidden (#{permission_set})", %{user: user} do
- assert_raise Ash.Error.Forbidden, fn ->
- Ash.destroy!(user, actor: user)
- end
+ test "list users returns only own user", %{user: user} do
+ {:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
+
+ # Should only return the own user (scope :own filters)
+ assert length(users) == 1
+ assert hd(users).id == user.id
+ end
+
+ test "cannot create user (returns forbidden)", %{user: user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ Accounts.User
+ |> Ash.Changeset.for_create(:create_user, %{
+ email: "new#{System.unique_integer([:positive])}@example.com"
+ })
+ |> Ash.create!(actor: user)
end
+ end
- @tag permission_set: permission_set
- test "cannot change role via update_user - forbidden (#{permission_set})", %{
- user: user,
- other_user: other_user
- } do
- other_role = Mv.Fixtures.role_fixture("read_only")
+ test "cannot destroy user (returns forbidden)", %{user: user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ Ash.destroy!(user, actor: user)
+ end
+ end
+ end
- assert {:error, %Ash.Error.Forbidden{}} =
- other_user
- |> Ash.Changeset.for_update(:update_user, %{role_id: other_role.id})
- |> Ash.update(actor: user, domain: Mv.Accounts)
+ describe "normal_user permission set (Kassenwart)" do
+ setup %{actor: actor} do
+ setup_user_with_own_access("normal_user", actor)
+ end
+
+ test "can read own user record", %{user: user} do
+ {:ok, fetched_user} =
+ Ash.get(Accounts.User, user.id, actor: user, domain: Mv.Accounts)
+
+ assert fetched_user.id == user.id
+ end
+
+ test "can update own email", %{user: user} do
+ new_email = "updated#{System.unique_integer([:positive])}@example.com"
+
+ # Non-admins use :update (email only); :update_user is admin-only (member link/unlink).
+ {:ok, updated_user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{email: new_email})
+ |> Ash.update(actor: user)
+
+ assert updated_user.email == Ash.CiString.new(new_email)
+ end
+
+ test "cannot read other users (returns not found due to auto_filter)", %{
+ user: user,
+ other_user: other_user
+ } do
+ # Note: With auto_filter policies, when a user tries to read a user that doesn't
+ # match the filter (id == actor.id), Ash returns NotFound, not Forbidden.
+ # This is the expected behavior - the filter makes the record "invisible" to the user.
+ assert_raise Ash.Error.Invalid, fn ->
+ Ash.get!(Accounts.User, other_user.id, actor: user, domain: Mv.Accounts)
+ end
+ end
+
+ test "cannot update other users (returns forbidden)", %{user: user, other_user: other_user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ other_user
+ |> Ash.Changeset.for_update(:update, %{email: "hacked@example.com"})
+ |> Ash.update!(actor: user)
+ end
+ end
+
+ test "list users returns only own user", %{user: user} do
+ {:ok, users} = Ash.read(Accounts.User, actor: user, domain: Mv.Accounts)
+
+ # Should only return the own user (scope :own filters)
+ assert length(users) == 1
+ assert hd(users).id == user.id
+ end
+
+ test "cannot create user (returns forbidden)", %{user: user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ Accounts.User
+ |> Ash.Changeset.for_create(:create_user, %{
+ email: "new#{System.unique_integer([:positive])}@example.com"
+ })
+ |> Ash.create!(actor: user)
+ end
+ end
+
+ test "cannot destroy user (returns forbidden)", %{user: user} do
+ assert_raise Ash.Error.Forbidden, fn ->
+ Ash.destroy!(user, actor: user)
end
end
end
describe "admin permission set" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
- other_user = Mv.Fixtures.user_with_role_fixture("own_data")
+ user = create_user_with_permission_set("admin", actor)
+ other_user = create_other_user(actor)
# Reload user to ensure role is preloaded
{:ok, user} =
@@ -177,88 +343,6 @@ defmodule Mv.Accounts.UserPoliciesTest do
# Verify user is deleted
assert {:error, _} = Ash.get(Accounts.User, other_user.id, domain: Mv.Accounts)
end
-
- test "admin can assign role to another user via update_user", %{
- other_user: other_user
- } do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
- normal_user_role = Mv.Fixtures.role_fixture("normal_user")
-
- {:ok, updated} =
- other_user
- |> Ash.Changeset.for_update(:update_user, %{role_id: normal_user_role.id})
- |> Ash.update(actor: admin)
-
- assert updated.role_id == normal_user_role.id
- end
- end
-
- describe "admin role assignment and last-admin validation" do
- test "two admins: one can change own role to normal_user (other remains admin)", %{
- actor: _actor
- } do
- _admin_role = Mv.Fixtures.role_fixture("admin")
- normal_user_role = Mv.Fixtures.role_fixture("normal_user")
-
- admin_a = Mv.Fixtures.user_with_role_fixture("admin")
- _admin_b = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, updated} =
- admin_a
- |> Ash.Changeset.for_update(:update_user, %{role_id: normal_user_role.id})
- |> Ash.update(actor: admin_a)
-
- assert updated.role_id == normal_user_role.id
- end
-
- test "single admin: changing own role to normal_user returns validation error", %{
- actor: _actor
- } do
- normal_user_role = Mv.Fixtures.role_fixture("normal_user")
- single_admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- assert {:error, %Ash.Error.Invalid{errors: errors}} =
- single_admin
- |> Ash.Changeset.for_update(:update_user, %{role_id: normal_user_role.id})
- |> Ash.update(actor: single_admin)
-
- error_messages =
- Enum.flat_map(errors, fn
- %Ash.Error.Changes.InvalidAttribute{message: msg} when is_binary(msg) -> [msg]
- %{message: msg} when is_binary(msg) -> [msg]
- _ -> []
- end)
-
- assert Enum.any?(error_messages, fn msg ->
- msg =~ "least one user must keep the Admin role" or msg =~ "Admin role"
- end),
- "Expected last-admin validation message, got: #{inspect(error_messages)}"
- end
-
- test "admin can switch to another admin role (two roles with permission_set_name admin)", %{
- actor: _actor
- } do
- # Two distinct roles both with permission_set_name "admin" (e.g. "Admin" and "Superadmin")
- admin_role_a = Mv.Fixtures.role_fixture("admin")
- admin_role_b = Mv.Fixtures.role_fixture("admin")
-
- admin_user = Mv.Fixtures.user_with_role_fixture("admin")
- # Ensure user has role_a so we can switch to role_b
- {:ok, admin_user} =
- admin_user
- |> Ash.Changeset.for_update(:update_user, %{role_id: admin_role_a.id})
- |> Ash.update(actor: admin_user)
-
- assert admin_user.role_id == admin_role_a.id
-
- # Switching to another admin role must be allowed (no last-admin error)
- {:ok, updated} =
- admin_user
- |> Ash.Changeset.for_update(:update_user, %{role_id: admin_role_b.id})
- |> Ash.update(actor: admin_user)
-
- assert updated.role_id == admin_role_b.id
- end
end
describe "AshAuthentication bypass" do
diff --git a/test/mv/authorization/permission_sets_test.exs b/test/mv/authorization/permission_sets_test.exs
index 2f429f9..404a87e 100644
--- a/test/mv/authorization/permission_sets_test.exs
+++ b/test/mv/authorization/permission_sets_test.exs
@@ -496,281 +496,6 @@ defmodule Mv.Authorization.PermissionSetsTest do
assert "*" in permissions.pages
end
-
- test "admin pages include explicit /settings and /membership_fee_settings" do
- permissions = PermissionSets.get_permissions(:admin)
-
- assert "/settings" in permissions.pages
- assert "/membership_fee_settings" in permissions.pages
- end
- end
-
- describe "get_permissions/1 - MemberGroup resource" do
- test "own_data has MemberGroup read with scope :linked only" do
- permissions = PermissionSets.get_permissions(:own_data)
-
- mg_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :read
- end)
-
- mg_create =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :create
- end)
-
- assert mg_read != nil
- assert mg_read.scope == :linked
- assert mg_read.granted == true
- assert mg_create == nil || mg_create.granted == false
- end
-
- test "read_only has MemberGroup read with scope :all, no create/destroy" do
- permissions = PermissionSets.get_permissions(:read_only)
-
- mg_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :read
- end)
-
- mg_create =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :create
- end)
-
- mg_destroy =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :destroy
- end)
-
- assert mg_read != nil
- assert mg_read.scope == :all
- assert mg_read.granted == true
- assert mg_create == nil || mg_create.granted == false
- assert mg_destroy == nil || mg_destroy.granted == false
- end
-
- test "normal_user has MemberGroup read/create/destroy with scope :all" do
- permissions = PermissionSets.get_permissions(:normal_user)
-
- mg_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :read
- end)
-
- mg_create =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :create
- end)
-
- mg_destroy =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :destroy
- end)
-
- assert mg_read != nil
- assert mg_read.scope == :all
- assert mg_read.granted == true
- assert mg_create != nil
- assert mg_create.scope == :all
- assert mg_create.granted == true
- assert mg_destroy != nil
- assert mg_destroy.scope == :all
- assert mg_destroy.granted == true
- end
-
- test "admin has MemberGroup read/create/destroy with scope :all" do
- permissions = PermissionSets.get_permissions(:admin)
-
- mg_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :read
- end)
-
- mg_create =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :create
- end)
-
- mg_destroy =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MemberGroup" && p.action == :destroy
- end)
-
- assert mg_read != nil
- assert mg_read.scope == :all
- assert mg_read.granted == true
- assert mg_create != nil
- assert mg_create.granted == true
- assert mg_destroy != nil
- assert mg_destroy.granted == true
- end
- end
-
- describe "get_permissions/1 - MembershipFeeType resource" do
- test "all permission sets have MembershipFeeType read with scope :all" do
- for set <- PermissionSets.all_permission_sets() do
- permissions = PermissionSets.get_permissions(set)
-
- mft_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeType" && p.action == :read
- end)
-
- assert mft_read != nil, "Permission set #{set} should have MembershipFeeType read"
- assert mft_read.scope == :all
- assert mft_read.granted == true
- end
- end
-
- test "only admin has MembershipFeeType create/update/destroy" do
- for set <- [:own_data, :read_only, :normal_user] do
- permissions = PermissionSets.get_permissions(set)
-
- mft_create =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeType" && p.action == :create
- end)
-
- mft_update =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeType" && p.action == :update
- end)
-
- mft_destroy =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeType" && p.action == :destroy
- end)
-
- assert mft_create == nil || mft_create.granted == false,
- "Permission set #{set} should not allow MembershipFeeType create"
-
- assert mft_update == nil || mft_update.granted == false,
- "Permission set #{set} should not allow MembershipFeeType update"
-
- assert mft_destroy == nil || mft_destroy.granted == false,
- "Permission set #{set} should not allow MembershipFeeType destroy"
- end
-
- admin_permissions = PermissionSets.get_permissions(:admin)
-
- mft_create =
- Enum.find(admin_permissions.resources, fn p ->
- p.resource == "MembershipFeeType" && p.action == :create
- end)
-
- mft_update =
- Enum.find(admin_permissions.resources, fn p ->
- p.resource == "MembershipFeeType" && p.action == :update
- end)
-
- mft_destroy =
- Enum.find(admin_permissions.resources, fn p ->
- p.resource == "MembershipFeeType" && p.action == :destroy
- end)
-
- assert mft_create != nil
- assert mft_create.scope == :all
- assert mft_create.granted == true
- assert mft_update != nil
- assert mft_update.granted == true
- assert mft_destroy != nil
- assert mft_destroy.granted == true
- end
- end
-
- describe "get_permissions/1 - MembershipFeeCycle resource" do
- test "all permission sets have MembershipFeeCycle read; own_data uses :linked, others :all" do
- for set <- PermissionSets.all_permission_sets() do
- permissions = PermissionSets.get_permissions(set)
-
- mfc_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :read
- end)
-
- assert mfc_read != nil, "Permission set #{set} should have MembershipFeeCycle read"
- assert mfc_read.granted == true
-
- expected_scope = if set == :own_data, do: :linked, else: :all
-
- assert mfc_read.scope == expected_scope,
- "Permission set #{set} should have MembershipFeeCycle read scope #{expected_scope}, got #{mfc_read.scope}"
- end
- end
-
- test "read_only has MembershipFeeCycle read only, no update" do
- permissions = PermissionSets.get_permissions(:read_only)
-
- mfc_update =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :update
- end)
-
- assert mfc_update == nil || mfc_update.granted == false
- end
-
- test "normal_user has MembershipFeeCycle read/create/update/destroy with scope :all" do
- permissions = PermissionSets.get_permissions(:normal_user)
-
- mfc_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :read
- end)
-
- mfc_create =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :create
- end)
-
- mfc_update =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :update
- end)
-
- mfc_destroy =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :destroy
- end)
-
- assert mfc_read != nil && mfc_read.granted == true
- assert mfc_create != nil && mfc_create.scope == :all && mfc_create.granted == true
- assert mfc_update != nil && mfc_update.granted == true
- assert mfc_destroy != nil && mfc_destroy.scope == :all && mfc_destroy.granted == true
- end
-
- test "admin has MembershipFeeCycle read/create/update/destroy with scope :all" do
- permissions = PermissionSets.get_permissions(:admin)
-
- mfc_read =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :read
- end)
-
- mfc_create =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :create
- end)
-
- mfc_update =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :update
- end)
-
- mfc_destroy =
- Enum.find(permissions.resources, fn p ->
- p.resource == "MembershipFeeCycle" && p.action == :destroy
- end)
-
- assert mfc_read != nil
- assert mfc_read.granted == true
- assert mfc_create != nil
- assert mfc_create.granted == true
- assert mfc_update != nil
- assert mfc_update.granted == true
- assert mfc_destroy != nil
- assert mfc_destroy.granted == true
- end
end
describe "valid_permission_set?/1" do
diff --git a/test/mv/membership/custom_field_policies_test.exs b/test/mv/membership/custom_field_policies_test.exs
index a6885f5..1e758d1 100644
--- a/test/mv/membership/custom_field_policies_test.exs
+++ b/test/mv/membership/custom_field_policies_test.exs
@@ -8,30 +8,67 @@ defmodule Mv.Membership.CustomFieldPoliciesTest do
use Mv.DataCase, async: false
alias Mv.Membership.CustomField
+ alias Mv.Accounts
+ alias Mv.Authorization
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
- defp create_custom_field do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ defp create_role_with_permission_set(permission_set_name, actor) do
+ role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
+ case Authorization.create_role(
+ %{
+ name: role_name,
+ description: "Test role for #{permission_set_name}",
+ permission_set_name: permission_set_name
+ },
+ actor: actor
+ ) do
+ {:ok, role} -> role
+ {:error, error} -> raise "Failed to create role: #{inspect(error)}"
+ end
+ end
+
+ defp create_user_with_permission_set(permission_set_name, actor) do
+ role = create_role_with_permission_set(permission_set_name, actor)
+
+ {:ok, user} =
+ Accounts.User
+ |> Ash.Changeset.for_create(:register_with_password, %{
+ email: "user#{System.unique_integer([:positive])}@example.com",
+ password: "testpassword123"
+ })
+ |> Ash.create(actor: actor)
+
+ {:ok, user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{})
+ |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
+ |> Ash.update(actor: actor)
+
+ {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
+ user_with_role
+ end
+
+ defp create_custom_field(actor) do
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field_#{System.unique_integer([:positive])}",
value_type: :string
})
- |> Ash.create(actor: admin, domain: Mv.Membership)
+ |> Ash.create(actor: actor, domain: Mv.Membership)
field
end
describe "read access (all roles)" do
- test "user with own_data can read all custom fields", %{actor: _actor} do
- custom_field = create_custom_field()
- user = Mv.Fixtures.user_with_role_fixture("own_data")
+ test "user with own_data can read all custom fields", %{actor: actor} do
+ custom_field = create_custom_field(actor)
+ user = create_user_with_permission_set("own_data", actor)
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
ids = Enum.map(fields, & &1.id)
@@ -41,9 +78,9 @@ defmodule Mv.Membership.CustomFieldPoliciesTest do
assert fetched.id == custom_field.id
end
- test "user with read_only can read all custom fields", %{actor: _actor} do
- custom_field = create_custom_field()
- user = Mv.Fixtures.user_with_role_fixture("read_only")
+ test "user with read_only can read all custom fields", %{actor: actor} do
+ custom_field = create_custom_field(actor)
+ user = create_user_with_permission_set("read_only", actor)
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
ids = Enum.map(fields, & &1.id)
@@ -53,9 +90,9 @@ defmodule Mv.Membership.CustomFieldPoliciesTest do
assert fetched.id == custom_field.id
end
- test "user with normal_user can read all custom fields", %{actor: _actor} do
- custom_field = create_custom_field()
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
+ test "user with normal_user can read all custom fields", %{actor: actor} do
+ custom_field = create_custom_field(actor)
+ user = create_user_with_permission_set("normal_user", actor)
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
ids = Enum.map(fields, & &1.id)
@@ -65,9 +102,9 @@ defmodule Mv.Membership.CustomFieldPoliciesTest do
assert fetched.id == custom_field.id
end
- test "user with admin can read all custom fields", %{actor: _actor} do
- custom_field = create_custom_field()
- user = Mv.Fixtures.user_with_role_fixture("admin")
+ test "user with admin can read all custom fields", %{actor: actor} do
+ custom_field = create_custom_field(actor)
+ user = create_user_with_permission_set("admin", actor)
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
ids = Enum.map(fields, & &1.id)
@@ -79,9 +116,9 @@ defmodule Mv.Membership.CustomFieldPoliciesTest do
end
describe "write access - non-admin cannot create/update/destroy" do
- setup %{actor: _actor} do
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
- custom_field = create_custom_field()
+ setup %{actor: actor} do
+ user = create_user_with_permission_set("normal_user", actor)
+ custom_field = create_custom_field(actor)
%{user: user, custom_field: custom_field}
end
@@ -115,9 +152,9 @@ defmodule Mv.Membership.CustomFieldPoliciesTest do
end
describe "write access - admin can create/update/destroy" do
- setup %{actor: _actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
- custom_field = create_custom_field()
+ setup %{actor: actor} do
+ user = create_user_with_permission_set("admin", actor)
+ custom_field = create_custom_field(actor)
%{user: user, custom_field: custom_field}
end
diff --git a/test/mv/membership/custom_field_value_policies_test.exs b/test/mv/membership/custom_field_value_policies_test.exs
index 64d6ff2..72b6af6 100644
--- a/test/mv/membership/custom_field_value_policies_test.exs
+++ b/test/mv/membership/custom_field_value_policies_test.exs
@@ -11,6 +11,7 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
alias Mv.Membership.{CustomField, CustomFieldValue}
alias Mv.Accounts
+ alias Mv.Authorization
require Ash.Query
@@ -19,9 +20,47 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
%{actor: system_actor}
end
- defp create_linked_member_for_user(user, _actor) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ # Helper to create a role with a specific permission set
+ defp create_role_with_permission_set(permission_set_name, actor) do
+ role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
+ case Authorization.create_role(
+ %{
+ name: role_name,
+ description: "Test role for #{permission_set_name}",
+ permission_set_name: permission_set_name
+ },
+ actor: actor
+ ) do
+ {:ok, role} -> role
+ {:error, error} -> raise "Failed to create role: #{inspect(error)}"
+ end
+ end
+
+ # Helper to create a user with a specific permission set
+ # Returns user with role preloaded (required for authorization)
+ defp create_user_with_permission_set(permission_set_name, actor) do
+ role = create_role_with_permission_set(permission_set_name, actor)
+
+ {:ok, user} =
+ Accounts.User
+ |> Ash.Changeset.for_create(:register_with_password, %{
+ email: "user#{System.unique_integer([:positive])}@example.com",
+ password: "testpassword123"
+ })
+ |> Ash.create(actor: actor)
+
+ {:ok, user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{})
+ |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
+ |> Ash.update(actor: actor)
+
+ {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
+ user_with_role
+ end
+
+ defp create_linked_member_for_user(user, actor) do
{:ok, member} =
Mv.Membership.create_member(
%{
@@ -29,20 +68,18 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
last_name: "Member",
email: "linked#{System.unique_integer([:positive])}@example.com"
},
- actor: admin
+ actor: actor
)
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.force_change_attribute(:member_id, member.id)
- |> Ash.update(actor: admin, domain: Mv.Accounts, return_notifications?: false)
+ |> Ash.update(actor: actor, domain: Mv.Accounts, return_notifications?: false)
member
end
- defp create_unlinked_member(_actor) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
+ defp create_unlinked_member(actor) do
{:ok, member} =
Mv.Membership.create_member(
%{
@@ -50,29 +87,25 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
last_name: "Member",
email: "unlinked#{System.unique_integer([:positive])}@example.com"
},
- actor: admin
+ actor: actor
)
member
end
- defp create_custom_field do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
+ defp create_custom_field(actor) do
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field_#{System.unique_integer([:positive])}",
value_type: :string
})
- |> Ash.create(actor: admin, domain: Mv.Membership)
+ |> Ash.create(actor: actor)
field
end
- defp create_custom_field_value(member_id, custom_field_id, value) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
+ defp create_custom_field_value(member_id, custom_field_id, value, actor) do
{:ok, cfv} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
@@ -80,22 +113,22 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
custom_field_id: custom_field_id,
value: %{"_union_type" => "string", "_union_value" => value}
})
- |> Ash.create(actor: admin, domain: Mv.Membership)
+ |> Ash.create(actor: actor, domain: Mv.Membership)
cfv
end
describe "own_data permission set (Mitglied)" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("own_data")
+ user = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
- custom_field = create_custom_field()
+ custom_field = create_custom_field(actor)
- cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
+ cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
- create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
+ create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{:ok, user} =
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
@@ -144,10 +177,10 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
test "can create custom field value for linked member", %{
user: user,
linked_member: linked_member,
- actor: _actor
+ actor: actor
} do
# Create a second custom field via admin (own_data cannot create CustomField)
- custom_field2 = create_custom_field()
+ custom_field2 = create_custom_field(actor)
{:ok, cfv} =
CustomFieldValue
@@ -224,15 +257,15 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
describe "read_only permission set (Vorstand/Buchhaltung)" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("read_only")
+ user = create_user_with_permission_set("read_only", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
- custom_field = create_custom_field()
+ custom_field = create_custom_field(actor)
- cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
+ cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
- create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
+ create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{:ok, user} =
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
@@ -307,15 +340,15 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
describe "normal_user permission set (Kassenwart)" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
+ user = create_user_with_permission_set("normal_user", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
- custom_field = create_custom_field()
+ custom_field = create_custom_field(actor)
- cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
+ cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
- create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
+ create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{:ok, user} =
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
@@ -346,10 +379,10 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
test "can create custom field value", %{
user: user,
unlinked_member: unlinked_member,
- actor: _actor
+ actor: actor
} do
# normal_user cannot create CustomField; use actor (admin) to create it
- custom_field = create_custom_field()
+ custom_field = create_custom_field(actor)
{:ok, cfv} =
CustomFieldValue
@@ -388,15 +421,15 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
describe "admin permission set" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
+ user = create_user_with_permission_set("admin", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
- custom_field = create_custom_field()
+ custom_field = create_custom_field(actor)
- cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
+ cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
- create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
+ create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{:ok, user} =
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
@@ -424,7 +457,7 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
end
test "can create custom field value", %{user: user, unlinked_member: unlinked_member} do
- custom_field = create_custom_field()
+ custom_field = create_custom_field(user)
{:ok, cfv} =
CustomFieldValue
diff --git a/test/mv/membership/group_policies_test.exs b/test/mv/membership/group_policies_test.exs
deleted file mode 100644
index 27287ff..0000000
--- a/test/mv/membership/group_policies_test.exs
+++ /dev/null
@@ -1,140 +0,0 @@
-defmodule Mv.Membership.GroupPoliciesTest do
- @moduledoc """
- Tests for Group resource authorization policies.
-
- Verifies that own_data, read_only, normal_user can read groups;
- normal_user and admin can create, update, and destroy groups.
- """
- use Mv.DataCase, async: false
-
- alias Mv.Membership
-
- require Ash.Query
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
- %{actor: system_actor}
- end
-
- defp create_group_fixture do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, group} =
- Membership.create_group(
- %{name: "Test Group #{System.unique_integer([:positive])}", description: "Test"},
- actor: admin
- )
-
- group
- end
-
- describe "own_data permission set" do
- setup %{actor: _actor} do
- user = Mv.Fixtures.user_with_role_fixture("own_data")
- group = create_group_fixture()
- %{user: user, group: group}
- end
-
- test "can read groups (list)", %{user: user} do
- {:ok, groups} = Membership.list_groups(actor: user)
- assert is_list(groups)
- end
-
- test "can read single group", %{user: user, group: group} do
- {:ok, found} = Ash.get(Membership.Group, group.id, actor: user, domain: Mv.Membership)
- assert found.id == group.id
- end
- end
-
- describe "read_only permission set" do
- setup %{actor: _actor} do
- user = Mv.Fixtures.user_with_role_fixture("read_only")
- group = create_group_fixture()
- %{user: user, group: group}
- end
-
- test "can read groups (list)", %{user: user} do
- {:ok, groups} = Membership.list_groups(actor: user)
- assert is_list(groups)
- end
-
- test "can read single group", %{user: user, group: group} do
- {:ok, found} = Ash.get(Membership.Group, group.id, actor: user, domain: Mv.Membership)
- assert found.id == group.id
- end
- end
-
- describe "normal_user permission set" do
- setup %{actor: _actor} do
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
- group = create_group_fixture()
- %{user: user, group: group}
- end
-
- test "can read groups (list)", %{user: user} do
- {:ok, groups} = Membership.list_groups(actor: user)
- assert is_list(groups)
- end
-
- test "can read single group", %{user: user, group: group} do
- {:ok, found} = Ash.get(Membership.Group, group.id, actor: user, domain: Mv.Membership)
- assert found.id == group.id
- end
-
- test "can create group", %{user: user} do
- assert {:ok, created} =
- Membership.create_group(
- %{name: "New Group #{System.unique_integer([:positive])}", description: "New"},
- actor: user
- )
-
- assert created.name =~ "New Group"
- end
-
- test "can update group", %{user: user, group: group} do
- assert {:ok, updated} =
- Membership.update_group(group, %{description: "Updated"}, actor: user)
-
- assert updated.description == "Updated"
- end
-
- test "can destroy group", %{user: user, group: group} do
- assert :ok = Membership.destroy_group(group, actor: user)
- end
- end
-
- describe "admin permission set" do
- setup %{actor: _actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
- group = create_group_fixture()
- %{user: user, group: group}
- end
-
- test "can read groups (list)", %{user: user} do
- {:ok, groups} = Membership.list_groups(actor: user)
- assert is_list(groups)
- end
-
- test "can create group", %{user: user} do
- name = "Admin Group #{System.unique_integer([:positive])}"
-
- assert {:ok, group} =
- Membership.create_group(%{name: name, description: "Admin created"}, actor: user)
-
- assert group.name == name
- end
-
- test "can update group", %{user: user, group: group} do
- assert {:ok, updated} =
- Membership.update_group(group, %{description: "Updated by admin"}, actor: user)
-
- assert updated.description == "Updated by admin"
- end
-
- test "can destroy group", %{user: user, group: group} do
- assert :ok = Membership.destroy_group(group, actor: user)
-
- assert {:error, _} = Ash.get(Membership.Group, group.id, actor: user, domain: Mv.Membership)
- end
- end
-end
diff --git a/test/mv/membership/member_email_validation_test.exs b/test/mv/membership/member_email_validation_test.exs
index 2c234a7..d1b5a10 100644
--- a/test/mv/membership/member_email_validation_test.exs
+++ b/test/mv/membership/member_email_validation_test.exs
@@ -8,6 +8,7 @@ defmodule Mv.Membership.MemberEmailValidationTest do
use Mv.DataCase, async: false
alias Mv.Accounts
+ alias Mv.Authorization
alias Mv.Helpers.SystemActor
alias Mv.Membership
@@ -16,8 +17,49 @@ defmodule Mv.Membership.MemberEmailValidationTest do
%{actor: system_actor}
end
- defp create_linked_member_for_user(user, _actor) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ defp create_role_with_permission_set(permission_set_name, actor) do
+ role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
+
+ case Authorization.create_role(
+ %{
+ name: role_name,
+ description: "Test role for #{permission_set_name}",
+ permission_set_name: permission_set_name
+ },
+ actor: actor
+ ) do
+ {:ok, role} -> role
+ {:error, error} -> raise "Failed to create role: #{inspect(error)}"
+ end
+ end
+
+ defp create_user_with_permission_set(permission_set_name, actor) do
+ role = create_role_with_permission_set(permission_set_name, actor)
+
+ {:ok, user} =
+ Accounts.User
+ |> Ash.Changeset.for_create(:register_with_password, %{
+ email: "user#{System.unique_integer([:positive])}@example.com",
+ password: "testpassword123"
+ })
+ |> Ash.create(actor: actor)
+
+ {:ok, user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{})
+ |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
+ |> Ash.update(actor: actor)
+
+ {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
+ user_with_role
+ end
+
+ defp create_admin_user(actor) do
+ create_user_with_permission_set("admin", actor)
+ end
+
+ defp create_linked_member_for_user(user, actor) do
+ admin = create_admin_user(actor)
{:ok, member} =
Membership.create_member(
@@ -37,8 +79,8 @@ defmodule Mv.Membership.MemberEmailValidationTest do
member
end
- defp create_unlinked_member(_actor) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ defp create_unlinked_member(actor) do
+ admin = create_admin_user(actor)
{:ok, member} =
Membership.create_member(
@@ -55,7 +97,7 @@ defmodule Mv.Membership.MemberEmailValidationTest do
describe "unlinked member" do
test "normal_user can update email of unlinked member", %{actor: actor} do
- normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
+ normal_user = create_user_with_permission_set("normal_user", actor)
unlinked_member = create_unlinked_member(actor)
new_email = "new#{System.unique_integer([:positive])}@example.com"
@@ -67,7 +109,7 @@ defmodule Mv.Membership.MemberEmailValidationTest do
end
test "validation does not block when member has no linked user", %{actor: actor} do
- normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
+ normal_user = create_user_with_permission_set("normal_user", actor)
unlinked_member = create_unlinked_member(actor)
new_email = "other#{System.unique_integer([:positive])}@example.com"
@@ -79,10 +121,10 @@ defmodule Mv.Membership.MemberEmailValidationTest do
describe "linked member – another user's member" do
test "normal_user cannot update email of another user's linked member", %{actor: actor} do
- user_a = Mv.Fixtures.user_with_role_fixture("own_data")
+ user_a = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user_a, actor)
- normal_user_b = Mv.Fixtures.user_with_role_fixture("normal_user")
+ normal_user_b = create_user_with_permission_set("normal_user", actor)
new_email = "other#{System.unique_integer([:positive])}@example.com"
assert {:error, %Ash.Error.Invalid{} = error} =
@@ -93,9 +135,9 @@ defmodule Mv.Membership.MemberEmailValidationTest do
end
test "admin can update email of linked member", %{actor: actor} do
- user_a = Mv.Fixtures.user_with_role_fixture("own_data")
+ user_a = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user_a, actor)
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ admin = create_admin_user(actor)
new_email = "admin_changed#{System.unique_integer([:positive])}@example.com"
@@ -108,7 +150,7 @@ defmodule Mv.Membership.MemberEmailValidationTest do
describe "linked member – own member" do
test "own_data user can update email of their own linked member", %{actor: actor} do
- own_data_user = Mv.Fixtures.user_with_role_fixture("own_data")
+ own_data_user = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(own_data_user, actor)
{:ok, own_data_user} =
@@ -126,7 +168,7 @@ defmodule Mv.Membership.MemberEmailValidationTest do
end
test "normal_user with linked member can update email of that same member", %{actor: actor} do
- normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
+ normal_user = create_user_with_permission_set("normal_user", actor)
linked_member = create_linked_member_for_user(normal_user, actor)
{:ok, normal_user} =
@@ -146,9 +188,9 @@ defmodule Mv.Membership.MemberEmailValidationTest do
describe "no-op / other fields" do
test "updating only other attributes on linked member as normal_user does not trigger validation error",
%{actor: actor} do
- user_a = Mv.Fixtures.user_with_role_fixture("own_data")
+ user_a = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user_a, actor)
- normal_user_b = Mv.Fixtures.user_with_role_fixture("normal_user")
+ normal_user_b = create_user_with_permission_set("normal_user", actor)
assert {:ok, updated} =
Membership.update_member(linked_member, %{first_name: "UpdatedName"},
@@ -160,9 +202,9 @@ defmodule Mv.Membership.MemberEmailValidationTest do
end
test "updating email of linked member as admin succeeds", %{actor: actor} do
- user_a = Mv.Fixtures.user_with_role_fixture("own_data")
+ user_a = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user_a, actor)
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ admin = create_admin_user(actor)
new_email = "admin_ok#{System.unique_integer([:positive])}@example.com"
@@ -175,7 +217,7 @@ defmodule Mv.Membership.MemberEmailValidationTest do
describe "read_only" do
test "read_only cannot update any member (policy rejects before validation)", %{actor: actor} do
- read_only_user = Mv.Fixtures.user_with_role_fixture("read_only")
+ read_only_user = create_user_with_permission_set("read_only", actor)
linked_member = create_linked_member_for_user(read_only_user, actor)
{:ok, read_only_user} =
diff --git a/test/mv/membership/member_group_policies_test.exs b/test/mv/membership/member_group_policies_test.exs
deleted file mode 100644
index ecac2f4..0000000
--- a/test/mv/membership/member_group_policies_test.exs
+++ /dev/null
@@ -1,234 +0,0 @@
-defmodule Mv.Membership.MemberGroupPoliciesTest do
- @moduledoc """
- Tests for MemberGroup resource authorization policies.
-
- Verifies own_data can only read linked member's associations;
- read_only can read all, cannot create/destroy;
- normal_user and admin can read, create, destroy.
- """
- use Mv.DataCase, async: false
-
- alias Mv.Membership
-
- require Ash.Query
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
- %{actor: system_actor}
- end
-
- defp create_member_fixture do
- Mv.Fixtures.member_fixture()
- end
-
- defp create_group_fixture do
- Mv.Fixtures.group_fixture()
- end
-
- defp create_member_group_fixture(member_id, group_id) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, member_group} =
- Membership.create_member_group(%{member_id: member_id, group_id: group_id}, actor: admin)
-
- member_group
- end
-
- describe "own_data permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("own_data")
- member = create_member_fixture()
- group = create_group_fixture()
- # Link user to member so actor.member_id is set
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- user =
- user
- |> Ash.Changeset.for_update(:update, %{})
- |> Ash.Changeset.force_change_attribute(:member_id, member.id)
- |> Ash.update(actor: admin)
-
- {:ok, user} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
- mg_linked = create_member_group_fixture(member.id, group.id)
- # MemberGroup for another member (not linked to user)
- other_member = create_member_fixture()
- other_group = create_group_fixture()
- mg_other = create_member_group_fixture(other_member.id, other_group.id)
- %{user: user, member: member, group: group, mg_linked: mg_linked, mg_other: mg_other}
- end
-
- test "can read member_groups for linked member only", %{user: user, mg_linked: mg_linked} do
- {:ok, list} =
- Mv.Membership.MemberGroup
- |> Ash.read(actor: user, domain: Mv.Membership)
-
- ids = Enum.map(list, & &1.id)
- assert mg_linked.id in ids
- refute Enum.empty?(list)
- end
-
- test "list returns only member_groups where member_id == actor.member_id", %{
- user: user,
- mg_linked: mg_linked,
- mg_other: mg_other
- } do
- {:ok, list} =
- Mv.Membership.MemberGroup
- |> Ash.read(actor: user, domain: Mv.Membership)
-
- ids = Enum.map(list, & &1.id)
- assert mg_linked.id in ids
- refute mg_other.id in ids
- end
-
- test "cannot create member_group (returns forbidden)", %{user: user, actor: _actor} do
- # Use fresh member/group so we assert on Forbidden, not on duplicate validation
- other_member = create_member_fixture()
- other_group = create_group_fixture()
-
- assert {:error, %Ash.Error.Forbidden{}} =
- Membership.create_member_group(
- %{member_id: other_member.id, group_id: other_group.id},
- actor: user
- )
- end
-
- test "cannot destroy member_group (returns forbidden)", %{user: user, mg_linked: mg_linked} do
- assert {:error, %Ash.Error.Forbidden{}} =
- Membership.destroy_member_group(mg_linked, actor: user)
- end
- end
-
- describe "read_only permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("read_only")
- member = create_member_fixture()
- group = create_group_fixture()
- mg = create_member_group_fixture(member.id, group.id)
- %{actor: actor, user: user, member: member, group: group, mg: mg}
- end
-
- test "can read all member_groups", %{user: user, mg: mg} do
- {:ok, list} =
- Mv.Membership.MemberGroup
- |> Ash.read(actor: user, domain: Mv.Membership)
-
- ids = Enum.map(list, & &1.id)
- assert mg.id in ids
- end
-
- test "cannot create member_group (returns forbidden)", %{user: user, actor: _actor} do
- member = create_member_fixture()
- group = create_group_fixture()
-
- assert {:error, %Ash.Error.Forbidden{}} =
- Membership.create_member_group(%{member_id: member.id, group_id: group.id},
- actor: user
- )
- end
-
- test "cannot destroy member_group (returns forbidden)", %{user: user, mg: mg} do
- assert {:error, %Ash.Error.Forbidden{}} =
- Membership.destroy_member_group(mg, actor: user)
- end
- end
-
- describe "normal_user permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
- member = create_member_fixture()
- group = create_group_fixture()
- mg = create_member_group_fixture(member.id, group.id)
- %{actor: actor, user: user, member: member, group: group, mg: mg}
- end
-
- test "can read all member_groups", %{user: user, mg: mg} do
- {:ok, list} =
- Mv.Membership.MemberGroup
- |> Ash.read(actor: user, domain: Mv.Membership)
-
- ids = Enum.map(list, & &1.id)
- assert mg.id in ids
- end
-
- test "can create member_group", %{user: user, actor: _actor} do
- member = create_member_fixture()
- group = create_group_fixture()
-
- assert {:ok, _mg} =
- Membership.create_member_group(%{member_id: member.id, group_id: group.id},
- actor: user
- )
- end
-
- test "can destroy member_group", %{user: user, mg: mg} do
- assert :ok = Membership.destroy_member_group(mg, actor: user)
- end
- end
-
- describe "admin permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
- member = create_member_fixture()
- group = create_group_fixture()
- mg = create_member_group_fixture(member.id, group.id)
- %{actor: actor, user: user, member: member, group: group, mg: mg}
- end
-
- test "can read all member_groups", %{user: user, mg: mg} do
- {:ok, list} =
- Mv.Membership.MemberGroup
- |> Ash.read(actor: user, domain: Mv.Membership)
-
- ids = Enum.map(list, & &1.id)
- assert mg.id in ids
- end
-
- test "admin with member_id set (linked to member) still reads all member_groups", %{
- actor: actor
- } do
- # Admin linked to a member (e.g. viewing as member context) must still get :all scope,
- # not restricted to linked member's groups (bypass is only for own_data).
- admin = Mv.Fixtures.user_with_role_fixture("admin")
- linked_member = create_member_fixture()
- other_member = create_member_fixture()
- group_a = create_group_fixture()
- group_b = create_group_fixture()
-
- admin =
- admin
- |> Ash.Changeset.for_update(:update, %{})
- |> Ash.Changeset.force_change_attribute(:member_id, linked_member.id)
- |> Ash.update(actor: actor)
-
- {:ok, admin} = Ash.load(admin, :role, domain: Mv.Accounts, actor: actor)
-
- mg_linked = create_member_group_fixture(linked_member.id, group_a.id)
- mg_other = create_member_group_fixture(other_member.id, group_b.id)
-
- {:ok, list} =
- Mv.Membership.MemberGroup
- |> Ash.read(actor: admin, domain: Mv.Membership)
-
- ids = Enum.map(list, & &1.id)
- assert mg_linked.id in ids, "Admin with member_id must see linked member's MemberGroups"
-
- assert mg_other.id in ids,
- "Admin with member_id must see all MemberGroups (:all), not only linked"
- end
-
- test "can create member_group", %{user: user, actor: _actor} do
- member = create_member_fixture()
- group = create_group_fixture()
-
- assert {:ok, _mg} =
- Membership.create_member_group(%{member_id: member.id, group_id: group.id},
- actor: user
- )
- end
-
- test "can destroy member_group", %{user: user, mg: mg} do
- assert :ok = Membership.destroy_member_group(mg, actor: user)
- end
- end
-end
diff --git a/test/mv/membership/member_policies_test.exs b/test/mv/membership/member_policies_test.exs
index a66941b..026c3c4 100644
--- a/test/mv/membership/member_policies_test.exs
+++ b/test/mv/membership/member_policies_test.exs
@@ -12,6 +12,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
alias Mv.Membership
alias Mv.Accounts
+ alias Mv.Authorization
require Ash.Query
@@ -20,9 +21,58 @@ defmodule Mv.Membership.MemberPoliciesTest do
%{actor: system_actor}
end
+ # Helper to create a role with a specific permission set
+ defp create_role_with_permission_set(permission_set_name, actor) do
+ role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
+
+ case Authorization.create_role(
+ %{
+ name: role_name,
+ description: "Test role for #{permission_set_name}",
+ permission_set_name: permission_set_name
+ },
+ actor: actor
+ ) do
+ {:ok, role} -> role
+ {:error, error} -> raise "Failed to create role: #{inspect(error)}"
+ end
+ end
+
+ # Helper to create a user with a specific permission set
+ # Returns user with role preloaded (required for authorization)
+ defp create_user_with_permission_set(permission_set_name, actor) do
+ # Create role with permission set
+ role = create_role_with_permission_set(permission_set_name, actor)
+
+ # Create user
+ {:ok, user} =
+ Accounts.User
+ |> Ash.Changeset.for_create(:register_with_password, %{
+ email: "user#{System.unique_integer([:positive])}@example.com",
+ password: "testpassword123"
+ })
+ |> Ash.create(actor: actor)
+
+ # Assign role to user
+ {:ok, user} =
+ user
+ |> Ash.Changeset.for_update(:update, %{})
+ |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
+ |> Ash.update(actor: actor)
+
+ # Reload user with role preloaded (critical for authorization!)
+ {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
+ user_with_role
+ end
+
+ # Helper to create an admin user (for creating test fixtures)
+ defp create_admin_user(actor) do
+ create_user_with_permission_set("admin", actor)
+ end
+
# Helper to create a member linked to a user
- defp create_linked_member_for_user(user, _actor) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ defp create_linked_member_for_user(user, actor) do
+ admin = create_admin_user(actor)
# Create member
# NOTE: We need to ensure the member is actually persisted to the database
@@ -55,8 +105,8 @@ defmodule Mv.Membership.MemberPoliciesTest do
end
# Helper to create an unlinked member (no user relationship)
- defp create_unlinked_member(_actor) do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
+ defp create_unlinked_member(actor) do
+ admin = create_admin_user(actor)
{:ok, member} =
Membership.create_member(
@@ -73,7 +123,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
describe "own_data permission set (Mitglied)" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("own_data")
+ user = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
@@ -157,7 +207,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
describe "read_only permission set (Vorstand/Buchhaltung)" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("read_only")
+ user = create_user_with_permission_set("read_only", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
@@ -223,7 +273,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
describe "normal_user permission set (Kassenwart)" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
+ user = create_user_with_permission_set("normal_user", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
@@ -280,7 +330,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
describe "admin permission set" do
setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
+ user = create_user_with_permission_set("admin", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
@@ -347,7 +397,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
# read_only has Member.read scope :all, but the special case ensures
# users can ALWAYS read their linked member, even if they had no read permission.
# This test verifies the special case works independently of permission sets.
- user = Mv.Fixtures.user_with_role_fixture("read_only")
+ user = create_user_with_permission_set("read_only", actor)
linked_member = create_linked_member_for_user(user, actor)
# Reload user to get updated member_id
@@ -366,7 +416,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
test "own_data user can read linked member (via special case bypass)", %{actor: actor} do
# own_data has Member.read scope :linked, but the special case ensures
# users can ALWAYS read their linked member regardless of permission set.
- user = Mv.Fixtures.user_with_role_fixture("own_data")
+ user = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user, actor)
# Reload user to get updated member_id
@@ -387,7 +437,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
} do
# Update is NOT handled by special case - it's handled by HasPermission
# with :linked scope. own_data has Member.update scope :linked.
- user = Mv.Fixtures.user_with_role_fixture("own_data")
+ user = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user, actor)
# Reload user to get updated member_id
diff --git a/test/mv/membership_fees/membership_fee_cycle_policies_test.exs b/test/mv/membership_fees/membership_fee_cycle_policies_test.exs
deleted file mode 100644
index 4d0badb..0000000
--- a/test/mv/membership_fees/membership_fee_cycle_policies_test.exs
+++ /dev/null
@@ -1,294 +0,0 @@
-defmodule Mv.MembershipFees.MembershipFeeCyclePoliciesTest do
- @moduledoc """
- Tests for MembershipFeeCycle resource authorization policies.
-
- Verifies own_data can only read :linked (linked member's cycles);
- read_only can only read (no create/update/destroy);
- normal_user and admin can read, create, update, destroy (including mark_as_paid).
- """
- use Mv.DataCase, async: false
-
- alias Mv.MembershipFees
- alias Mv.Membership
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
- %{actor: system_actor}
- end
-
- defp create_member_fixture do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, member} =
- Membership.create_member(
- %{
- first_name: "Test",
- last_name: "Member",
- email: "test#{System.unique_integer([:positive])}@example.com"
- },
- actor: admin
- )
-
- member
- end
-
- defp create_fee_type_fixture do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, fee_type} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "Test Fee #{System.unique_integer([:positive])}",
- amount: Decimal.new("10.00"),
- interval: :yearly,
- description: "Test"
- },
- actor: admin
- )
-
- fee_type
- end
-
- defp create_cycle_fixture do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
- member = create_member_fixture()
- fee_type = create_fee_type_fixture()
-
- {:ok, cycle} =
- MembershipFees.create_membership_fee_cycle(
- %{
- member_id: member.id,
- membership_fee_type_id: fee_type.id,
- cycle_start: Date.utc_today(),
- amount: Decimal.new("10.00"),
- status: :unpaid
- },
- actor: admin
- )
-
- cycle
- end
-
- describe "own_data permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("own_data")
- linked_member = create_member_fixture()
- other_member = create_member_fixture()
- fee_type = create_fee_type_fixture()
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- user =
- user
- |> Ash.Changeset.for_update(:update, %{}, domain: Mv.Accounts)
- |> Ash.Changeset.force_change_attribute(:member_id, linked_member.id)
- |> Ash.update(actor: admin, domain: Mv.Accounts)
-
- {:ok, user} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
-
- {:ok, cycle_linked} =
- MembershipFees.create_membership_fee_cycle(
- %{
- member_id: linked_member.id,
- membership_fee_type_id: fee_type.id,
- cycle_start: Date.utc_today(),
- amount: Decimal.new("10.00"),
- status: :unpaid
- },
- actor: admin
- )
-
- {:ok, cycle_other} =
- MembershipFees.create_membership_fee_cycle(
- %{
- member_id: other_member.id,
- membership_fee_type_id: fee_type.id,
- cycle_start: Date.add(Date.utc_today(), -365),
- amount: Decimal.new("10.00"),
- status: :unpaid
- },
- actor: admin
- )
-
- %{user: user, cycle_linked: cycle_linked, cycle_other: cycle_other}
- end
-
- test "can read only linked member's cycles", %{
- user: user,
- cycle_linked: cycle_linked,
- cycle_other: cycle_other
- } do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeCycle
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- ids = Enum.map(list, & &1.id)
- assert cycle_linked.id in ids
- refute cycle_other.id in ids
- end
- end
-
- describe "read_only permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("read_only")
- cycle = create_cycle_fixture()
- %{actor: actor, user: user, cycle: cycle}
- end
-
- test "can read membership_fee_cycles (list)", %{user: user} do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeCycle
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- assert is_list(list)
- end
-
- test "cannot update cycle (returns forbidden)", %{user: user, cycle: cycle} do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user)
- end
-
- test "cannot mark_as_paid (returns forbidden)", %{user: user, cycle: cycle} do
- assert {:error, %Ash.Error.Forbidden{}} =
- cycle
- |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees)
- |> Ash.update(actor: user, domain: Mv.MembershipFees)
- end
-
- test "cannot create cycle (returns forbidden)", %{user: user, actor: _actor} do
- member = create_member_fixture()
- fee_type = create_fee_type_fixture()
-
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.create_membership_fee_cycle(
- %{
- member_id: member.id,
- membership_fee_type_id: fee_type.id,
- cycle_start: Date.utc_today(),
- amount: Decimal.new("10.00"),
- status: :unpaid
- },
- actor: user
- )
- end
-
- test "cannot destroy cycle (returns forbidden)", %{user: user, cycle: cycle} do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.destroy_membership_fee_cycle(cycle, actor: user)
- end
- end
-
- describe "normal_user permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
- cycle = create_cycle_fixture()
- %{actor: actor, user: user, cycle: cycle}
- end
-
- test "can read membership_fee_cycles (list)", %{user: user} do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeCycle
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- assert is_list(list)
- end
-
- test "can update cycle status", %{user: user, cycle: cycle} do
- assert {:ok, updated} =
- MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user)
-
- assert updated.status == :paid
- end
-
- test "can mark_as_paid", %{user: user, cycle: cycle} do
- assert {:ok, updated} =
- cycle
- |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees)
- |> Ash.update(actor: user, domain: Mv.MembershipFees)
-
- assert updated.status == :paid
- end
-
- test "can create cycle", %{user: user, actor: _actor} do
- member = create_member_fixture()
- fee_type = create_fee_type_fixture()
-
- assert {:ok, created} =
- MembershipFees.create_membership_fee_cycle(
- %{
- member_id: member.id,
- membership_fee_type_id: fee_type.id,
- cycle_start: Date.utc_today(),
- amount: Decimal.new("10.00"),
- status: :unpaid
- },
- actor: user
- )
-
- assert created.member_id == member.id
- end
-
- test "can destroy cycle", %{user: user, cycle: cycle} do
- assert :ok = MembershipFees.destroy_membership_fee_cycle(cycle, actor: user)
- end
- end
-
- describe "admin permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
- cycle = create_cycle_fixture()
- %{actor: actor, user: user, cycle: cycle}
- end
-
- test "can read membership_fee_cycles (list)", %{user: user} do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeCycle
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- assert is_list(list)
- end
-
- test "can update cycle", %{user: user, cycle: cycle} do
- assert {:ok, updated} =
- MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user)
-
- assert updated.status == :paid
- end
-
- test "can mark_as_paid", %{user: user, cycle: cycle} do
- cycle_unpaid =
- cycle
- |> Ash.Changeset.for_update(:mark_as_unpaid, %{}, domain: Mv.MembershipFees)
- |> Ash.update!(actor: user, domain: Mv.MembershipFees)
-
- assert {:ok, updated} =
- cycle_unpaid
- |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees)
- |> Ash.update(actor: user, domain: Mv.MembershipFees)
-
- assert updated.status == :paid
- end
-
- test "can create cycle", %{user: user, actor: _actor} do
- member = create_member_fixture()
- fee_type = create_fee_type_fixture()
-
- assert {:ok, created} =
- MembershipFees.create_membership_fee_cycle(
- %{
- member_id: member.id,
- membership_fee_type_id: fee_type.id,
- cycle_start: Date.utc_today(),
- amount: Decimal.new("10.00"),
- status: :unpaid
- },
- actor: user
- )
-
- assert created.member_id == member.id
- end
-
- test "can destroy cycle", %{user: user, cycle: cycle} do
- assert :ok = MembershipFees.destroy_membership_fee_cycle(cycle, actor: user)
- end
- end
-end
diff --git a/test/mv/membership_fees/membership_fee_type_policies_test.exs b/test/mv/membership_fees/membership_fee_type_policies_test.exs
deleted file mode 100644
index 9fd3f5c..0000000
--- a/test/mv/membership_fees/membership_fee_type_policies_test.exs
+++ /dev/null
@@ -1,260 +0,0 @@
-defmodule Mv.MembershipFees.MembershipFeeTypePoliciesTest do
- @moduledoc """
- Tests for MembershipFeeType resource authorization policies.
-
- Verifies all roles (own_data, read_only, normal_user, admin) can read;
- only admin can create, update, and destroy; non-admin create/update/destroy → Forbidden.
- """
- use Mv.DataCase, async: false
-
- alias Mv.MembershipFees
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
- %{actor: system_actor}
- end
-
- defp create_membership_fee_type_fixture do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, fee_type} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "Test Fee #{System.unique_integer([:positive])}",
- amount: Decimal.new("10.00"),
- interval: :yearly,
- description: "Test"
- },
- actor: admin
- )
-
- fee_type
- end
-
- describe "own_data permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("own_data")
- fee_type = create_membership_fee_type_fixture()
- %{actor: actor, user: user, fee_type: fee_type}
- end
-
- test "can read membership_fee_types (list)", %{user: user} do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeType
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- assert is_list(list)
- end
-
- test "can read single membership_fee_type", %{user: user, fee_type: fee_type} do
- {:ok, found} =
- Ash.get(Mv.MembershipFees.MembershipFeeType, fee_type.id,
- actor: user,
- domain: Mv.MembershipFees
- )
-
- assert found.id == fee_type.id
- end
-
- test "cannot create membership_fee_type (returns forbidden)", %{user: user} do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "New Fee #{System.unique_integer([:positive])}",
- amount: Decimal.new("5.00"),
- interval: :monthly
- },
- actor: user
- )
- end
-
- test "cannot update membership_fee_type (returns forbidden)", %{
- user: user,
- fee_type: fee_type
- } do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.update_membership_fee_type(fee_type, %{name: "Updated"},
- actor: user
- )
- end
-
- test "cannot destroy membership_fee_type (returns forbidden)", %{user: user, actor: _actor} do
- # Use a fee type with no members/cycles so destroy would succeed if authorized
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, isolated} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "Isolated #{System.unique_integer([:positive])}",
- amount: Decimal.new("1.00"),
- interval: :yearly
- },
- actor: admin
- )
-
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.destroy_membership_fee_type(isolated, actor: user)
- end
- end
-
- describe "read_only permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("read_only")
- fee_type = create_membership_fee_type_fixture()
- %{actor: actor, user: user, fee_type: fee_type}
- end
-
- test "can read membership_fee_types (list)", %{user: user} do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeType
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- assert is_list(list)
- end
-
- test "cannot create membership_fee_type (returns forbidden)", %{user: user} do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "New Fee #{System.unique_integer([:positive])}",
- amount: Decimal.new("5.00"),
- interval: :monthly
- },
- actor: user
- )
- end
-
- test "cannot update membership_fee_type (returns forbidden)", %{
- user: user,
- fee_type: fee_type
- } do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.update_membership_fee_type(fee_type, %{name: "Updated"},
- actor: user
- )
- end
-
- test "cannot destroy membership_fee_type (returns forbidden)", %{user: user, actor: _actor} do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, isolated} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "Isolated #{System.unique_integer([:positive])}",
- amount: Decimal.new("1.00"),
- interval: :yearly
- },
- actor: admin
- )
-
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.destroy_membership_fee_type(isolated, actor: user)
- end
- end
-
- describe "normal_user permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("normal_user")
- fee_type = create_membership_fee_type_fixture()
- %{actor: actor, user: user, fee_type: fee_type}
- end
-
- test "can read membership_fee_types (list)", %{user: user} do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeType
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- assert is_list(list)
- end
-
- test "cannot create membership_fee_type (returns forbidden)", %{user: user} do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "New Fee #{System.unique_integer([:positive])}",
- amount: Decimal.new("5.00"),
- interval: :monthly
- },
- actor: user
- )
- end
-
- test "cannot update membership_fee_type (returns forbidden)", %{
- user: user,
- fee_type: fee_type
- } do
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.update_membership_fee_type(fee_type, %{name: "Updated"},
- actor: user
- )
- end
-
- test "cannot destroy membership_fee_type (returns forbidden)", %{user: user, actor: _actor} do
- admin = Mv.Fixtures.user_with_role_fixture("admin")
-
- {:ok, isolated} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "Isolated #{System.unique_integer([:positive])}",
- amount: Decimal.new("1.00"),
- interval: :yearly
- },
- actor: admin
- )
-
- assert {:error, %Ash.Error.Forbidden{}} =
- MembershipFees.destroy_membership_fee_type(isolated, actor: user)
- end
- end
-
- describe "admin permission set" do
- setup %{actor: actor} do
- user = Mv.Fixtures.user_with_role_fixture("admin")
- fee_type = create_membership_fee_type_fixture()
- %{actor: actor, user: user, fee_type: fee_type}
- end
-
- test "can read membership_fee_types (list)", %{user: user} do
- {:ok, list} =
- Mv.MembershipFees.MembershipFeeType
- |> Ash.read(actor: user, domain: Mv.MembershipFees)
-
- assert is_list(list)
- end
-
- test "can create membership_fee_type", %{user: user} do
- name = "Admin Fee #{System.unique_integer([:positive])}"
-
- assert {:ok, created} =
- MembershipFees.create_membership_fee_type(
- %{name: name, amount: Decimal.new("20.00"), interval: :quarterly},
- actor: user
- )
-
- assert created.name == name
- end
-
- test "can update membership_fee_type", %{user: user, fee_type: fee_type} do
- new_name = "Updated #{System.unique_integer([:positive])}"
-
- assert {:ok, updated} =
- MembershipFees.update_membership_fee_type(fee_type, %{name: new_name}, actor: user)
-
- assert updated.name == new_name
- end
-
- test "can destroy membership_fee_type", %{user: user} do
- {:ok, isolated} =
- MembershipFees.create_membership_fee_type(
- %{
- name: "To Delete #{System.unique_integer([:positive])}",
- amount: Decimal.new("1.00"),
- interval: :yearly
- },
- actor: user
- )
-
- assert :ok = MembershipFees.destroy_membership_fee_type(isolated, actor: user)
- end
- end
-end
diff --git a/test/mv_web/helpers/membership_fee_helpers_test.exs b/test/mv_web/helpers/membership_fee_helpers_test.exs
index 6726091..530143f 100644
--- a/test/mv_web/helpers/membership_fee_helpers_test.exs
+++ b/test/mv_web/helpers/membership_fee_helpers_test.exs
@@ -134,8 +134,8 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
# Load cycles with membership_fee_type relationship
member =
member
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: actor)
- |> Ash.load!(:membership_fee_type, actor: actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
# Use a fixed date in 2024 to ensure 2023 is last completed
today = ~D[2024-06-15]
@@ -180,8 +180,8 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
# Load cycles and fee type (will be empty)
member =
member
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: actor)
- |> Ash.load!(:membership_fee_type, actor: actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
last_cycle = MembershipFeeHelpers.get_last_completed_cycle(member, Date.utc_today())
assert last_cycle == nil
@@ -245,8 +245,8 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
# Load cycles with membership_fee_type relationship
member =
member
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: actor)
- |> Ash.load!(:membership_fee_type, actor: actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
result = MembershipFeeHelpers.get_current_cycle(member, today)
diff --git a/test/mv_web/live/membership_fee_type_live/form_test.exs b/test/mv_web/live/membership_fee_type_live/form_test.exs
index 71edbba..f0a21c7 100644
--- a/test/mv_web/live/membership_fee_type_live/form_test.exs
+++ b/test/mv_web/live/membership_fee_type_live/form_test.exs
@@ -50,7 +50,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
end
describe "create form" do
- test "creates new membership fee type", %{conn: conn, user: user} do
+ test "creates new membership fee type", %{conn: conn} do
{:ok, view, _html} = live(conn, "/membership_fee_types/new")
form_data = %{
@@ -67,13 +67,12 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
assert to == "/membership_fee_types"
- # Verify type was created (use actor so read is authorized)
+ # Verify type was created
type =
MembershipFeeType
|> Ash.Query.filter(name == "New Type")
- |> Ash.read_one!(domain: Mv.MembershipFees, actor: user)
+ |> Ash.read_one!()
- assert type != nil, "Expected membership fee type to be created"
assert type.amount == Decimal.new("75.00")
assert type.interval == :yearly
end
@@ -141,7 +140,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
assert html =~ "3" || html =~ "members" || html =~ "Mitglieder"
end
- test "amount change can be confirmed", %{conn: conn, user: user} do
+ test "amount change can be confirmed", %{conn: conn} do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
@@ -160,17 +159,12 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|> form("#membership-fee-type-form", %{"membership_fee_type[amount]" => "75.00"})
|> render_submit()
- # Amount should be updated (use actor so read is authorized)
- updated_type =
- MembershipFeeType
- |> Ash.Query.filter(id == ^fee_type.id)
- |> Ash.read_one!(domain: Mv.MembershipFees, actor: user)
-
- assert updated_type != nil
+ # Amount should be updated
+ updated_type = Ash.read_one!(MembershipFeeType |> Ash.Query.filter(id == ^fee_type.id))
assert updated_type.amount == Decimal.new("75.00")
end
- test "amount change can be cancelled", %{conn: conn, user: user} do
+ test "amount change can be cancelled", %{conn: conn} do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
@@ -184,13 +178,8 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|> element("button[phx-click='cancel_amount_change']")
|> render_click()
- # Amount should remain unchanged (use actor so read is authorized)
- updated_type =
- MembershipFeeType
- |> Ash.Query.filter(id == ^fee_type.id)
- |> Ash.read_one!(domain: Mv.MembershipFees, actor: user)
-
- assert updated_type != nil
+ # Amount should remain unchanged
+ updated_type = Ash.read_one!(MembershipFeeType |> Ash.Query.filter(id == ^fee_type.id))
assert updated_type.amount == Decimal.new("50.00")
end
diff --git a/test/mv_web/live/profile_navigation_test.exs b/test/mv_web/live/profile_navigation_test.exs
index 089d1fc..b8562cd 100644
--- a/test/mv_web/live/profile_navigation_test.exs
+++ b/test/mv_web/live/profile_navigation_test.exs
@@ -61,7 +61,6 @@ defmodule MvWeb.ProfileNavigationTest do
end
@tag :skip
- # credo:disable-for-next-line Credo.Check.Design.TagTODO
# TODO: Implement user initials in navbar avatar - see issue #170
test "shows user initials in avatar", %{conn: conn} do
# Setup: Create and login a user
diff --git a/test/mv_web/member_live/index/membership_fee_status_test.exs b/test/mv_web/member_live/index/membership_fee_status_test.exs
index aa729ef..950b65f 100644
--- a/test/mv_web/member_live/index/membership_fee_status_test.exs
+++ b/test/mv_web/member_live/index/membership_fee_status_test.exs
@@ -127,12 +127,10 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
# Load cycles with membership_fee_type relationship
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
member =
member
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
# Use fixed date in 2024 to ensure 2023 is last completed
# We need to manually set the date for the helper function
@@ -185,8 +183,8 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
# Load cycles with membership_fee_type relationship
member =
member
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
status = MembershipFeeStatus.get_cycle_status_for_member(member, true)
@@ -224,8 +222,8 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
# Load cycles and fee type first (will be empty)
member =
member
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
status = MembershipFeeStatus.get_cycle_status_for_member(member, false)
@@ -275,14 +273,12 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
member2 = create_member(%{membership_fee_type_id: fee_type.id})
create_cycle(member2, fee_type, %{cycle_start: last_year_start, status: :unpaid})
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
members =
[member1, member2]
|> Enum.map(fn m ->
m
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
end)
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :paid, false)
@@ -304,14 +300,12 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
member2 = create_member(%{membership_fee_type_id: fee_type.id})
create_cycle(member2, fee_type, %{cycle_start: last_year_start, status: :unpaid})
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
members =
[member1, member2]
|> Enum.map(fn m ->
m
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
end)
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :unpaid, false)
@@ -333,14 +327,12 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
member2 = create_member(%{membership_fee_type_id: fee_type.id})
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :unpaid})
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
members =
[member1, member2]
|> Enum.map(fn m ->
m
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
end)
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :paid, true)
@@ -362,14 +354,12 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
member2 = create_member(%{membership_fee_type_id: fee_type.id})
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :unpaid})
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
members =
[member1, member2]
|> Enum.map(fn m ->
m
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
end)
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :unpaid, true)
@@ -383,14 +373,12 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
member1 = create_member(%{membership_fee_type_id: fee_type.id})
member2 = create_member(%{membership_fee_type_id: fee_type.id})
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
members =
[member1, member2]
|> Enum.map(fn m ->
m
- |> Ash.load!([membership_fee_cycles: [:membership_fee_type]], actor: system_actor)
- |> Ash.load!(:membership_fee_type, actor: system_actor)
+ |> Ash.load!(membership_fee_cycles: [:membership_fee_type])
+ |> Ash.load!(:membership_fee_type)
end)
# filter_unpaid_members should still work for backwards compatibility
diff --git a/test/mv_web/member_live/show_membership_fees_test.exs b/test/mv_web/member_live/show_membership_fees_test.exs
index 57abfd1..20bf46d 100644
--- a/test/mv_web/member_live/show_membership_fees_test.exs
+++ b/test/mv_web/member_live/show_membership_fees_test.exs
@@ -28,6 +28,21 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|> Ash.create!(actor: system_actor)
end
+ # Helper to create a member
+ defp create_member(attrs) do
+ system_actor = Mv.Helpers.SystemActor.get_system_actor()
+
+ default_attrs = %{
+ first_name: "Test",
+ last_name: "Member",
+ email: "test.member.#{System.unique_integer([:positive])}@example.com"
+ }
+
+ attrs = Map.merge(default_attrs, attrs)
+ {:ok, member} = Mv.Membership.create_member(attrs, actor: system_actor)
+ member
+ end
+
# Helper to create a cycle
defp create_cycle(member, fee_type, attrs) do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
@@ -58,7 +73,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
describe "cycles table display" do
test "displays all cycles for member", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
+ member = create_member(%{membership_fee_type_id: fee_type.id})
_cycle1 = create_cycle(member, fee_type, %{cycle_start: ~D[2022-01-01], status: :paid})
_cycle2 = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
@@ -80,7 +95,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
test "table columns show correct data", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly, amount: Decimal.new("60.00")})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
+ member = create_member(%{membership_fee_type_id: fee_type.id})
create_cycle(member, fee_type, %{
cycle_start: ~D[2023-01-01],
@@ -109,7 +124,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
yearly_type = create_fee_type(%{interval: :yearly, name: "Yearly Type"})
_monthly_type = create_fee_type(%{interval: :monthly, name: "Monthly Type"})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: yearly_type.id})
+ member = create_member(%{membership_fee_type_id: yearly_type.id})
{:ok, _view, html} = live(conn, "/members/#{member.id}")
@@ -117,30 +132,20 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
assert html =~ "Yearly Type"
end
- test "shows no type message when no type assigned and Regenerate Cycles button is hidden", %{
- conn: conn
- } do
- member = Mv.Fixtures.member_fixture(%{})
+ test "shows no type message when no type assigned", %{conn: conn} do
+ member = create_member(%{})
- {:ok, view, html} = live(conn, "/members/#{member.id}")
+ {:ok, _view, html} = live(conn, "/members/#{member.id}")
# Should show message about no type assigned
assert html =~ "No membership fee type assigned" || html =~ "No type"
-
- # Switch to membership fees tab: message and no Regenerate Cycles button
- view
- |> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
- |> render_click()
-
- refute has_element?(view, "button[phx-click='regenerate_cycles']"),
- "Regenerate Cycles should be hidden when no membership fee type is assigned"
end
end
describe "status change actions" do
test "mark as paid works", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
+ member = create_member(%{membership_fee_type_id: fee_type.id})
cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
@@ -171,7 +176,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
test "mark as suspended works", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
+ member = create_member(%{membership_fee_type_id: fee_type.id})
cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
@@ -202,7 +207,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
test "mark as unpaid works", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
+ member = create_member(%{membership_fee_type_id: fee_type.id})
cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
@@ -235,7 +240,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
describe "cycle regeneration" do
test "manual regeneration button exists and can be clicked", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
+ member = create_member(%{membership_fee_type_id: fee_type.id})
{:ok, view, _html} = live(conn, "/members/#{member.id}")
@@ -261,7 +266,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
describe "edge cases" do
test "handles members without membership fee type gracefully", %{conn: conn} do
# No fee type
- member = Mv.Fixtures.member_fixture(%{})
+ member = create_member(%{})
{:ok, _view, html} = live(conn, "/members/#{member.id}")
@@ -269,120 +274,4 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
assert html =~ member.first_name
end
end
-
- describe "read_only user (Vorstand/Buchhaltung) - no cycle action buttons" do
- @tag role: :read_only
- test "read_only does not see Regenerate Cycles, Delete All Cycles, or Create Cycle buttons",
- %{
- conn: conn
- } do
- fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
- _cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
-
- {:ok, view, _html} = live(conn, "/members/#{member.id}")
-
- view
- |> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
- |> render_click()
-
- refute has_element?(view, "button[phx-click='regenerate_cycles']")
- refute has_element?(view, "button[phx-click='delete_all_cycles']")
- refute has_element?(view, "button[phx-click='open_create_cycle_modal']")
- end
-
- @tag role: :read_only
- test "read_only does not see Paid, Unpaid, Suspended, or Delete buttons in cycles table", %{
- conn: conn
- } do
- fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
- cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
-
- {:ok, view, _html} = live(conn, "/members/#{member.id}")
-
- view
- |> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
- |> render_click()
-
- # Row action buttons must not be present for read_only
- refute has_element?(view, "button[phx-click='mark_cycle_status']")
- refute has_element?(view, "button[phx-click='delete_cycle']")
- # Sanity: cycle row is present (read is allowed)
- assert has_element?(view, "tr[id='cycle-#{cycle.id}']")
- end
- end
-
- describe "read_only cannot delete all cycles (policy enforced via Ash.destroy)" do
- @tag role: :read_only
- test "Ash.destroy returns Forbidden for read_only so handler would reject", %{
- current_user: read_only_user
- } do
- # The handler uses Ash.destroy per cycle, so if the handler were triggered
- # (e.g. via dev tools), the server would enforce policy and show an error.
- # This test verifies that Ash.destroy(cycle, actor: read_only_user) returns Forbidden.
- fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
- cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
-
- assert {:error, %Ash.Error.Forbidden{}} =
- Ash.destroy(cycle, domain: Mv.MembershipFees, actor: read_only_user)
- end
- end
-
- describe "read_only cannot trigger regenerate_cycles (handler enforces can?)" do
- @tag role: :read_only
- test "read_only cannot create MembershipFeeCycle so regenerate_cycles handler would show flash error",
- %{current_user: read_only_user} do
- # The regenerate_cycles handler checks can?(actor, :create, MembershipFeeCycle) before
- # calling the generator. If a read_only user triggered the event (e.g. via DevTools),
- # the handler returns flash error and no new cycles are created.
- # This test verifies the condition the handler uses.
- refute MvWeb.Authorization.can?(read_only_user, :create, MembershipFeeCycle),
- "read_only must not be allowed to create MembershipFeeCycle so handler rejects regenerate_cycles"
- end
- end
-
- describe "confirm_delete_all_cycles handler (policy enforced)" do
- @tag role: :admin
- test "admin can delete all cycles via UI and cycles are removed", %{conn: conn} do
- # Use English locale so confirmation "Yes" matches gettext("Yes")
- conn = put_session(conn, :locale, "en")
-
- fee_type = create_fee_type(%{interval: :yearly})
- member = Mv.Fixtures.member_fixture(%{membership_fee_type_id: fee_type.id})
- _c1 = create_cycle(member, fee_type, %{cycle_start: ~D[2022-01-01], status: :paid})
- _c2 = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
-
- {:ok, view, _html} = live(conn, "/members/#{member.id}")
-
- view
- |> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
- |> render_click()
-
- view
- |> element("button[phx-click='delete_all_cycles']")
- |> render_click()
-
- view
- |> element("input[phx-keyup='update_delete_all_confirmation']")
- |> render_keyup(%{"value" => "Yes"})
-
- view
- |> element("button[phx-click='confirm_delete_all_cycles']")
- |> render_click()
-
- _html = render(view)
-
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- remaining =
- Mv.MembershipFees.MembershipFeeCycle
- |> Ash.Query.filter(member_id == ^member.id)
- |> Ash.read!(actor: system_actor)
-
- assert remaining == [],
- "Expected all cycles to be deleted (handler enforces policy via Ash.destroy)"
- end
- end
end
diff --git a/test/mv_web/plugs/check_page_permission_test.exs b/test/mv_web/plugs/check_page_permission_test.exs
index 2e33474..4b2217c 100644
--- a/test/mv_web/plugs/check_page_permission_test.exs
+++ b/test/mv_web/plugs/check_page_permission_test.exs
@@ -742,18 +742,6 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
assert conn.status == 200
end
- @tag role: :normal_user
- test "GET /groups/new returns 200", %{conn: conn} do
- conn = get(conn, "/groups/new")
- assert conn.status == 200
- end
-
- @tag role: :normal_user
- test "GET /groups/:slug/edit returns 200", %{conn: conn, group_slug: slug} do
- conn = get(conn, "/groups/#{slug}/edit")
- assert conn.status == 200
- end
-
@tag role: :normal_user
test "GET /members/:id/show/edit returns 200", %{conn: conn, member_id: id} do
conn = get(conn, "/members/#{id}/show/edit")
@@ -842,6 +830,22 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
assert redirected_to(conn) == "/users/#{user.id}"
end
+ @tag role: :normal_user
+ test "GET /groups/new redirects to user profile", %{conn: conn, current_user: user} do
+ conn = get(conn, "/groups/new")
+ assert redirected_to(conn) == "/users/#{user.id}"
+ end
+
+ @tag role: :normal_user
+ test "GET /groups/:slug/edit redirects to user profile", %{
+ conn: conn,
+ current_user: user,
+ group_slug: slug
+ } do
+ conn = get(conn, "/groups/#{slug}/edit")
+ assert redirected_to(conn) == "/users/#{user.id}"
+ end
+
@tag role: :normal_user
test "GET /admin/roles redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/admin/roles")
diff --git a/test/mv_web/user_live/form_test.exs b/test/mv_web/user_live/form_test.exs
index a22c230..48c0238 100644
--- a/test/mv_web/user_live/form_test.exs
+++ b/test/mv_web/user_live/form_test.exs
@@ -213,35 +213,6 @@ defmodule MvWeb.UserLive.FormTest do
assert not is_nil(updated_user.hashed_password)
assert updated_user.hashed_password != ""
end
-
- test "admin can change user role and change persists", %{conn: conn} do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- role_a = Mv.Fixtures.role_fixture("normal_user")
- role_b = Mv.Fixtures.role_fixture("read_only")
-
- user = create_test_user(%{email: "rolechange@example.com"})
- {:ok, user} = Mv.Accounts.update_user(user, %{role_id: role_a.id}, actor: system_actor)
- assert user.role_id == role_a.id
-
- {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
-
- view
- |> form("#user-form",
- user: %{
- email: "rolechange@example.com",
- role_id: role_b.id
- }
- )
- |> render_submit()
-
- assert_redirected(view, "/users")
-
- updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor)
-
- assert updated_user.role_id == role_b.id,
- "Expected role_id to persist as #{role_b.id}, got #{inspect(updated_user.role_id)}"
- end
end
describe "edit user form - validation" do
diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs
index 11cd70b..cf1cc80 100644
--- a/test/mv_web/user_live/index_test.exs
+++ b/test/mv_web/user_live/index_test.exs
@@ -55,6 +55,7 @@ defmodule MvWeb.UserLive.IndexTest do
# Should show ascending indicator (up arrow)
assert html =~ "hero-chevron-up"
+ assert html =~ ~s(aria-sort="ascending")
# Test actual sort order: alpha should appear before mike, mike before zulu
alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0)
@@ -75,6 +76,7 @@ defmodule MvWeb.UserLive.IndexTest do
# Should now show descending indicator (down arrow)
assert html =~ "hero-chevron-down"
+ assert html =~ ~s(aria-sort="descending")
# Test actual sort order reversed: zulu should now appear before mike, mike before alpha
alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0)
@@ -105,6 +107,7 @@ defmodule MvWeb.UserLive.IndexTest do
# Click again to toggle back to ascending
html = view |> element("button[phx-value-field='email']") |> render_click()
assert html =~ "hero-chevron-up"
+ assert html =~ ~s(aria-sort="ascending")
# Should be back to original ascending order
alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0)
@@ -376,45 +379,6 @@ defmodule MvWeb.UserLive.IndexTest do
end
end
- describe "Password column display" do
- test "user without password shows em dash in Password column", %{conn: conn} do
- # User created with hashed_password: nil (no password) - must not get default password
- user_no_pw =
- create_test_user(%{
- email: "no-password@example.com",
- hashed_password: nil
- })
-
- conn = conn_with_oidc_user(conn)
- {:ok, view, html} = live(conn, "/users")
-
- assert html =~ "no-password@example.com"
-
- # Password column must show "—" (em dash) for user without password, not "Enabled"
- row = view |> element("tr#row-#{user_no_pw.id}") |> render()
- assert row =~ "—", "Password column should show em dash for user without password"
-
- refute row =~ "Enabled",
- "Password column must not show Enabled when user has no password"
- end
-
- test "user with password shows Enabled in Password column", %{conn: conn} do
- user_with_pw =
- create_test_user(%{
- email: "with-password@example.com",
- password: "test123"
- })
-
- conn = conn_with_oidc_user(conn)
- {:ok, view, html} = live(conn, "/users")
-
- assert html =~ "with-password@example.com"
-
- row = view |> element("tr#row-#{user_with_pw.id}") |> render()
- assert row =~ "Enabled", "Password column should show Enabled when user has password"
- end
- end
-
describe "member linking display" do
@tag :slow
test "displays linked member name in user list", %{conn: conn} do