defmodule Mv.Membership.GroupDatabaseConstraintsTest do @moduledoc """ Tests for database-level constraints (unique, foreign keys, CASCADE). These tests verify that constraints are enforced at the database level, not just application level. """ use Mv.DataCase, async: false alias Mv.Membership require Ash.Query import Ash.Expr setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end describe "Unique Constraints" do test "database enforces unique name constraint (case-insensitive via LOWER)", %{actor: actor} do {:ok, _group1} = Membership.create_group(%{name: "Test Group"}, actor: actor) # Try to create with same name, different case - should fail at DB level assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(%{name: "TEST GROUP"}, actor: actor) assert Enum.any?(errors, fn err -> err.field == :name and (String.contains?(err.message, "already been taken") or String.contains?(err.message, "already exists") or String.contains?(err.message, "duplicate")) end) end test "database enforces unique slug constraint (case-sensitive)", %{actor: actor} do {:ok, _group1} = Membership.create_group(%{name: "Test Group"}, actor: actor) # Try to create with name that generates same slug - should fail at DB level assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(%{name: "test-group"}, actor: actor) assert Enum.any?(errors, fn err -> (err.field == :slug or err.field == :name) and (String.contains?(err.message, "already been taken") or String.contains?(err.message, "already exists") or String.contains?(err.message, "duplicate")) end) end end describe "Foreign Key Constraints" do test "cannot create MemberGroup with non-existent member_id", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) fake_member_id = Ash.UUID.generate() assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member_group(%{member_id: fake_member_id, group_id: group.id}, actor: actor ) assert Enum.any?(errors, fn err -> (err.field == :member_id or err.field == :member) and (String.contains?(err.message, "does not exist") or String.contains?(err.message, "not found") or String.contains?(err.message, "foreign key")) end) end test "cannot create MemberGroup with non-existent group_id", %{actor: actor} do {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) fake_group_id = Ash.UUID.generate() assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member_group(%{member_id: member.id, group_id: fake_group_id}, actor: actor ) assert Enum.any?(errors, fn err -> (err.field == :group_id or err.field == :group) and (String.contains?(err.message, "does not exist") or String.contains?(err.message, "not found") or String.contains?(err.message, "foreign key")) end) end end describe "CASCADE Delete Constraints" do test "deleting member cascades to member_groups (verified at DB level)", %{actor: actor} do {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) {:ok, member_group} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) # Verify association exists assert member_group.member_id == member.id # Delete member :ok = Membership.destroy_member(member, actor: actor) # Verify MemberGroup is deleted at DB level (CASCADE) {:ok, mgs} = Ash.read( Mv.Membership.MemberGroup |> Ash.Query.filter(expr(id == ^member_group.id)), actor: actor, domain: Mv.Membership ) assert mgs == [] end test "deleting group cascades to member_groups (verified at DB level)", %{actor: actor} do {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) {:ok, member_group} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) # Verify association exists assert member_group.group_id == group.id # Delete group :ok = Membership.destroy_group(group, actor: actor) # Verify MemberGroup is deleted at DB level (CASCADE) {:ok, mgs} = Ash.read( Mv.Membership.MemberGroup |> Ash.Query.filter(expr(id == ^member_group.id)), actor: actor, domain: Mv.Membership ) assert mgs == [] end end end