Add NOT NULL constraint to users.role_id and optimize default_role_id
All checks were successful
continuous-integration/drone/push Build is passing

- Add database-level NOT NULL constraint for users.role_id
- Update SystemActor tests to verify NOT NULL constraint enforcement
- Add process dictionary caching for default_role_id/0 to reduce DB queries
This commit is contained in:
Moritz 2026-01-25 17:04:37 +01:00
parent 86c8b23c77
commit 2d446f63ea
Signed by: moritz
GPG key ID: 1020A035E5DD0824
6 changed files with 501 additions and 37 deletions

View file

@ -4,8 +4,6 @@ defmodule Mv.Helpers.SystemActorTest do
"""
use Mv.DataCase, async: false
import Ecto.Query
alias Mv.Helpers.SystemActor
alias Mv.Authorization
alias Mv.Accounts
@ -267,18 +265,10 @@ defmodule Mv.Helpers.SystemActorTest do
end
describe "edge cases" do
test "raises error if admin user has no role", %{admin_user: admin_user} do
# Remove role from admin user by directly setting role_id to NULL in database
# (We can't use Ash because allow_nil? false prevents setting role_id to nil)
# Convert UUID to binary format for Postgrex
admin_user_id = Ecto.UUID.cast!(admin_user.id)
Mv.Repo.update_all(
from(u in "users", where: u.id == type(^admin_user_id, :binary_id)),
set: [role_id: nil]
)
# Delete system user to force fallback
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
@ -291,12 +281,29 @@ defmodule Mv.Helpers.SystemActorTest do
:ok
end
SystemActor.invalidate_cache()
# 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)
# 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
# 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
@ -372,23 +379,32 @@ defmodule Mv.Helpers.SystemActorTest do
end
end
test "raises error if system user has no role", %{system_user: system_user} do
# Remove role from system user by directly setting role_id to NULL in database
# (We can't use Ash because allow_nil? false prevents setting role_id to nil)
# Convert UUID to binary format for Postgrex
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)
Mv.Repo.update_all(
from(u in "users", where: u.id == type(^system_user_id, :binary_id)),
set: [role_id: nil]
)
# 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
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
# 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