From 9c31f0c16c726313e42831c80d28aa3b0d16a686 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 27 Jan 2026 14:29:26 +0100 Subject: [PATCH] Add tests for system actor protection and hiding Index: system actor not in list, destroy returns Ash.Error.Invalid. Show/Form: redirect to /users when viewing or editing system actor user. --- test/mv/helpers/system_actor_test.exs | 24 ++++++++++++++++-------- test/mv_web/live/user_live/show_test.exs | 10 ++++++++++ test/mv_web/user_live/form_test.exs | 10 ++++++++++ test/mv_web/user_live/index_test.exs | 21 +++++++++++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/test/mv/helpers/system_actor_test.exs b/test/mv/helpers/system_actor_test.exs index af28443..e676130 100644 --- a/test/mv/helpers/system_actor_test.exs +++ b/test/mv/helpers/system_actor_test.exs @@ -10,6 +10,14 @@ defmodule Mv.Helpers.SystemActorTest do require Ash.Query + # Deletes a user row directly via SQL, bypassing Ash validations. + # Use only in tests when setting up "no system user" / "no users" scenarios; + # Ash.destroy! forbids deleting the system actor user. + defp delete_user_bypass_ash(user) do + id = Ecto.UUID.dump!(user.id) + Ecto.Adapters.SQL.query!(Mv.Repo, "DELETE FROM users WHERE id = $1", [id]) + end + # Helper function to ensure admin role exists defp ensure_admin_role do case Authorization.list_roles() do @@ -124,7 +132,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^"system@mila.local") |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok @@ -163,7 +171,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^"system@mila.local") |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok @@ -177,7 +185,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^admin_email) |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok @@ -227,7 +235,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^"system@mila.local") |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok @@ -241,7 +249,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^admin_email) |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok @@ -275,7 +283,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^"system@mila.local") |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok @@ -314,7 +322,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^"system@mila.local") |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok @@ -328,7 +336,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.Query.filter(email == ^admin_email) |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + delete_user_bypass_ash(user) _ -> :ok diff --git a/test/mv_web/live/user_live/show_test.exs b/test/mv_web/live/user_live/show_test.exs index 3551fdf..9518106 100644 --- a/test/mv_web/live/user_live/show_test.exs +++ b/test/mv_web/live/user_live/show_test.exs @@ -154,4 +154,14 @@ defmodule MvWeb.UserLive.ShowTest do assert html =~ gettext("Show User") || html =~ to_string(user.email) end end + + describe "system actor user" do + test "redirects to user list when viewing system actor user", %{conn: conn} do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + conn = conn_with_oidc_user(conn) + + assert {:error, {:live_redirect, %{to: "/users"}}} = + live(conn, ~p"/users/#{system_actor.id}") + end + end end diff --git a/test/mv_web/user_live/form_test.exs b/test/mv_web/user_live/form_test.exs index ed309fb..4b76c19 100644 --- a/test/mv_web/user_live/form_test.exs +++ b/test/mv_web/user_live/form_test.exs @@ -420,4 +420,14 @@ defmodule MvWeb.UserLive.FormTest do assert is_nil(updated_user.member) end end + + describe "system actor user" do + test "redirects to user list when editing system actor user", %{conn: conn} do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + conn = conn_with_oidc_user(conn, %{email: "admin@example.com"}) + + assert {:error, {:live_redirect, %{to: "/users"}}} = + live(conn, "/users/#{system_actor.id}/edit") + end + end end diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs index 41c198d..a1a02ea 100644 --- a/test/mv_web/user_live/index_test.exs +++ b/test/mv_web/user_live/index_test.exs @@ -405,6 +405,27 @@ defmodule MvWeb.UserLive.IndexTest do end end + describe "system actor user" do + test "does not show system actor user in list", %{conn: conn} do + # Ensure system actor exists (e.g. via get_system_actor in conn_with_oidc_user) + _system_actor = Mv.Helpers.SystemActor.get_system_actor() + system_email = Mv.Helpers.SystemActor.system_user_email() + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + refute html =~ system_email, + "System actor user (#{system_email}) must not appear in the user list" + end + + test "destroying system actor user returns error", %{current_user: current_user} do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + assert {:error, %Ash.Error.Invalid{}} = + Ash.destroy(system_actor, domain: Mv.Accounts, actor: current_user) + end + end + describe "member linking display" do test "displays linked member name in user list", %{conn: conn} do system_actor = Mv.Helpers.SystemActor.get_system_actor()