defmodule Mv.Membership.GroupTest do @moduledoc """ Tests for Group resource validations, CRUD operations, and relationships. """ 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 "Validations - Name & Description" do @valid_attrs %{ name: "Test Group", description: "Test description" } test "create group with valid attributes", %{actor: actor} do assert {:ok, group} = Membership.create_group(@valid_attrs, actor: actor) assert group.name == "Test Group" assert group.description == "Test description" assert group.slug != nil end test "create group with name only (description nil)", %{actor: actor} do attrs = Map.delete(@valid_attrs, :description) assert {:ok, group} = Membership.create_group(attrs, actor: actor) assert group.name == "Test Group" assert group.description == nil end test "return error when name is missing", %{actor: actor} do attrs = Map.delete(@valid_attrs, :name) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(attrs, actor: actor) assert error_message(errors, :name) =~ "must be present" end test "return error when name exceeds 100 characters", %{actor: actor} do long_name = String.duplicate("a", 101) attrs = Map.put(@valid_attrs, :name, long_name) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(attrs, actor: actor) assert error_message(errors, :name) =~ "must be at most 100" end test "return error when name is not unique (case-insensitive) - application level validation", %{ actor: actor } do {:ok, _group1} = Membership.create_group(@valid_attrs, actor: actor) # Try to create with same name, different case # This tests application-level validation (Ash validations) attrs2 = Map.put(@valid_attrs, :name, "TEST GROUP") assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(attrs2, actor: actor) error_msg = error_message(errors, :name) assert error_msg =~ "already been taken" || error_msg =~ "already exists" end test "description max length is 500 characters", %{actor: actor} do long_description = String.duplicate("a", 501) attrs = Map.put(@valid_attrs, :description, long_description) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(attrs, actor: actor) assert error_message(errors, :description) =~ "must be at most 500" end end describe "Slug Generation & Validation" do test "slug is automatically generated from name on create", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Test Group Name"}, actor: actor) assert group.slug == "test-group-name" end test "slug is unique (prevents duplicate slugs from different names)", %{actor: actor} do {:ok, _group1} = Membership.create_group(%{name: "Test!!!"}, actor: actor) # Second group with name that generates same slug should fail assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(%{name: "Test???"}, 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")) end) end test "slug is immutable (doesn't change when name is updated)", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Original Name"}, actor: actor) original_slug = group.slug assert original_slug == "original-name" {:ok, updated_group} = Membership.update_group(group, %{name: "New Different Name"}, actor: actor) assert updated_group.slug == original_slug assert updated_group.name == "New Different Name" end test "slug cannot be empty (rejects name with only special characters)", %{actor: actor} do assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_group(%{name: "!!!"}, actor: actor) assert Enum.any?(errors, fn err -> (err.field == :slug or err.field == :name) and (String.contains?(err.message, "cannot be empty") or String.contains?(err.message, "is required")) end) end end describe "CRUD Operations" do test "create group with name and description", %{actor: actor} do attrs = %{name: "New Group", description: "Description"} assert {:ok, group} = Membership.create_group(attrs, actor: actor) assert group.name == "New Group" assert group.description == "Description" end test "update group name (slug remains unchanged)", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Original"}, actor: actor) original_slug = group.slug {:ok, updated} = Membership.update_group(group, %{name: "Updated"}, actor: actor) assert updated.name == "Updated" assert updated.slug == original_slug end test "update group description", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Test", description: "Old"}, actor: actor) {:ok, updated} = Membership.update_group(group, %{description: "New Description"}, actor: actor) assert updated.description == "New Description" end test "prevent duplicate name on update (case-insensitive)", %{actor: actor} do {:ok, _group1} = Membership.create_group(%{name: "Group One"}, actor: actor) {:ok, group2} = Membership.create_group(%{name: "Group Two"}, actor: actor) # Try to update group2 with group1's name (different case) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.update_group(group2, %{name: "GROUP ONE"}, actor: actor) error_msg = error_message(errors, :name) assert error_msg =~ "already been taken" || error_msg =~ "already exists" end end describe "Calculations" do test "member count calculation returns 0 for empty group", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Empty Group"}, actor: actor) # Load with calculation {:ok, group_with_count} = Ash.load(group, :member_count, actor: actor, domain: Mv.Membership) assert group_with_count.member_count == 0 end test "member count calculation returns correct count when members added/removed", %{ actor: actor } do {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) {:ok, member1} = Membership.create_member(%{email: "member1@test.com"}, actor: actor) {:ok, member2} = Membership.create_member(%{email: "member2@test.com"}, actor: actor) # Add members to group {: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 ) # Check count {:ok, group_with_count} = Ash.load(group, :member_count, actor: actor, domain: Mv.Membership) assert group_with_count.member_count == 2 # Remove one member {:ok, mg_to_delete} = Ash.read_one( Mv.Membership.MemberGroup |> Ash.Query.filter(expr(member_id == ^member1.id and group_id == ^group.id)), actor: actor, domain: Mv.Membership ) :ok = Membership.destroy_member_group(mg_to_delete, actor: actor) # Check count again {:ok, group_with_count_updated} = Ash.load(group, :member_count, actor: actor, domain: Mv.Membership) assert group_with_count_updated.member_count == 1 end end describe "Relationships & Deletion" do test "group has many_to_many members relationship (load with preloading)", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, _mg} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) # Load group with members {:ok, group_with_members} = Ash.load(group, :members, actor: actor, domain: Mv.Membership) assert length(group_with_members.members) == 1 assert hd(group_with_members.members).id == member.id end test "delete group cascades to member_groups (members remain intact)", %{actor: actor} do {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor) {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor) {:ok, _mg} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: actor ) # Delete group :ok = Membership.destroy_group(group, actor: actor) # Member should still exist {:ok, member_reloaded} = Ash.get(Mv.Membership.Member, member.id, actor: actor) assert member_reloaded != nil # MemberGroup should be deleted {:ok, mgs} = Ash.read( Mv.Membership.MemberGroup |> Ash.Query.filter(expr(group_id == ^group.id)), actor: actor, domain: Mv.Membership ) assert mgs == [] end end # Helper function for error evaluation # Returns the error message for a given field, or empty string if not found defp error_message(errors, field) do case Enum.find(errors, fn err -> Map.get(err, :field) == field end) do nil -> "" err -> Map.get(err, :message, "") end end end