test: add tests for approval ui

This commit is contained in:
Simon 2026-03-10 23:21:57 +01:00
parent 021b709e6a
commit 50433e607f
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
6 changed files with 466 additions and 11 deletions

View file

@ -0,0 +1,139 @@
defmodule Mv.Membership.JoinRequestApprovalDomainTest do
@moduledoc """
Domain tests for JoinRequest approval: approve/reject and promotion to Member (Step 2).
Asserts that approve creates one Member with mapped data, reject does not create Member,
status rules, and idempotency. No User creation in MVP.
"""
use Mv.DataCase, async: true
import Ash.Expr
require Ash.Query
alias Mv.Fixtures
alias Mv.Helpers.SystemActor
alias Mv.Membership
alias Mv.Membership.Member
defp member_count do
actor = SystemActor.get_system_actor()
{:ok, members} = Membership.list_members(actor: actor)
length(members)
end
describe "approve_join_request/2 promotion to Member" do
test "approve creates exactly one member with email, first_name, last_name from JoinRequest" do
request =
Fixtures.submitted_join_request_fixture(%{
first_name: "Approved",
last_name: "User"
})
count_before = member_count()
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, approved} = Membership.approve_join_request(request.id, actor: user)
assert approved.status == :approved
assert member_count() == count_before + 1
request_email = request.email
[member] =
Member
|> Ash.Query.filter(expr(^ref(:email) == ^request_email))
|> Ash.read!(actor: SystemActor.get_system_actor(), domain: Membership)
assert member.email == request.email
assert member.first_name == request.first_name
assert member.last_name == request.last_name
end
test "approve does not create a User (MVP)" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, _} = Membership.approve_join_request(request.id, actor: user)
# No User should exist with this email from the approval flow
request_email = request.email
users_with_email =
Mv.Accounts.User
|> Ash.Query.filter(expr(^ref(:email) == ^request_email))
|> Ash.read!(authorize?: false)
assert users_with_email == []
end
end
describe "reject_join_request/2" do
test "reject does not create a member" do
request = Fixtures.submitted_join_request_fixture()
count_before = member_count()
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, rejected} = Membership.reject_join_request(request.id, actor: user)
assert rejected.status == :rejected
assert rejected.rejected_at != nil
assert member_count() == count_before
end
end
describe "approve_join_request/2 status and idempotency" do
test "approve when status is already approved is idempotent or returns error" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, _} = Membership.approve_join_request(request.id, actor: user)
count_after_first = member_count()
# Second approve: either {:ok, request} with no duplicate member, or {:error, _}
result = Membership.approve_join_request(request.id, actor: user)
if match?({:ok, _}, result) do
assert member_count() == count_after_first
else
assert {:error, _} = result
end
end
test "approve when status is pending_confirmation returns error" do
token = "pending-token-#{System.unique_integer([:positive])}"
attrs = %{
email: "pending#{System.unique_integer([:positive])}@example.com",
confirmation_token: token
}
{:ok, request} = Membership.submit_join_request(attrs, actor: nil)
assert request.status == :pending_confirmation
user = Fixtures.user_with_role_fixture("normal_user")
assert {:error, _} = Membership.approve_join_request(request.id, actor: user)
end
test "approve when status is rejected returns error" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, _} = Membership.reject_join_request(request.id, actor: user)
assert {:error, _} = Membership.approve_join_request(request.id, actor: user)
end
end
describe "approve_join_request/2 defaults" do
test "created member has join_date and membership_fee_type when not in form_data" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, _} = Membership.approve_join_request(request.id, actor: user)
request_email = request.email
[member] =
Member
|> Ash.Query.filter(expr(^ref(:email) == ^request_email))
|> Ash.read!(actor: SystemActor.get_system_actor(), domain: Membership)
assert member.join_date != nil
assert member.membership_fee_type_id != nil
end
end
end

View file

