System Actor Mode for Systemic Flows closes #348 #361
1 changed files with 233 additions and 68 deletions
|
|
@ -10,84 +10,91 @@ defmodule Mv.Helpers.SystemActorTest do
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
setup do
|
# Helper function to ensure admin role exists
|
||||||
# Ensure admin role exists
|
defp ensure_admin_role do
|
||||||
admin_role =
|
case Authorization.list_roles() do
|
||||||
case Authorization.list_roles() do
|
{:ok, roles} ->
|
||||||
{:ok, roles} ->
|
case Enum.find(roles, &(&1.permission_set_name == "admin")) do
|
||||||
case Enum.find(roles, &(&1.permission_set_name == "admin")) do
|
nil ->
|
||||||
nil ->
|
{:ok, role} =
|
||||||
{:ok, role} =
|
Authorization.create_role(%{
|
||||||
Authorization.create_role(%{
|
name: "Admin",
|
||||||
name: "Admin",
|
description: "Administrator with full access",
|
||||||
description: "Administrator with full access",
|
permission_set_name: "admin"
|
||||||
permission_set_name: "admin"
|
})
|
||||||
})
|
|
||||||
|
|
||||||
role
|
role
|
||||||
|
|
||||||
role ->
|
role ->
|
||||||
role
|
role
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
{:ok, role} =
|
{:ok, role} =
|
||||||
Authorization.create_role(%{
|
Authorization.create_role(%{
|
||||||
name: "Admin",
|
name: "Admin",
|
||||||
description: "Administrator with full access",
|
description: "Administrator with full access",
|
||||||
permission_set_name: "admin"
|
permission_set_name: "admin"
|
||||||
})
|
})
|
||||||
|
|
||||||
role
|
role
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Ensure system user exists
|
# Helper function to ensure system user exists with admin role
|
||||||
system_user =
|
defp ensure_system_user(admin_role) do
|
||||||
case Accounts.User
|
case Accounts.User
|
||||||
|> Ash.Query.filter(email == ^"system@mila.local")
|
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||||
|> Ash.read_one(domain: Mv.Accounts) do
|
|> Ash.read_one(domain: Mv.Accounts) do
|
||||||
{:ok, user} when not is_nil(user) ->
|
{:ok, user} when not is_nil(user) ->
|
||||||
user
|
user
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||||
|> Ash.update!()
|
|> Ash.update!()
|
||||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Accounts.create_user!(%{email: "system@mila.local"},
|
Accounts.create_user!(%{email: "system@mila.local"},
|
||||||
upsert?: true,
|
upsert?: true,
|
||||||
upsert_identity: :unique_email
|
upsert_identity: :unique_email
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||||
|> Ash.update!()
|
|> Ash.update!()
|
||||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Ensure admin user exists (for fallback)
|
# Helper function to ensure admin user exists with admin role
|
||||||
|
defp ensure_admin_user(admin_role) do
|
||||||
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||||
|
|
||||||
admin_user =
|
case Accounts.User
|
||||||
case Accounts.User
|
|> Ash.Query.filter(email == ^admin_email)
|
||||||
|> Ash.Query.filter(email == ^admin_email)
|
|> Ash.read_one(domain: Mv.Accounts) do
|
||||||
|> Ash.read_one(domain: Mv.Accounts) do
|
{:ok, user} when not is_nil(user) ->
|
||||||
{:ok, user} when not is_nil(user) ->
|
user
|
||||||
user
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
|> Ash.update!()
|
||||||
|> Ash.update!()
|
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Accounts.create_user!(%{email: admin_email},
|
Accounts.create_user!(%{email: admin_email},
|
||||||
upsert?: true,
|
upsert?: true,
|
||||||
upsert_identity: :unique_email
|
upsert_identity: :unique_email
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||||
|> Ash.update!()
|
|> Ash.update!()
|
||||||
|> Ash.load!(:role, domain: Mv.Accounts)
|
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||||
end
|
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
|
# Invalidate cache to ensure fresh load
|
||||||
SystemActor.invalidate_cache()
|
SystemActor.invalidate_cache()
|
||||||
|
|
@ -194,4 +201,162 @@ defmodule Mv.Helpers.SystemActorTest do
|
||||||
assert actor1.id == actor2.id
|
assert actor1.id == actor2.id
|
||||||
end
|
end
|
||||||
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
|
||||||
|
case Accounts.User
|
||||||
|
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||||
|
|> Ash.read_one(domain: Mv.Accounts) do
|
||||||
|
{:ok, user} when not is_nil(user) ->
|
||||||
|
Ash.destroy!(user, domain: Mv.Accounts)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||||
|
|
||||||
|
case Accounts.User
|
||||||
|
|> Ash.Query.filter(email == ^admin_email)
|
||||||
|
|> Ash.read_one(domain: Mv.Accounts) do
|
||||||
|
{:ok, user} when not is_nil(user) ->
|
||||||
|
Ash.destroy!(user, domain: Mv.Accounts)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
: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 no role", %{admin_user: admin_user} do
|
||||||
|
# Remove role from admin user
|
||||||
|
admin_user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:role, nil, type: :append_and_remove)
|
||||||
|
|> Ash.update!()
|
||||||
|
|
||||||
|
# Delete system user to force fallback
|
||||||
|
case Accounts.User
|
||||||
|
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||||
|
|> Ash.read_one(domain: Mv.Accounts) do
|
||||||
|
{:ok, user} when not is_nil(user) ->
|
||||||
|
Ash.destroy!(user, domain: Mv.Accounts)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
SystemActor.invalidate_cache()
|
||||||
|
|
||||||
|
# Should raise error because admin user has no role
|
||||||
|
assert_raise RuntimeError, ~r/System actor must have a role assigned/, fn ->
|
||||||
|
SystemActor.get_system_actor()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles concurrent calls without race conditions" do
|
||||||
|
# Delete system user and admin user to force creation
|
||||||
|
case Accounts.User
|
||||||
|
|> Ash.Query.filter(email == ^"system@mila.local")
|
||||||
|
|> Ash.read_one(domain: Mv.Accounts) do
|
||||||
|
{:ok, user} when not is_nil(user) ->
|
||||||
|
Ash.destroy!(user, domain: Mv.Accounts)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||||
|
|
||||||
|
case Accounts.User
|
||||||
|
|> Ash.Query.filter(email == ^admin_email)
|
||||||
|
|> Ash.read_one(domain: Mv.Accounts) do
|
||||||
|
{:ok, user} when not is_nil(user) ->
|
||||||
|
Ash.destroy!(user, domain: Mv.Accounts)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
: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"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 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!()
|
||||||
|
|
||||||
|
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 no role", %{system_user: system_user} do
|
||||||
|
# Remove role from system user
|
||||||
|
system_user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:role, nil, type: :append_and_remove)
|
||||||
|
|> Ash.update!()
|
||||||
|
|
||||||
|
SystemActor.invalidate_cache()
|
||||||
|
|
||||||
|
# Should raise error because system user has no role
|
||||||
|
assert_raise RuntimeError, ~r/System actor must have a role assigned/, fn ->
|
||||||
|
SystemActor.get_system_actor()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue