defmodule Mv.Helpers.SystemActorTest do @moduledoc """ Tests for the SystemActor helper module. """ use Mv.DataCase, async: false alias Mv.Helpers.SystemActor alias Mv.Authorization alias Mv.Accounts require Ash.Query # Helper function to ensure admin role exists defp ensure_admin_role do case Authorization.list_roles() do {:ok, roles} -> case Enum.find(roles, &(&1.permission_set_name == "admin")) do nil -> {:ok, role} = Authorization.create_role(%{ name: "Admin", description: "Administrator with full access", permission_set_name: "admin" }) role role -> role end _ -> {:ok, role} = Authorization.create_role(%{ name: "Admin", description: "Administrator with full access", permission_set_name: "admin" }) role end end # Helper function to ensure system user exists with admin role defp ensure_system_user(admin_role) do # Use authorize?: false for bootstrap operations case Accounts.User |> Ash.Query.filter(email == ^"system@mila.local") |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do {:ok, user} when not is_nil(user) -> user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.update!(authorize?: false) |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) _ -> Accounts.create_user!(%{email: "system@mila.local"}, upsert?: true, upsert_identity: :unique_email, authorize?: false ) |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.update!(authorize?: false) |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) end end # Helper function to ensure admin user exists with admin role defp ensure_admin_user(admin_role) do # Use authorize?: false for bootstrap operations admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" case Accounts.User |> Ash.Query.filter(email == ^admin_email) |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do {:ok, user} when not is_nil(user) -> user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.update!(authorize?: false) |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) _ -> Accounts.create_user!(%{email: admin_email}, upsert?: true, upsert_identity: :unique_email, authorize?: false ) |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.update!(authorize?: false) |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) end end setup do admin_role = ensure_admin_role() system_user = ensure_system_user(admin_role) admin_user = ensure_admin_user(admin_role) # Invalidate cache to ensure fresh load SystemActor.invalidate_cache() %{admin_role: admin_role, system_user: system_user, admin_user: admin_user} end describe "get_system_actor/0" do test "returns system user with admin role", %{system_user: _system_user} do system_actor = SystemActor.get_system_actor() assert %Mv.Accounts.User{} = system_actor assert to_string(system_actor.email) == "system@mila.local" assert system_actor.role != nil assert system_actor.role.permission_set_name == "admin" end test "falls back to admin user if system user doesn't exist", %{admin_user: _admin_user} do # Delete system user if it exists system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end # Invalidate cache to force reload SystemActor.invalidate_cache() # Should fall back to admin user system_actor = SystemActor.get_system_actor() assert %Mv.Accounts.User{} = system_actor assert system_actor.role != nil assert system_actor.role.permission_set_name == "admin" # Should be admin user, not system user assert to_string(system_actor.email) != "system@mila.local" end test "caches system actor for performance" do # First call actor1 = SystemActor.get_system_actor() # Second call should return cached actor (same struct) actor2 = SystemActor.get_system_actor() # Should be the same struct (cached) assert actor1.id == actor2.id end test "creates system user in test environment if none exists", %{admin_role: _admin_role} do # In test environment, system actor should auto-create if missing # Delete all users to test auto-creation system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end # Invalidate cache SystemActor.invalidate_cache() # Should auto-create system user in test environment system_actor = SystemActor.get_system_actor() assert %Mv.Accounts.User{} = system_actor assert to_string(system_actor.email) == "system@mila.local" assert system_actor.role != nil assert system_actor.role.permission_set_name == "admin" end end describe "invalidate_cache/0" do test "forces reload of system actor on next call" do # Get initial actor actor1 = SystemActor.get_system_actor() # Invalidate cache :ok = SystemActor.invalidate_cache() # Next call should reload (but should be same user) actor2 = SystemActor.get_system_actor() # Should be same user (same ID) assert actor1.id == actor2.id end end describe "get_system_actor_result/0" do test "returns ok tuple with system actor" do assert {:ok, actor} = SystemActor.get_system_actor_result() assert %Mv.Accounts.User{} = actor assert actor.role.permission_set_name == "admin" end test "returns error tuple when system actor cannot be loaded" do # Delete all users to force error system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end SystemActor.invalidate_cache() # In test environment, it should auto-create, so this should succeed # But if it fails, we should get an error tuple result = SystemActor.get_system_actor_result() # Should either succeed (auto-created) or return error assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "system_user_email/0" do test "returns the system user email address" do assert SystemActor.system_user_email() == "system@mila.local" end end describe "edge cases" do test "raises error if admin user has invalid role (role loading fails)", %{ admin_user: admin_user } do # Delete system user to force fallback to admin user system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end # Test that NOT NULL + FK constraints prevent setting role_id to NULL # We verify this by attempting to set role_id to NULL and expecting a constraint violation admin_user_id = Ecto.UUID.cast!(admin_user.id) admin_user_id_binary = Ecto.UUID.dump!(admin_user_id) # Attempting to set role_id to NULL should fail due to NOT NULL constraint assert_raise Postgrex.Error, ~r/null value in column.*role_id.*violates not-null constraint/i, fn -> Ecto.Adapters.SQL.query!( Mv.Repo, """ UPDATE users SET role_id = NULL WHERE id = $1::uuid """, [admin_user_id_binary] ) end # Note: With NOT NULL + FK constraints, we can't test the "no role" case directly # because the database prevents it. This is the desired behavior - the constraints # guarantee that role_id is always valid. end test "handles concurrent calls without race conditions" do # Delete system user and admin user to force creation system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" system_actor = SystemActor.get_system_actor() case Accounts.User |> 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) _ -> :ok end SystemActor.invalidate_cache() # Call get_system_actor concurrently from multiple processes tasks = for _ <- 1..10 do Task.async(fn -> SystemActor.get_system_actor() end) end results = Task.await_many(tasks, :infinity) # All should succeed and return the same actor assert length(results) == 10 assert Enum.all?(results, &(&1.role.permission_set_name == "admin")) assert Enum.all?(results, fn actor -> to_string(actor.email) == "system@mila.local" end) # All should have the same ID (same user) ids = Enum.map(results, & &1.id) assert Enum.uniq(ids) |> length() == 1 end test "raises error if system user has wrong role", %{system_user: system_user} do # Create a non-admin role (using read_only as it's a valid permission set) {:ok, read_only_role} = Authorization.create_role(%{ name: "Read Only Role", description: "Read-only access", permission_set_name: "read_only" }) system_actor = SystemActor.get_system_actor() # Assign wrong role to system user system_user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, read_only_role, type: :append_and_remove) |> Ash.update!(actor: system_actor) SystemActor.invalidate_cache() # Should raise error because system user doesn't have admin role assert_raise RuntimeError, ~r/System actor must have admin role/, fn -> SystemActor.get_system_actor() end end test "raises error if system user has invalid role (role loading fails)", %{ system_user: system_user } do # Test that NOT NULL + FK constraints prevent setting role_id to NULL # We verify this by attempting to set role_id to NULL and expecting a constraint violation system_user_id = Ecto.UUID.cast!(system_user.id) system_user_id_binary = Ecto.UUID.dump!(system_user_id) # Attempting to set role_id to NULL should fail due to NOT NULL constraint assert_raise Postgrex.Error, ~r/null value in column.*role_id.*violates not-null constraint/i, fn -> Ecto.Adapters.SQL.query!( Mv.Repo, """ UPDATE users SET role_id = NULL WHERE id = $1::uuid """, [system_user_id_binary] ) end # Note: With NOT NULL + FK constraints, we can't test the "no role" case directly # because the database prevents it. This is the desired behavior - the constraints # guarantee that role_id is always valid. end end end