add join request resource #463

Merged
simon merged 9 commits from feature/308-web-form into main 2026-03-10 10:11:55 +01:00
Showing only changes of commit 2a04fad4fe - Show all commits

View file

@ -0,0 +1,114 @@
defmodule Mv.Membership.JoinRequestTest do
@moduledoc """
Tests for JoinRequest resource (public join flow, Subtask 1).
Covers: submit and confirm actions with actor: nil, validations, idempotent confirm,
and policy that non-public actions (e.g. read) are Forbidden with actor: nil.
No UI or email; resource and persistence only.
Requires: Mv.Membership.JoinRequest resource and domain functions
submit_join_request/2, confirm_join_request/2. Policy test requires the resource
to be loaded; unskip when JoinRequest exists.
"""
use Mv.DataCase, async: true
alias Mv.Membership
# Valid minimal attributes for submit (email required; confirmation_token optional for tests)
@valid_submit_attrs %{
email: "join#{System.unique_integer([:positive])}@example.com"
}
describe "submit_join_request/2 (create with actor: nil)" do
test "creates JoinRequest in pending_confirmation with valid attributes and actor nil" do
attrs = Map.put(@valid_submit_attrs, :confirmation_token, "test-token-#{System.unique_integer([:positive])}")
assert {:ok, request} =
Membership.submit_join_request(attrs, actor: nil)
assert request.status == :pending_confirmation
assert request.email == attrs.email
assert request.confirmation_token_hash != nil
assert request.confirmation_token_expires_at != nil
end
test "persists first_name, last_name and form_data when provided" do
attrs =
@valid_submit_attrs
|> Map.put(:confirmation_token, "token-#{System.unique_integer([:positive])}")
|> Map.put(:first_name, "Jane")
|> Map.put(:last_name, "Doe")
|> Map.put(:form_data, %{"city" => "Berlin", "notes" => "Hello"})
|> Map.put(:schema_version, 1)
assert {:ok, request} =
Membership.submit_join_request(attrs, actor: nil)
assert request.first_name == "Jane"
assert request.last_name == "Doe"
assert request.form_data == %{"city" => "Berlin", "notes" => "Hello"}
assert request.schema_version == 1
end
test "returns validation error when email is missing" do
attrs = %{first_name: "Test"} |> Map.delete(:email)
assert {:error, %Ash.Error.Invalid{errors: errors}} =
Membership.submit_join_request(attrs, actor: nil)
assert error_message(errors, :email) =~ "must be present"
end
end
describe "confirm_join_request/2 (update with actor: nil)" do
test "updates JoinRequest to submitted when given valid token and actor nil" do
token = "confirm-token-#{System.unique_integer([:positive])}"
attrs = Map.put(@valid_submit_attrs, :confirmation_token, token)
{:ok, request_before} = Membership.submit_join_request(attrs, actor: nil)
assert request_before.status == :pending_confirmation
assert {:ok, request_after} =
Membership.confirm_join_request(token, actor: nil)
assert request_after.id == request_before.id
assert request_after.status == :submitted
assert request_after.submitted_at != nil
end
test "confirm is idempotent when called twice with same token" do
token = "idempotent-token-#{System.unique_integer([:positive])}"
attrs = Map.put(@valid_submit_attrs, :confirmation_token, token)
{:ok, _} = Membership.submit_join_request(attrs, actor: nil)
{:ok, first} = Membership.confirm_join_request(token, actor: nil)
submitted_at = first.submitted_at
assert {:ok, second} = Membership.confirm_join_request(token, actor: nil)
assert second.status == :submitted
assert second.submitted_at == submitted_at
end
test "returns error when token is unknown or invalid" do
assert {:error, _} = Membership.confirm_join_request("nonexistent-token", actor: nil)
end
@tag :skip
test "returns error when token is expired (requires fixture for expired token)"
end
describe "policies (actor: nil)" do
@tag :skip
test "read with actor nil returns Forbidden (unskip and add: Ash.read(Mv.Membership.JoinRequest, actor: nil, domain: Mv.Membership) -> expect Forbidden)" do
# When JoinRequest resource exists: assert {:error, %Ash.Error.Forbidden{}} = Ash.read(Mv.Membership.JoinRequest, actor: nil, domain: Mv.Membership)
flunk("Add JoinRequest resource, then unskip and replace this with the Ash.read assertion")
end
end
defp error_message(errors, field) do
errors
|> Enum.filter(fn err -> Map.get(err, :field) == field end)
|> Enum.map(&Map.get(&1, :message, ""))
|> List.first()
end
end