test: add tests for approval ui
This commit is contained in:
parent
021b709e6a
commit
50433e607f
6 changed files with 466 additions and 11 deletions
139
test/membership/join_request_approval_domain_test.exs
Normal file
139
test/membership/join_request_approval_domain_test.exs
Normal 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
|
||||
115
test/membership/join_request_approval_policy_test.exs
Normal file
115
test/membership/join_request_approval_policy_test.exs
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue