defmodule Mv.MembershipFees.MembershipFeeCyclePoliciesTest do @moduledoc """ Tests for MembershipFeeCycle resource authorization policies. Verifies read_only can only read (no update/mark_as_paid); normal_user and admin can read and update (including mark_as_paid); only admin can create and destroy. """ use Mv.DataCase, async: false alias Mv.MembershipFees alias Mv.Membership alias Mv.Accounts alias Mv.Authorization 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_fee_type_fixture(actor) do admin = create_admin_user(actor) {:ok, fee_type} = MembershipFees.create_membership_fee_type( %{ name: "Test Fee #{System.unique_integer([:positive])}", amount: Decimal.new("10.00"), interval: :yearly, description: "Test" }, actor: admin ) fee_type end defp create_cycle_fixture(actor) do admin = create_admin_user(actor) member = create_member_fixture(actor) fee_type = create_fee_type_fixture(actor) {:ok, cycle} = MembershipFees.create_membership_fee_cycle( %{ member_id: member.id, membership_fee_type_id: fee_type.id, cycle_start: Date.utc_today(), amount: Decimal.new("10.00"), status: :unpaid }, actor: admin ) cycle end describe "read_only permission set" do setup %{actor: actor} do user = create_user_with_permission_set("read_only", actor) cycle = create_cycle_fixture(actor) %{actor: actor, user: user, cycle: cycle} end test "can read membership_fee_cycles (list)", %{user: user} do {:ok, list} = Mv.MembershipFees.MembershipFeeCycle |> Ash.read(actor: user, domain: Mv.MembershipFees) assert is_list(list) end test "cannot update cycle (returns forbidden)", %{user: user, cycle: cycle} do assert {:error, %Ash.Error.Forbidden{}} = MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user) end test "cannot mark_as_paid (returns forbidden)", %{user: user, cycle: cycle} do assert {:error, %Ash.Error.Forbidden{}} = cycle |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees) |> Ash.update(actor: user, domain: Mv.MembershipFees) end test "cannot create cycle (returns forbidden)", %{user: user, actor: actor} do member = create_member_fixture(actor) fee_type = create_fee_type_fixture(actor) assert {:error, %Ash.Error.Forbidden{}} = MembershipFees.create_membership_fee_cycle( %{ member_id: member.id, membership_fee_type_id: fee_type.id, cycle_start: Date.utc_today(), amount: Decimal.new("10.00"), status: :unpaid }, actor: user ) end test "cannot destroy cycle (returns forbidden)", %{user: user, cycle: cycle} do assert {:error, %Ash.Error.Forbidden{}} = MembershipFees.destroy_membership_fee_cycle(cycle, actor: user) end end describe "normal_user permission set" do setup %{actor: actor} do user = create_user_with_permission_set("normal_user", actor) cycle = create_cycle_fixture(actor) %{actor: actor, user: user, cycle: cycle} end test "can read membership_fee_cycles (list)", %{user: user} do {:ok, list} = Mv.MembershipFees.MembershipFeeCycle |> Ash.read(actor: user, domain: Mv.MembershipFees) assert is_list(list) end test "can update cycle status", %{user: user, cycle: cycle} do assert {:ok, updated} = MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user) assert updated.status == :paid end test "can mark_as_paid", %{user: user, cycle: cycle} do assert {:ok, updated} = cycle |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees) |> Ash.update(actor: user, domain: Mv.MembershipFees) assert updated.status == :paid end test "can create cycle", %{user: user, actor: actor} do member = create_member_fixture(actor) fee_type = create_fee_type_fixture(actor) assert {:ok, created} = MembershipFees.create_membership_fee_cycle( %{ member_id: member.id, membership_fee_type_id: fee_type.id, cycle_start: Date.utc_today(), amount: Decimal.new("10.00"), status: :unpaid }, actor: user ) assert created.member_id == member.id end test "can destroy cycle", %{user: user, cycle: cycle} do assert :ok = MembershipFees.destroy_membership_fee_cycle(cycle, actor: user) end end describe "admin permission set" do setup %{actor: actor} do user = create_user_with_permission_set("admin", actor) cycle = create_cycle_fixture(actor) %{actor: actor, user: user, cycle: cycle} end test "can read membership_fee_cycles (list)", %{user: user} do {:ok, list} = Mv.MembershipFees.MembershipFeeCycle |> Ash.read(actor: user, domain: Mv.MembershipFees) assert is_list(list) end test "can update cycle", %{user: user, cycle: cycle} do assert {:ok, updated} = MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user) assert updated.status == :paid end test "can mark_as_paid", %{user: user, cycle: cycle} do cycle_unpaid = cycle |> Ash.Changeset.for_update(:mark_as_unpaid, %{}, domain: Mv.MembershipFees) |> Ash.update!(actor: user, domain: Mv.MembershipFees) assert {:ok, updated} = cycle_unpaid |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees) |> Ash.update(actor: user, domain: Mv.MembershipFees) assert updated.status == :paid end test "can create cycle", %{user: user, actor: actor} do member = create_member_fixture(actor) fee_type = create_fee_type_fixture(actor) assert {:ok, created} = MembershipFees.create_membership_fee_cycle( %{ member_id: member.id, membership_fee_type_id: fee_type.id, cycle_start: Date.utc_today(), amount: Decimal.new("10.00"), status: :unpaid }, actor: user ) assert created.member_id == member.id end test "can destroy cycle", %{user: user, cycle: cycle} do assert :ok = MembershipFees.destroy_membership_fee_cycle(cycle, actor: user) end end end