@ -0,0 +1,115 @@
defmodule Mv.Membership.JoinRequestApprovalPolicyTest do
@moduledoc """
Policy tests for JoinRequest approval UI (Step 2).
Asserts that approve/reject and list are allowed for normal_user and admin,
and forbidden for read_only, own_data, and actor: nil.
No UI; domain and resource policies only.
"""
use Mv.DataCase, async: true
alias Mv.Fixtures
alias Mv.Membership
describe "list_join_requests/1" do
test "normal_user can list join requests" do
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, _list} = Membership.list_join_requests(actor: user)
end
test "admin can list join requests" do
user = Fixtures.user_with_role_fixture("admin")
assert {:ok, _list} = Membership.list_join_requests(actor: user)
end
test "read_only cannot list join requests" do
user = Fixtures.user_with_role_fixture("read_only")
assert {:error, %Ash.Error.Forbidden{}} = Membership.list_join_requests(actor: user)
end
test "own_data cannot list join requests" do
user = Fixtures.user_with_role_fixture("own_data")
assert {:error, %Ash.Error.Forbidden{}} = Membership.list_join_requests(actor: user)
end
test "actor nil cannot list join requests" do
assert {:error, %Ash.Error.Forbidden{}} = Membership.list_join_requests(actor: nil)
end
end
describe "approve_join_request/2" do
setup do
request = Fixtures.submitted_join_request_fixture()
%{request: request}
end
test "normal_user can approve a submitted join request", %{request: request} do
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, approved} = Membership.approve_join_request(request.id, actor: user)
assert approved.status == :approved
assert approved.approved_at != nil
assert approved.reviewed_by_user_id == user.id
end
test "admin can approve a submitted join request", %{request: request} do
user = Fixtures.user_with_role_fixture("admin")
assert {:ok, approved} = Membership.approve_join_request(request.id, actor: user)
assert approved.status == :approved
end
test "read_only cannot approve", %{request: request} do
user = Fixtures.user_with_role_fixture("read_only")
assert {:error, %Ash.Error.Forbidden{}} =
Membership.approve_join_request(request.id, actor: user)
end
test "own_data cannot approve", %{request: request} do
user = Fixtures.user_with_role_fixture("own_data")
assert {:error, %Ash.Error.Forbidden{}} =
Membership.approve_join_request(request.id, actor: user)
end
test "actor nil cannot approve", %{request: request} do
assert {:error, %Ash.Error.Forbidden{}} =
Membership.approve_join_request(request.id, actor: nil)
end
end
describe "reject_join_request/2" do
setup do
request = Fixtures.submitted_join_request_fixture()
%{request: request}
end
test "normal_user can reject a submitted join request", %{request: request} do
user = Fixtures.user_with_role_fixture("normal_user")
assert {:ok, rejected} = Membership.reject_join_request(request.id, actor: user)
assert rejected.status == :rejected
assert rejected.rejected_at != nil
assert rejected.reviewed_by_user_id == user.id
end
test "admin can reject a submitted join request", %{request: request} do
user = Fixtures.user_with_role_fixture("admin")
assert {:ok, rejected} = Membership.reject_join_request(request.id, actor: user)
assert rejected.status == :rejected
end
test "read_only cannot reject", %{request: request} do
user = Fixtures.user_with_role_fixture("read_only")
assert {:error, %Ash.Error.Forbidden{}} =
Membership.reject_join_request(request.id, actor: user)
end
test "own_data cannot reject", %{request: request} do
user = Fixtures.user_with_role_fixture("own_data")
assert {:error, %Ash.Error.Forbidden{}} =
Membership.reject_join_request(request.id, actor: user)
end
test "actor nil cannot reject", %{request: request} do
assert {:error, %Ash.Error.Forbidden{}} =
Membership.reject_join_request(request.id, actor: nil)
end
end
end

View file

