defmodule Mv.Membership.MemberGroupsRelationshipTest do @moduledoc """ Tests for Member resource extension with groups relationship. """ use Mv.DataCase, async: true alias Mv.Membership require Ash.Query import Ash.Expr setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end describe "Relationships" do test "member has many_to_many groups relationship (load with preloading)", %{actor: actor} do {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, group1} = Membership.create_group(%{name: "Group One"}, actor: actor) {:ok, group2} = Membership.create_group(%{name: "Group Two"}, actor: actor) {:ok, _mg1} = Membership.create_member_group(%{member_id: member.id, group_id: group1.id}, actor: actor ) {:ok, _mg2} = Membership.create_member_group(%{member_id: member.id, group_id: group2.id}, actor: actor ) # Load member with groups {:ok, member_with_groups} = Ash.load(member, :groups, actor: actor, domain: Mv.Membership) assert length(member_with_groups.groups) == 2 assert Enum.any?(member_with_groups.groups, &(&1.id == group1.id)) assert Enum.any?(member_with_groups.groups, &(&1.id == group2.id)) end test "load multiple members with groups preloaded (N+1 prevention)", %{actor: actor} do {:ok, member1} = Membership.create_member(%{email: "member1@test.com"}, actor: actor) {:ok, member2} = Membership.create_member(%{email: "member2@test.com"}, actor: actor) {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) {:ok, _mg1} = Membership.create_member_group(%{member_id: member1.id, group_id: group.id}, actor: actor ) {:ok, _mg2} = Membership.create_member_group(%{member_id: member2.id, group_id: group.id}, actor: actor ) # Load all members with groups in single query {:ok, members} = Ash.read(Mv.Membership.Member, actor: actor, domain: Mv.Membership, load: [:groups]) member1_loaded = Enum.find(members, &(&1.id == member1.id)) member2_loaded = Enum.find(members, &(&1.id == member2.id)) assert length(member1_loaded.groups) == 1 assert length(member2_loaded.groups) == 1 assert hd(member1_loaded.groups).id == group.id assert hd(member2_loaded.groups).id == group.id end end describe "Member-Group Association Operations" do test "add member to group via Ash API", %{actor: actor} do {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) assert {:ok, member_group} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) assert member_group.member_id == member.id assert member_group.group_id == group.id end test "remove member from group via Ash API", %{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 ) # Remove association :ok = Membership.destroy_member_group(member_group, actor: actor) # Verify association is removed {:ok, mgs} = Ash.read( Mv.Membership.MemberGroup |> Ash.Query.filter(expr(member_id == ^member.id and group_id == ^group.id)), actor: actor, domain: Mv.Membership ) assert mgs == [] end test "add member to multiple groups in single operation", %{actor: actor} do {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, group1} = Membership.create_group(%{name: "Group One"}, actor: actor) {:ok, group2} = Membership.create_group(%{name: "Group Two"}, actor: actor) {:ok, group3} = Membership.create_group(%{name: "Group Three"}, actor: actor) # Add to all groups {:ok, _mg1} = Membership.create_member_group(%{member_id: member.id, group_id: group1.id}, actor: actor ) {:ok, _mg2} = Membership.create_member_group(%{member_id: member.id, group_id: group2.id}, actor: actor ) {:ok, _mg3} = Membership.create_member_group(%{member_id: member.id, group_id: group3.id}, actor: actor ) # Verify all associations exist {:ok, member_with_groups} = Ash.load(member, :groups, actor: actor, domain: Mv.Membership) assert length(member_with_groups.groups) == 3 end end describe "Edge Cases" do test "adding member to same group twice fails (duplicate prevention)", %{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, _mg1} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) # Try to add again assert {:error, %Ash.Error.Invalid{}} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) end test "removing member from group they're not in (idempotent, no error)", %{actor: actor} do {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) # Verify no association exists {:ok, nil} = Ash.read_one( Mv.Membership.MemberGroup |> Ash.Query.filter(expr(member_id == ^member.id and group_id == ^group.id)), actor: actor, domain: Mv.Membership ) # Test idempotency: Create association, delete it, then try to delete again # This verifies that destroy_member_group is idempotent {:ok, member_group} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) # First deletion should succeed assert :ok = Membership.destroy_member_group(member_group, actor: actor) # Verify association is deleted {:ok, nil} = Ash.read_one( Mv.Membership.MemberGroup |> Ash.Query.filter(expr(id == ^member_group.id)), actor: actor, domain: Mv.Membership ) # Try to destroy again - should be idempotent (either succeed or return not found error) # Note: This tests the idempotency of the destroy action result = Membership.destroy_member_group(member_group, actor: actor) # Should either succeed (idempotent) or return an error (not found) # Both behaviors are acceptable for idempotency assert result == :ok || match?({:error, _}, result) end end end