This guide explains how to use Casbin-Ex with Ecto.Adapters.SQL.Sandbox when you need to wrap Casbin operations in database transactions.
When using Ecto.Adapters.SQL.Sandbox in checkout mode (the default), database connections are restricted to the process that checked them out. The Casbin EnforcerServer runs in a separate process, which causes issues when:
- Your test wraps Casbin operations in a
Repo.transaction - The transaction locks the connection to the test process
- The
EnforcerServerprocess cannot access the locked connection, even withSandbox.allow/3
This results in the error:
** (DBConnection.ConnectionError) could not checkout the connection owned by #PID<...>
The recommended solution is to use Ecto's shared mode for tests that need to call Casbin within transactions:
defmodule MyApp.RolesTest do
use MyApp.DataCase
use MyApp.CasbinCase
setup do
# Check out a connection
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
# Enable shared mode - this allows the EnforcerServer to use the connection
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
# Allow the EnforcerServer process to access the connection
case Registry.lookup(Casbin.EnforcerRegistry, "my_enforcer") do
[{enforcer_pid, _}] ->
Ecto.Adapters.SQL.Sandbox.allow(MyApp.Repo, self(), enforcer_pid)
[] ->
:ok
end
:ok
end
test "create role with permissions in transaction" do
permissions = [
%{resource: "orgs", action: "read"},
%{resource: "users", action: "read"}
]
# This now works because of shared mode
assert {:ok, :created} =
MyApp.Repo.transaction(fn ->
Enum.each(permissions, fn %{resource: resource, action: action} ->
case Casbin.EnforcerServer.add_policy("my_enforcer", {:p, ["analyst", resource, action]}) do
:ok -> :ok
{:error, reason} -> MyApp.Repo.rollback(reason)
end
end)
{:ok, :created}
end)
end
end- Shared mode allows multiple processes to access the same database connection
- All tests in a module using shared mode will share the connection
- This may reduce test isolation compared to the default checkout mode
- It's still safe because each test gets a clean transaction that's rolled back
Use shared mode when you need to:
- Wrap Casbin operations in application-level transactions
- Test rollback behavior with Casbin
- Test atomic operations that involve both Casbin and other database changes
If you don't need transactions in your tests, you can:
- Avoid wrapping in transactions: Call Casbin operations directly without
Repo.transaction - Test transactions separately: Test transaction logic separately from Casbin operations
- Use async: false: Use
async: falseto run tests serially with shared connections
Here's a complete example of a test module using shared mode:
defmodule MyApp.Authorization.RolesTest do
use MyApp.DataCase, async: false # async: false for shared mode
alias MyApp.Repo
alias MyApp.Authorization.Roles
setup do
# Set up sandbox in shared mode
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
# Allow EnforcerServer to access the connection
case Registry.lookup(Casbin.EnforcerRegistry, "my_enforcer") do
[{pid, _}] -> Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
[] -> :ok
end
:ok
end
test "creates role with permissions atomically" do
permissions = [
%{resource: "users", action: "read"},
%{resource: "users", action: "write"}
]
# This transaction includes Casbin operations
result = Repo.transaction(fn ->
# Create database record
{:ok, role} = Roles.create_role_record("admin")
# Add Casbin policies
Enum.each(permissions, fn perm ->
case Casbin.EnforcerServer.add_policy(
"my_enforcer",
{:p, ["admin", perm.resource, perm.action]}
) do
:ok -> :ok
{:error, reason} -> Repo.rollback(reason)
end
end)
{:ok, role}
end)
assert {:ok, _role} = result
end
test "rolls back Casbin operations on error" do
result = Repo.transaction(fn ->
# Add a policy
:ok = Casbin.EnforcerServer.add_policy(
"my_enforcer",
{:p, ["temp_role", "resource", "action"]}
)
# Simulate an error
Repo.rollback(:simulated_error)
end)
assert {:error, :simulated_error} = result
# Verify the policy was not persisted
policies = Casbin.EnforcerServer.list_policies("my_enforcer", %{sub: "temp_role"})
assert policies == []
end
end