@ -212,6 +212,72 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
end
end
describe "join_requests routes (approval UI, Step 2)" do
test "normal_user can access /join_requests" do
user = Fixtures.user_with_role_fixture("normal_user")
conn = conn_with_user("/join_requests", user) |> CheckPagePermission.call([])
refute conn.halted
end
test "normal_user can access /join_requests/:id" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("normal_user")
conn = conn_with_user("/join_requests/#{request.id}", user) |> CheckPagePermission.call([])
refute conn.halted
end
test "read_only cannot access /join_requests" do
user = Fixtures.user_with_role_fixture("read_only")
conn = conn_with_user("/join_requests", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "read_only cannot access /join_requests/:id" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("read_only")
conn = conn_with_user("/join_requests/#{request.id}", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "own_data cannot access /join_requests" do
user = Fixtures.user_with_role_fixture("own_data")
conn = conn_with_user("/join_requests", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "own_data cannot access /join_requests/:id" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("own_data")
conn = conn_with_user("/join_requests/#{request.id}", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "admin can access /join_requests" do
user = Fixtures.user_with_role_fixture("admin")
conn = conn_with_user("/join_requests", user) |> CheckPagePermission.call([])
refute conn.halted
end
test "admin can access /join_requests/:id" do
request = Fixtures.submitted_join_request_fixture()
user = Fixtures.user_with_role_fixture("admin")
conn = conn_with_user("/join_requests/#{request.id}", user) |> CheckPagePermission.call([])
refute conn.halted
end
end
describe "error handling" do
test "user with no role is denied" do
user = Fixtures.user_with_role_fixture("admin")
@ -429,6 +495,22 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
conn = get(conn, "/admin/roles/#{id}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /join_requests redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/join_requests")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /join_requests/:id redirects to user profile", %{
conn: conn,
current_user: user
} do
request = Fixtures.submitted_join_request_fixture()
conn = get(conn, "/join_requests/#{request.id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
end
describe "integration: Mitglied (own_data) can access allowed paths via full router" do
@ -713,15 +795,37 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
conn = get(conn, "/admin/roles/#{id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /join_requests redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/join_requests")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /join_requests/:id redirects to user profile", %{
conn: conn,
current_user: user
} do
request = Fixtures.submitted_join_request_fixture()
conn = get(conn, "/join_requests/#{request.id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
end
# normal_user (Kassenwart): allowed /, /members, /members/new, /members/:id, /members/:id/edit, /groups, /groups/:slug
# normal_user (Kassenwart): allowed /, /members, /members/new, /members/:id, /members/:id/edit, /groups, /groups/:slug, /join_requests
describe "integration: normal_user (Kassenwart) allowed paths via full router" do
setup %{conn: conn, current_user: current_user} do
member = Mv.Fixtures.member_fixture()
group = Mv.Fixtures.group_fixture()
join_request = Fixtures.submitted_join_request_fixture()
{:ok, conn: conn, current_user: current_user, member_id: member.id, group_slug: group.slug}
{:ok,
conn: conn,
current_user: current_user,
member_id: member.id,
group_slug: group.slug,
join_request_id: join_request.id}
end
@tag role: :normal_user
@ -804,6 +908,18 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
conn = get(conn, "/users/#{user.id}/show/edit")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /join_requests returns 200", %{conn: conn} do
conn = get(conn, "/join_requests")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /join_requests/:id returns 200", %{conn: conn, join_request_id: id} do
conn = get(conn, "/join_requests/#{id}")
assert conn.status == 200
end
end
describe "integration: normal_user denied paths via full router" do

View file

@ -299,4 +299,38 @@ defmodule Mv.Fixtures do
{:error, error} -> raise "Failed to create group: #{inspect(error)}"
end
end
@doc """
Creates a join request in status :submitted (for approval UI tests).
Uses the public flow: submit_join_request then confirm_join_request with a known token.
Returns the JoinRequest struct so tests can use its id for approve/reject.
## Parameters
- `attrs` - Optional map: :email, :first_name, :last_name, :form_data, :schema_version.
Defaults: unique email; confirmation_token is generated and used internally.
## Returns
- JoinRequest struct with status :submitted
## Examples
iex> request = submitted_join_request_fixture()
iex> request.status
:submitted
iex> request = submitted_join_request_fixture(%{first_name: "Jane", last_name: "Doe"})
"""
def submitted_join_request_fixture(attrs \\ %{}) do
token = "fixture-token-#{System.unique_integer([:positive])}"
base = %{
email: "join#{System.unique_integer([:positive])}@example.com",
confirmation_token: token
}
attrs = base |> Map.merge(attrs) |> Map.put(:confirmation_token, token)
{:ok, _} = Membership.submit_join_request(attrs, actor: nil)
{:ok, request} = Membership.confirm_join_request(token, actor: nil)
request
end
end