- Policy tests: use Fixtures where applicable; create_custom_field() fix in custom_field_value. - Replace unused actor with _actor, remove unused alias Accounts in policy tests. - profile_navigation_test: disable Credo for intentional TODO comment.
463 lines
14 KiB
Elixir
463 lines
14 KiB
Elixir
defmodule Mv.Membership.CustomFieldValuePoliciesTest do
|
|
@moduledoc """
|
|
Tests for CustomFieldValue resource authorization policies.
|
|
|
|
Tests all 4 permission sets (own_data, read_only, normal_user, admin)
|
|
and verifies that policies correctly enforce access control based on
|
|
user roles and permission sets.
|
|
"""
|
|
# async: false because we need database commits to be visible across queries
|
|
use Mv.DataCase, async: false
|
|
|
|
alias Mv.Membership.{CustomField, CustomFieldValue}
|
|
alias Mv.Accounts
|
|
|
|
require Ash.Query
|
|
|
|
setup do
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
%{actor: system_actor}
|
|
end
|
|
|
|
defp create_linked_member_for_user(user, _actor) do
|
|
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
|
|
|
{:ok, member} =
|
|
Mv.Membership.create_member(
|
|
%{
|
|
first_name: "Linked",
|
|
last_name: "Member",
|
|
email: "linked#{System.unique_integer([:positive])}@example.com"
|
|
},
|
|
actor: admin
|
|
)
|
|
|
|
user
|
|
|> Ash.Changeset.for_update(:update, %{})
|
|
|> Ash.Changeset.force_change_attribute(:member_id, member.id)
|
|
|> Ash.update(actor: admin, domain: Mv.Accounts, return_notifications?: false)
|
|
|
|
member
|
|
end
|
|
|
|
defp create_unlinked_member(_actor) do
|
|
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
|
|
|
{:ok, member} =
|
|
Mv.Membership.create_member(
|
|
%{
|
|
first_name: "Unlinked",
|
|
last_name: "Member",
|
|
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
|
},
|
|
actor: admin
|
|
)
|
|
|
|
member
|
|
end
|
|
|
|
defp create_custom_field do
|
|
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
|
|
|
{:ok, field} =
|
|
CustomField
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
name: "test_field_#{System.unique_integer([:positive])}",
|
|
value_type: :string
|
|
})
|
|
|> Ash.create(actor: admin, domain: Mv.Membership)
|
|
|
|
field
|
|
end
|
|
|
|
defp create_custom_field_value(member_id, custom_field_id, value) do
|
|
admin = Mv.Fixtures.user_with_role_fixture("admin")
|
|
|
|
{:ok, cfv} =
|
|
CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: member_id,
|
|
custom_field_id: custom_field_id,
|
|
value: %{"_union_type" => "string", "_union_value" => value}
|
|
})
|
|
|> Ash.create(actor: admin, domain: Mv.Membership)
|
|
|
|
cfv
|
|
end
|
|
|
|
describe "own_data permission set (Mitglied)" do
|
|
setup %{actor: actor} do
|
|
user = Mv.Fixtures.user_with_role_fixture("own_data")
|
|
linked_member = create_linked_member_for_user(user, actor)
|
|
unlinked_member = create_unlinked_member(actor)
|
|
custom_field = create_custom_field()
|
|
|
|
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
|
|
|
|
cfv_unlinked =
|
|
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
|
|
|
|
{:ok, user} =
|
|
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
|
|
|
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)
|
|
|
|
%{
|
|
user: user,
|
|
linked_member: linked_member,
|
|
unlinked_member: unlinked_member,
|
|
custom_field: custom_field,
|
|
cfv_linked: cfv_linked,
|
|
cfv_unlinked: cfv_unlinked,
|
|
actor: actor
|
|
}
|
|
end
|
|
|
|
test "can read custom field values of linked member", %{user: user, cfv_linked: cfv_linked} do
|
|
{:ok, cfv} =
|
|
Ash.get(CustomFieldValue, cfv_linked.id, actor: user, domain: Mv.Membership)
|
|
|
|
assert cfv.id == cfv_linked.id
|
|
end
|
|
|
|
test "can list custom field values returns only linked member's values", %{
|
|
user: user,
|
|
cfv_linked: cfv_linked
|
|
} do
|
|
{:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)
|
|
|
|
assert length(values) == 1
|
|
assert hd(values).id == cfv_linked.id
|
|
end
|
|
|
|
test "can update custom field value of linked member", %{user: user, cfv_linked: cfv_linked} do
|
|
{:ok, updated} =
|
|
cfv_linked
|
|
|> Ash.Changeset.for_update(:update, %{
|
|
value: %{"_union_type" => "string", "_union_value" => "updated"}
|
|
})
|
|
|> Ash.update(actor: user, domain: Mv.Membership)
|
|
|
|
assert %Ash.Union{value: "updated", type: :string} = updated.value
|
|
end
|
|
|
|
test "can create custom field value for linked member", %{
|
|
user: user,
|
|
linked_member: linked_member,
|
|
actor: _actor
|
|
} do
|
|
# Create a second custom field via admin (own_data cannot create CustomField)
|
|
custom_field2 = create_custom_field()
|
|
|
|
{:ok, cfv} =
|
|
CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: linked_member.id,
|
|
custom_field_id: custom_field2.id,
|
|
value: %{"_union_type" => "string", "_union_value" => "new"}
|
|
})
|
|
|> Ash.create(actor: user, domain: Mv.Membership)
|
|
|
|
assert cfv.member_id == linked_member.id
|
|
assert cfv.custom_field_id == custom_field2.id
|
|
end
|
|
|
|
test "can destroy custom field value of linked member", %{
|
|
user: user,
|
|
cfv_linked: cfv_linked,
|
|
actor: actor
|
|
} do
|
|
result = Ash.destroy(cfv_linked, actor: user, domain: Mv.Membership)
|
|
assert :ok = result
|
|
|
|
assert {:error, _} =
|
|
Ash.get(CustomFieldValue, cfv_linked.id, domain: Mv.Membership, actor: actor)
|
|
end
|
|
|
|
test "cannot read custom field values of unlinked member", %{
|
|
user: user,
|
|
cfv_unlinked: cfv_unlinked
|
|
} do
|
|
assert_raise Ash.Error.Invalid, fn ->
|
|
Ash.get!(CustomFieldValue, cfv_unlinked.id, actor: user, domain: Mv.Membership)
|
|
end
|
|
end
|
|
|
|
test "cannot update custom field value of unlinked member", %{
|
|
user: user,
|
|
cfv_unlinked: cfv_unlinked
|
|
} do
|
|
assert_raise Ash.Error.Forbidden, fn ->
|
|
cfv_unlinked
|
|
|> Ash.Changeset.for_update(:update, %{
|
|
value: %{"_union_type" => "string", "_union_value" => "hacked"}
|
|
})
|
|
|> Ash.update!(actor: user, domain: Mv.Membership)
|
|
end
|
|
end
|
|
|
|
test "cannot create custom field value for unlinked member", %{
|
|
user: user,
|
|
unlinked_member: unlinked_member,
|
|
custom_field: custom_field
|
|
} do
|
|
assert_raise Ash.Error.Forbidden, fn ->
|
|
CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: unlinked_member.id,
|
|
custom_field_id: custom_field.id,
|
|
value: %{"_union_type" => "string", "_union_value" => "forbidden"}
|
|
})
|
|
|> Ash.create!(actor: user, domain: Mv.Membership)
|
|
end
|
|
end
|
|
|
|
test "cannot destroy custom field value of unlinked member", %{
|
|
user: user,
|
|
cfv_unlinked: cfv_unlinked
|
|
} do
|
|
assert_raise Ash.Error.Forbidden, fn ->
|
|
Ash.destroy!(cfv_unlinked, actor: user, domain: Mv.Membership)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "read_only permission set (Vorstand/Buchhaltung)" do
|
|
setup %{actor: actor} do
|
|
user = Mv.Fixtures.user_with_role_fixture("read_only")
|
|
linked_member = create_linked_member_for_user(user, actor)
|
|
unlinked_member = create_unlinked_member(actor)
|
|
custom_field = create_custom_field()
|
|
|
|
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
|
|
|
|
cfv_unlinked =
|
|
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
|
|
|
|
{:ok, user} =
|
|
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
|
|
|
%{
|
|
user: user,
|
|
cfv_linked: cfv_linked,
|
|
cfv_unlinked: cfv_unlinked,
|
|
custom_field: custom_field,
|
|
linked_member: linked_member,
|
|
unlinked_member: unlinked_member
|
|
}
|
|
end
|
|
|
|
test "can read all custom field values", %{
|
|
user: user,
|
|
cfv_linked: cfv_linked,
|
|
cfv_unlinked: cfv_unlinked
|
|
} do
|
|
{:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)
|
|
|
|
ids = Enum.map(values, & &1.id)
|
|
assert cfv_linked.id in ids
|
|
assert cfv_unlinked.id in ids
|
|
end
|
|
|
|
test "can read individual custom field value", %{user: user, cfv_unlinked: cfv_unlinked} do
|
|
{:ok, cfv} =
|
|
Ash.get(CustomFieldValue, cfv_unlinked.id, actor: user, domain: Mv.Membership)
|
|
|
|
assert cfv.id == cfv_unlinked.id
|
|
end
|
|
|
|
test "cannot create custom field value (returns forbidden)", %{
|
|
user: user,
|
|
linked_member: linked_member,
|
|
custom_field: custom_field
|
|
} do
|
|
assert_raise Ash.Error.Forbidden, fn ->
|
|
CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: linked_member.id,
|
|
custom_field_id: custom_field.id,
|
|
value: %{"_union_type" => "string", "_union_value" => "forbidden"}
|
|
})
|
|
|> Ash.create!(actor: user, domain: Mv.Membership)
|
|
end
|
|
end
|
|
|
|
test "cannot update custom field value (returns forbidden)", %{
|
|
user: user,
|
|
cfv_linked: cfv_linked
|
|
} do
|
|
assert_raise Ash.Error.Forbidden, fn ->
|
|
cfv_linked
|
|
|> Ash.Changeset.for_update(:update, %{
|
|
value: %{"_union_type" => "string", "_union_value" => "hacked"}
|
|
})
|
|
|> Ash.update!(actor: user, domain: Mv.Membership)
|
|
end
|
|
end
|
|
|
|
test "cannot destroy custom field value (returns forbidden)", %{
|
|
user: user,
|
|
cfv_linked: cfv_linked
|
|
} do
|
|
assert_raise Ash.Error.Forbidden, fn ->
|
|
Ash.destroy!(cfv_linked, actor: user, domain: Mv.Membership)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "normal_user permission set (Kassenwart)" do
|
|
setup %{actor: actor} do
|
|
user = Mv.Fixtures.user_with_role_fixture("normal_user")
|
|
linked_member = create_linked_member_for_user(user, actor)
|
|
unlinked_member = create_unlinked_member(actor)
|
|
custom_field = create_custom_field()
|
|
|
|
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
|
|
|
|
cfv_unlinked =
|
|
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
|
|
|
|
{:ok, user} =
|
|
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
|
|
|
%{
|
|
user: user,
|
|
cfv_linked: cfv_linked,
|
|
cfv_unlinked: cfv_unlinked,
|
|
custom_field: custom_field,
|
|
linked_member: linked_member,
|
|
unlinked_member: unlinked_member,
|
|
actor: actor
|
|
}
|
|
end
|
|
|
|
test "can read all custom field values", %{
|
|
user: user,
|
|
cfv_linked: cfv_linked,
|
|
cfv_unlinked: cfv_unlinked
|
|
} do
|
|
{:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)
|
|
|
|
ids = Enum.map(values, & &1.id)
|
|
assert cfv_linked.id in ids
|
|
assert cfv_unlinked.id in ids
|
|
end
|
|
|
|
test "can create custom field value", %{
|
|
user: user,
|
|
unlinked_member: unlinked_member,
|
|
actor: _actor
|
|
} do
|
|
# normal_user cannot create CustomField; use actor (admin) to create it
|
|
custom_field = create_custom_field()
|
|
|
|
{:ok, cfv} =
|
|
CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: unlinked_member.id,
|
|
custom_field_id: custom_field.id,
|
|
value: %{"_union_type" => "string", "_union_value" => "new"}
|
|
})
|
|
|> Ash.create(actor: user, domain: Mv.Membership)
|
|
|
|
assert cfv.member_id == unlinked_member.id
|
|
end
|
|
|
|
test "can update any custom field value", %{user: user, cfv_unlinked: cfv_unlinked} do
|
|
{:ok, updated} =
|
|
cfv_unlinked
|
|
|> Ash.Changeset.for_update(:update, %{
|
|
value: %{"_union_type" => "string", "_union_value" => "updated"}
|
|
})
|
|
|> Ash.update(actor: user, domain: Mv.Membership)
|
|
|
|
assert %Ash.Union{value: "updated", type: :string} = updated.value
|
|
end
|
|
|
|
test "can destroy any custom field value", %{
|
|
user: user,
|
|
cfv_unlinked: cfv_unlinked,
|
|
actor: actor
|
|
} do
|
|
:ok = Ash.destroy(cfv_unlinked, actor: user, domain: Mv.Membership)
|
|
|
|
assert {:error, _} =
|
|
Ash.get(CustomFieldValue, cfv_unlinked.id, domain: Mv.Membership, actor: actor)
|
|
end
|
|
end
|
|
|
|
describe "admin permission set" do
|
|
setup %{actor: actor} do
|
|
user = Mv.Fixtures.user_with_role_fixture("admin")
|
|
linked_member = create_linked_member_for_user(user, actor)
|
|
unlinked_member = create_unlinked_member(actor)
|
|
custom_field = create_custom_field()
|
|
|
|
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")
|
|
|
|
cfv_unlinked =
|
|
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")
|
|
|
|
{:ok, user} =
|
|
Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
|
|
|
%{
|
|
user: user,
|
|
cfv_linked: cfv_linked,
|
|
cfv_unlinked: cfv_unlinked,
|
|
custom_field: custom_field,
|
|
linked_member: linked_member,
|
|
unlinked_member: unlinked_member
|
|
}
|
|
end
|
|
|
|
test "can read all custom field values", %{
|
|
user: user,
|
|
cfv_linked: cfv_linked,
|
|
cfv_unlinked: cfv_unlinked
|
|
} do
|
|
{:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)
|
|
|
|
ids = Enum.map(values, & &1.id)
|
|
assert cfv_linked.id in ids
|
|
assert cfv_unlinked.id in ids
|
|
end
|
|
|
|
test "can create custom field value", %{user: user, unlinked_member: unlinked_member} do
|
|
custom_field = create_custom_field()
|
|
|
|
{:ok, cfv} =
|
|
CustomFieldValue
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
member_id: unlinked_member.id,
|
|
custom_field_id: custom_field.id,
|
|
value: %{"_union_type" => "string", "_union_value" => "new"}
|
|
})
|
|
|> Ash.create(actor: user, domain: Mv.Membership)
|
|
|
|
assert cfv.member_id == unlinked_member.id
|
|
end
|
|
|
|
test "can update any custom field value", %{user: user, cfv_unlinked: cfv_unlinked} do
|
|
{:ok, updated} =
|
|
cfv_unlinked
|
|
|> Ash.Changeset.for_update(:update, %{
|
|
value: %{"_union_type" => "string", "_union_value" => "updated"}
|
|
})
|
|
|> Ash.update(actor: user, domain: Mv.Membership)
|
|
|
|
assert %Ash.Union{value: "updated", type: :string} = updated.value
|
|
end
|
|
|
|
test "can destroy any custom field value", %{
|
|
user: user,
|
|
cfv_unlinked: cfv_unlinked,
|
|
actor: actor
|
|
} do
|
|
:ok = Ash.destroy(cfv_unlinked, actor: user, domain: Mv.Membership)
|
|
|
|
assert {:error, _} =
|
|
Ash.get(CustomFieldValue, cfv_unlinked.id, domain: Mv.Membership, actor: actor)
|
|
end
|
|
end
|
|
end
|