defmodule Mv.Membership.MemberGroupPoliciesTest do @moduledoc """ Tests for MemberGroup resource authorization policies. Verifies own_data can only read linked member's associations; read_only can read all, cannot create/destroy; normal_user and admin can read, create, destroy. """ use Mv.DataCase, async: false alias Mv.Membership alias Mv.Accounts alias Mv.Authorization require Ash.Query setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end defp create_role_with_permission_set(permission_set_name, actor) do role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}" case Authorization.create_role( %{ name: role_name, description: "Test role for #{permission_set_name}", permission_set_name: permission_set_name }, actor: actor ) do {:ok, role} -> role {:error, error} -> raise "Failed to create role: #{inspect(error)}" end end defp create_user_with_permission_set(permission_set_name, actor) do role = create_role_with_permission_set(permission_set_name, actor) {:ok, user} = Accounts.User |> Ash.Changeset.for_create(:register_with_password, %{ email: "user#{System.unique_integer([:positive])}@example.com", password: "testpassword123" }) |> Ash.create(actor: actor) {:ok, user} = user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove) |> Ash.update(actor: actor) {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor) user_with_role end defp create_admin_user(actor) do create_user_with_permission_set("admin", actor) end defp create_member_fixture(actor) do admin = create_admin_user(actor) {:ok, member} = Membership.create_member( %{ first_name: "Test", last_name: "Member", email: "test#{System.unique_integer([:positive])}@example.com" }, actor: admin ) member end defp create_group_fixture(actor) do admin = create_admin_user(actor) {:ok, group} = Membership.create_group( %{name: "Test Group #{System.unique_integer([:positive])}", description: "Test"}, actor: admin ) group end defp create_member_group_fixture(member_id, group_id, actor) do admin = create_admin_user(actor) {:ok, member_group} = Membership.create_member_group(%{member_id: member_id, group_id: group_id}, actor: admin) member_group end describe "own_data permission set" do setup %{actor: actor} do user = create_user_with_permission_set("own_data", actor) member = create_member_fixture(actor) group = create_group_fixture(actor) # Link user to member so actor.member_id is set admin = create_admin_user(actor) user = user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.force_change_attribute(:member_id, member.id) |> Ash.update(actor: admin) {:ok, user} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor) mg_linked = create_member_group_fixture(member.id, group.id, actor) # MemberGroup for another member (not linked to user) other_member = create_member_fixture(actor) other_group = create_group_fixture(actor) mg_other = create_member_group_fixture(other_member.id, other_group.id, actor) %{user: user, member: member, group: group, mg_linked: mg_linked, mg_other: mg_other} end test "can read member_groups for linked member only", %{user: user, mg_linked: mg_linked} do {:ok, list} = Mv.Membership.MemberGroup |> Ash.read(actor: user, domain: Mv.Membership) ids = Enum.map(list, & &1.id) assert mg_linked.id in ids refute Enum.empty?(list) end test "list returns only member_groups where member_id == actor.member_id", %{ user: user, mg_linked: mg_linked, mg_other: mg_other } do {:ok, list} = Mv.Membership.MemberGroup |> Ash.read(actor: user, domain: Mv.Membership) ids = Enum.map(list, & &1.id) assert mg_linked.id in ids refute mg_other.id in ids end test "cannot create member_group (returns forbidden)", %{user: user, actor: actor} do # Use fresh member/group so we assert on Forbidden, not on duplicate validation other_member = create_member_fixture(actor) other_group = create_group_fixture(actor) assert {:error, %Ash.Error.Forbidden{}} = Membership.create_member_group( %{member_id: other_member.id, group_id: other_group.id}, actor: user ) end test "cannot destroy member_group (returns forbidden)", %{user: user, mg_linked: mg_linked} do assert {:error, %Ash.Error.Forbidden{}} = Membership.destroy_member_group(mg_linked, actor: user) end end describe "read_only permission set" do setup %{actor: actor} do user = create_user_with_permission_set("read_only", actor) member = create_member_fixture(actor) group = create_group_fixture(actor) mg = create_member_group_fixture(member.id, group.id, actor) %{actor: actor, user: user, member: member, group: group, mg: mg} end test "can read all member_groups", %{user: user, mg: mg} do {:ok, list} = Mv.Membership.MemberGroup |> Ash.read(actor: user, domain: Mv.Membership) ids = Enum.map(list, & &1.id) assert mg.id in ids end test "cannot create member_group (returns forbidden)", %{user: user, actor: actor} do member = create_member_fixture(actor) group = create_group_fixture(actor) assert {:error, %Ash.Error.Forbidden{}} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: user ) end test "cannot destroy member_group (returns forbidden)", %{user: user, mg: mg} do assert {:error, %Ash.Error.Forbidden{}} = Membership.destroy_member_group(mg, actor: user) end end describe "normal_user permission set" do setup %{actor: actor} do user = create_user_with_permission_set("normal_user", actor) member = create_member_fixture(actor) group = create_group_fixture(actor) mg = create_member_group_fixture(member.id, group.id, actor) %{actor: actor, user: user, member: member, group: group, mg: mg} end test "can read all member_groups", %{user: user, mg: mg} do {:ok, list} = Mv.Membership.MemberGroup |> Ash.read(actor: user, domain: Mv.Membership) ids = Enum.map(list, & &1.id) assert mg.id in ids end test "can create member_group", %{user: user, actor: actor} do member = create_member_fixture(actor) group = create_group_fixture(actor) assert {:ok, _mg} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: user ) end test "can destroy member_group", %{user: user, mg: mg} do assert :ok = Membership.destroy_member_group(mg, actor: user) end end describe "admin permission set" do setup %{actor: actor} do user = create_user_with_permission_set("admin", actor) member = create_member_fixture(actor) group = create_group_fixture(actor) mg = create_member_group_fixture(member.id, group.id, actor) %{actor: actor, user: user, member: member, group: group, mg: mg} end test "can read all member_groups", %{user: user, mg: mg} do {:ok, list} = Mv.Membership.MemberGroup |> Ash.read(actor: user, domain: Mv.Membership) ids = Enum.map(list, & &1.id) assert mg.id in ids end test "can create member_group", %{user: user, actor: actor} do member = create_member_fixture(actor) group = create_group_fixture(actor) assert {:ok, _mg} = Membership.create_member_group(%{member_id: member.id, group_id: group.id}, actor: user ) end test "can destroy member_group", %{user: user, mg: mg} do assert :ok = Membership.destroy_member_group(mg, actor: user) end end end