mitgliederverwaltung/test/mv/membership/custom_field_value_policies_test.exs
Moritz 4473cfd372
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/production Build is passing
Tests: use code interface for Member create/update (actor propagation)
2026-01-29 16:10:12 +01:00

496 lines
15 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
alias Mv.Authorization
require Ash.Query
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
# Helper to create a role with a specific permission set
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
# Helper to create a user with a specific permission set
# Returns user with role preloaded (required for authorization)
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_linked_member_for_user(user, actor) do
{:ok, member} =
Mv.Membership.create_member(
%{
first_name: "Linked",
last_name: "Member",
email: "linked#{System.unique_integer([:positive])}@example.com"
},
actor: actor
)
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.force_change_attribute(:member_id, member.id)
|> Ash.update(actor: actor, domain: Mv.Accounts, return_notifications?: false)
member
end
defp create_unlinked_member(actor) do
{:ok, member} =
Mv.Membership.create_member(
%{
first_name: "Unlinked",
last_name: "Member",
email: "unlinked#{System.unique_integer([:positive])}@example.com"
},
actor: actor
)
member
end
defp create_custom_field(actor) do
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field_#{System.unique_integer([:positive])}",
value_type: :string
})
|> Ash.create(actor: actor)
field
end
defp create_custom_field_value(member_id, custom_field_id, value, actor) do
{: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: actor, domain: Mv.Membership)
cfv
end
describe "own_data permission set (Mitglied)" do
setup %{actor: actor} do
user = create_user_with_permission_set("own_data", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
custom_field = create_custom_field(actor)
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{: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(actor)
{: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 = create_user_with_permission_set("read_only", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
custom_field = create_custom_field(actor)
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{: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 = create_user_with_permission_set("normal_user", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
custom_field = create_custom_field(actor)
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{: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(actor)
{: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 = create_user_with_permission_set("admin", actor)
linked_member = create_linked_member_for_user(user, actor)
unlinked_member = create_unlinked_member(actor)
custom_field = create_custom_field(actor)
cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked", actor)
cfv_unlinked =
create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked", actor)
{: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(user)
{: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