mitgliederverwaltung/test/mv/membership/join_request_test.exs
Simon b41f005d9e
Some checks failed
continuous-integration/drone/push Build is failing
refactor: apply review notes
2026-02-20 18:24:20 +01:00

118 lines
4.4 KiB
Elixir

defmodule Mv.Membership.JoinRequestTest do
@moduledoc """
Tests for JoinRequest resource and public policies (Subtask 1: onboarding join concept).
Covers: public create/read with actor nil, idempotency of confirm (confirmation_token_hash),
and minimal required attributes. No framework behaviour is tested; only our policies and constraints.
"""
use Mv.DataCase, async: false
alias Mv.Helpers.SystemActor
alias Mv.Membership
alias Mv.Membership.JoinRequest
require Ash.Query
# Client-only attributes for :confirm (server sets status, submitted_at, source, schema_version)
defp valid_confirm_attrs(opts \\ []) do
token =
Keyword.get(opts, :confirmation_token_hash, "hash_#{System.unique_integer([:positive])}")
[
email: "join_#{System.unique_integer([:positive])}@example.com",
confirmation_token_hash: token,
payload: %{}
]
|> Enum.into(%{})
end
describe "Public policies (actor: nil)" do
test "confirm with actor nil succeeds" do
attrs = valid_confirm_attrs()
assert {:ok, %JoinRequest{} = request} =
Membership.confirm_join_request(attrs, actor: nil)
assert request.email == attrs.email
assert request.status == "submitted"
assert request.source == "public_join"
end
test "no public read: actor nil cannot read JoinRequest (by id or list)" do
attrs = valid_confirm_attrs()
{:ok, created} = Membership.confirm_join_request(attrs, actor: nil)
get_result = Ash.get(JoinRequest, created.id, actor: nil, domain: Mv.Membership)
assert match?({:error, %Ash.Error.Forbidden{}}, get_result) or
match?(
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{}]}},
get_result
)
list_result = JoinRequest |> Ash.read(actor: nil, domain: Mv.Membership)
assert match?({:error, %Ash.Error.Forbidden{}}, list_result) or
match?({:error, %Ash.Error.Invalid{}}, list_result) or
list_result == {:ok, []},
"actor nil must not see any JoinRequests: got #{inspect(list_result)}"
end
test "generic create with actor nil is forbidden" do
# Use full attrs required by :create so the only failure is policy, not validation
attrs =
valid_confirm_attrs()
|> Map.merge(%{
status: "submitted",
submitted_at: DateTime.utc_now(),
source: "public_join",
schema_version: 1
})
assert {:error, %Ash.Error.Forbidden{errors: [%Ash.Error.Forbidden.Policy{}]}} =
JoinRequest
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create(actor: nil, domain: Mv.Membership)
end
end
describe "Idempotency (confirmation_token_hash)" do
test "second create with same confirmation_token_hash does not create duplicate" do
system_actor = SystemActor.get_system_actor()
token = "idempotent_token_#{System.unique_integer([:positive])}"
attrs1 = valid_confirm_attrs(confirmation_token_hash: token)
attrs2 = valid_confirm_attrs(confirmation_token_hash: token)
attrs2 = %{attrs2 | email: "other_#{System.unique_integer([:positive])}@example.com"}
assert {:ok, _first} = Membership.confirm_join_request(attrs1, actor: nil)
# Second call with same token: idempotent return {:ok, nil} (no public read)
assert {:ok, nil} = Membership.confirm_join_request(attrs2, actor: nil)
# Count via allowed admin read (no authorize?: false)
assert {:ok, list} = Membership.list_join_requests(actor: system_actor)
count = Enum.count(list, &(&1.confirmation_token_hash == token))
assert count == 1,
"expected exactly one JoinRequest with this confirmation_token_hash, got #{count}"
end
end
describe "Resource and validations" do
test "create with minimal required attributes succeeds" do
attrs = valid_confirm_attrs()
assert {:ok, %JoinRequest{}} = Membership.confirm_join_request(attrs, actor: nil)
end
test "email is required" do
attrs = valid_confirm_attrs() |> Map.delete(:email)
assert {:error, %Ash.Error.Invalid{errors: errors}} =
Membership.confirm_join_request(attrs, actor: nil)
assert Enum.any?(errors, fn e -> Map.get(e, :field) == :email end),
"expected an error for field :email, got: #{inspect(errors)}"
end
end
end