104 lines
3.8 KiB
Elixir
104 lines
3.8 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.Membership
|
|
alias Mv.Membership.JoinRequest
|
|
|
|
require Ash.Query
|
|
|
|
# Minimal valid attributes for the public :confirm action (per concept §2.3.2)
|
|
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,
|
|
status: "submitted",
|
|
submitted_at: DateTime.utc_now(),
|
|
source: "public_join",
|
|
schema_version: 1,
|
|
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 "read with actor nil succeeds for created join request" do
|
|
attrs = valid_confirm_attrs()
|
|
{:ok, created} = Membership.confirm_join_request(attrs, actor: nil)
|
|
|
|
assert {:ok, %JoinRequest{} = read} =
|
|
Ash.get(JoinRequest, created.id, actor: nil, domain: Mv.Membership)
|
|
|
|
assert read.id == created.id
|
|
assert read.email == created.email
|
|
end
|
|
|
|
test "generic create with actor nil is forbidden" do
|
|
attrs = valid_confirm_attrs()
|
|
|
|
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 = Mv.Helpers.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, existing} (concept §2.3.2)
|
|
assert {:ok, second} = Membership.confirm_join_request(attrs2, actor: nil)
|
|
assert second.id == first.id, "idempotent confirm must return the existing record"
|
|
|
|
count =
|
|
JoinRequest
|
|
|> Ash.Query.filter(confirmation_token_hash == ^token)
|
|
|> Ash.read!(actor: system_actor, domain: Mv.Membership, authorize?: false)
|
|
|> length()
|
|
|
|
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
|