test: add page permission tests and ConnCase role tags

- ConnCase: add :read_only and :normal_user role tags for tests.
- Add CheckPagePermission plug tests (unit + integration for member, read_only,
  normal_user, admin). Update permission_sets_test (refute "/" for own_data).
- Profile navigation, global_settings, role_live, membership_fee_type: use
  users with role for "/" access; expect redirect for own_data on /settings
  and /admin/roles.
This commit is contained in:
Moritz 2026-01-29 23:56:12 +01:00
parent 626e8a872e
commit ad00e8e7b6
Signed by: moritz
GPG key ID: 1020A035E5DD0824
8 changed files with 943 additions and 58 deletions

View file

@ -127,7 +127,8 @@ defmodule Mv.Authorization.PermissionSetsTest do
test "includes correct pages" do
permissions = PermissionSets.get_permissions(:own_data)
assert "/" in permissions.pages
# Root "/" is not allowed for own_data (Mitglied is redirected to profile)
refute "/" in permissions.pages
assert "/profile" in permissions.pages
assert "/members/:id" in permissions.pages
end

View file

@ -158,15 +158,12 @@ defmodule MvWeb.GlobalSettingsLiveTest do
end
test "non-admin user does not see import section", %{conn: conn} do
# Create non-admin user (member role)
# Member (own_data) is redirected when accessing /settings (no page permission)
member_user = Mv.Fixtures.user_with_role_fixture("own_data")
conn = MvWeb.ConnCase.conn_with_password_user(conn, member_user)
{:ok, _view, html} = live(conn, ~p"/settings")
# Import section should not be visible
refute html =~ "Import Members" or html =~ "CSV Import" or
(html =~ "Import" and html =~ "CSV")
assert {:error, {:redirect, %{to: to}}} = live(conn, ~p"/settings")
assert to == "/users/#{member_user.id}"
end
end
@ -236,15 +233,12 @@ defmodule MvWeb.GlobalSettingsLiveTest do
end
test "non-admin cannot start import", %{conn: conn} do
# Create non-admin user
# Member (own_data) is redirected when accessing /settings (no page permission)
member_user = Mv.Fixtures.user_with_role_fixture("own_data")
conn = MvWeb.ConnCase.conn_with_password_user(conn, member_user)
{:ok, view, _html} = live(conn, ~p"/settings")
# Since non-admin shouldn't see the section, we check that import section is not visible
html = render(view)
refute html =~ "Import Members" or html =~ "CSV Import" or html =~ "start_import"
assert {:error, {:redirect, %{to: to}}} = live(conn, ~p"/settings")
assert to == "/users/#{member_user.id}"
end
test "invalid CSV shows user-friendly error", %{conn: conn} do

View file

@ -11,17 +11,8 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
require Ash.Query
setup %{conn: conn} do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
# Create admin user
{:ok, user} =
Mv.Accounts.User
|> Ash.Changeset.for_create(:register_with_password, %{
email: "admin#{System.unique_integer([:positive])}@mv.local",
password: "testpassword123"
})
|> Ash.create(actor: system_actor)
# User must have admin role (or normal_user) to access /membership_fee_types pages
user = Mv.Fixtures.user_with_role_fixture("admin")
authenticated_conn = conn_with_password_user(conn, user)
%{conn: authenticated_conn, user: user}
end

View file

@ -29,8 +29,8 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|> Ash.create!(actor: admin_user)
end
# Helper to create a member
# Uses admin actor from global setup to ensure authorization; falls back to system_actor when nil
# Helper to create a member. Requires actor (e.g. admin_user from setup); no fallback so
# missing-actor bugs are not masked in tests.
defp create_member(attrs, actor) do
default_attrs = %{
first_name: "Test",
@ -39,8 +39,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
}
attrs = Map.merge(default_attrs, attrs)
effective_actor = actor || Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} = Mv.Membership.create_member(attrs, actor: effective_actor)
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
member
end

View file

@ -9,8 +9,8 @@ defmodule MvWeb.ProfileNavigationTest do
describe "profile navigation" do
test "clicking profile button redirects to current user profile", %{conn: conn} do
# Setup: Create and login a user
user = create_test_user(%{email: "test@example.com"})
# User needs a role with page permission for "/" (e.g. admin)
user = Mv.Fixtures.user_with_role_fixture("admin")
conn = conn_with_password_user(conn, user)
{:ok, view, _html} = live(conn, "/")
@ -21,9 +21,18 @@ defmodule MvWeb.ProfileNavigationTest do
assert_redirected(view, "/users/#{user.id}")
end
test "profile navigation shows correct user data", %{conn: conn} do
# Setup: Create and login a user
test "profile navigation shows correct user data", %{conn: conn, actor: actor} do
# User with password (from create_test_user) and admin role so they can access "/"
user = create_test_user(%{email: "test@example.com"})
admin_role = Mv.Fixtures.role_fixture("admin")
{:ok, user} =
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update(actor: actor)
user = Ash.load!(user, :role, domain: Mv.Accounts, actor: actor)
conn = conn_with_password_user(conn, user)
# Navigate to profile
@ -40,8 +49,8 @@ defmodule MvWeb.ProfileNavigationTest do
describe "sidebar" do
test "renders profile button with correct attributes", %{conn: conn} do
# Setup: Create and login a user
user = create_test_user(%{email: "test@example.com"})
# User needs a role with page permission for "/"
user = Mv.Fixtures.user_with_role_fixture("admin")
conn = conn_with_password_user(conn, user)
{:ok, _view, html} = live(conn, "/")
@ -85,16 +94,27 @@ defmodule MvWeb.ProfileNavigationTest do
})
|> Ash.create!(domain: Mv.Accounts, actor: actor)
# Assign role so user can access "/" (page permission)
admin_role = Mv.Fixtures.role_fixture("admin")
{:ok, user_with_role} =
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update(actor: actor)
user_with_role = Ash.load!(user_with_role, :role, domain: Mv.Accounts, actor: actor)
# Login user via OIDC
conn = sign_in_user_via_oidc(conn, user)
conn = sign_in_user_via_oidc(conn, user_with_role)
# Navigate to home and click profile
{:ok, view, _html} = live(conn, "/")
view |> element("a", "Profil") |> render_click()
# Verify we're on the correct profile page with OIDC specific information
{:ok, _profile_view, html} = live(conn, "/users/#{user.id}")
assert html =~ to_string(user.email)
{:ok, _profile_view, html} = live(conn, "/users/#{user_with_role.id}")
assert html =~ to_string(user_with_role.email)
# Password auth should be disabled for OIDC users
assert html =~ "Not enabled"
end
@ -103,14 +123,10 @@ defmodule MvWeb.ProfileNavigationTest do
conn: conn,
actor: actor
} do
# Create password user
password_user =
create_test_user(%{
email: "password2@example.com",
password: "test_password123"
})
# Users need a role with page permission for "/"
password_user = Mv.Fixtures.user_with_role_fixture("admin")
# Create OIDC user
# Create OIDC user and assign admin role
user_info = %{
"sub" => "oidc_789",
"preferred_username" => "oidc@example.com"
@ -129,6 +145,17 @@ defmodule MvWeb.ProfileNavigationTest do
})
|> Ash.create!(domain: Mv.Accounts, actor: actor)
admin_role = Mv.Fixtures.role_fixture("admin")
{:ok, oidc_user_with_role} =
oidc_user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update(actor: actor)
oidc_user_with_role =
Ash.load!(oidc_user_with_role, :role, domain: Mv.Accounts, actor: actor)
# Test with password user
conn_password = conn_with_password_user(conn, password_user)
{:ok, view_password, _html} = live(conn_password, "/")
@ -136,16 +163,17 @@ defmodule MvWeb.ProfileNavigationTest do
assert_redirected(view_password, "/users/#{password_user.id}")
# Test with OIDC user
conn_oidc = sign_in_user_via_oidc(conn, oidc_user)
conn_oidc = sign_in_user_via_oidc(conn, oidc_user_with_role)
{:ok, view_oidc, _html} = live(conn_oidc, "/")
view_oidc |> element("a", "Profil") |> render_click()
assert_redirected(view_oidc, "/users/#{oidc_user.id}")
assert_redirected(view_oidc, "/users/#{oidc_user_with_role.id}")
end
end
describe "authenticated views" do
# User must have a role with page permission to access /members, /users, etc.
setup %{conn: conn} do
user = create_test_user(%{email: "test@example.com"})
user = Mv.Fixtures.user_with_role_fixture("admin")
conn = conn_with_password_user(conn, user)
{:ok, conn: conn, user: user}
end

View file

@ -441,18 +441,11 @@ defmodule MvWeb.RoleLiveTest do
end
test "only admin can access /admin/roles", %{conn: conn, actor: actor} do
{conn, _user} = create_non_admin_user(conn, actor)
{conn, user} = create_non_admin_user(conn, actor)
# Non-admin should be redirected or see error
# Note: Authorization is checked via can_access_page? which returns false
# The page might still mount but show no content or redirect
# For now, we just verify the page doesn't work as expected for non-admin
{:ok, _view, html} = live(conn, "/admin/roles")
# Non-admin should not see "New Role" button (can? returns false)
# But the button might still be in HTML, just hidden or disabled
# We verify that the page loads but admin features are restricted
assert html =~ "Listing Roles" || html =~ "Roles"
# Non-admin (no role or non-admin role) is redirected by CheckPagePermission plug
assert {:error, {:redirect, %{to: to}}} = live(conn, "/admin/roles")
assert to == "/users/#{user.id}"
end
test "admin can access /admin/roles", %{conn: conn, actor: actor} do

View file

@ -0,0 +1,867 @@
defmodule MvWeb.Plugs.CheckPagePermissionTest do
@moduledoc """
Tests for the CheckPagePermission plug.
"""
use MvWeb.ConnCase, async: true
alias MvWeb.Plugs.CheckPagePermission
alias Mv.Fixtures
defp conn_with_user(path, user) do
build_conn(:get, path)
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_private(:phoenix_router, MvWeb.Router)
|> Plug.Conn.assign(:current_user, user)
end
defp conn_without_user(path) do
build_conn(:get, path)
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_private(:phoenix_router, MvWeb.Router)
end
describe "static routes" do
test "user with permission for \"/members\" can access (conn not halted)" do
user = Fixtures.user_with_role_fixture("read_only")
conn = conn_with_user("/members", user) |> CheckPagePermission.call([])
refute conn.halted
end
test "user without permission for \"/members\" is denied (conn halted, redirected to user profile)" do
user = Fixtures.user_with_role_fixture("own_data")
conn = conn_with_user("/members", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "flash error message present after denial" do
user = Fixtures.user_with_role_fixture("own_data")
conn = conn_with_user("/members", user) |> CheckPagePermission.call([])
assert Phoenix.Flash.get(conn.assigns[:flash] || %{}, :error) ==
"You don't have permission to access this page."
end
end
describe "dynamic routes" do
test "user with \"/members/:id\" permission can access \"/members/123\"" do
user = Fixtures.user_with_role_fixture("read_only")
conn = conn_with_user("/members/123", user) |> CheckPagePermission.call([])
refute conn.halted
end
test "user with \"/members/:id/edit\" permission can access \"/members/456/edit\"" do
user = Fixtures.user_with_role_fixture("normal_user")
conn = conn_with_user("/members/456/edit", user) |> CheckPagePermission.call([])
refute conn.halted
end
test "user with only \"/members/:id\" cannot access \"/members/123/edit\"" do
user = Fixtures.user_with_role_fixture("read_only")
conn = conn_with_user("/members/123/edit", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "own_data user with linked member can access /members/:id/edit (plug direct call)" do
member = Fixtures.member_fixture()
user = Fixtures.user_with_role_fixture("own_data")
user_with_member = Mv.Authorization.Actor.ensure_loaded(user)
# Simulate user with linked member (struct may not have member_id after session load)
user_with_member = %{user_with_member | member_id: member.id}
assert CheckPagePermission.user_can_access_page?(
user_with_member,
"/members/#{member.id}/edit"
),
"plug must allow own_data user with linked member to access member edit"
conn =
conn_with_user("/members/#{member.id}/edit", user_with_member)
|> CheckPagePermission.call([])
refute conn.halted
end
test "own_data user with linked member can access /members/:id/show/edit (plug direct call)" do
member = Fixtures.member_fixture()
user = Fixtures.user_with_role_fixture("own_data")
user_with_member = Mv.Authorization.Actor.ensure_loaded(user)
user_with_member = %{user_with_member | member_id: member.id}
assert CheckPagePermission.user_can_access_page?(
user_with_member,
"/members/#{member.id}/show/edit"
)
conn =
conn_with_user("/members/#{member.id}/show/edit", user_with_member)
|> CheckPagePermission.call([])
refute conn.halted
end
end
describe "read_only and normal_user denied on admin routes" do
test "read_only cannot access /admin/roles" do
user = Fixtures.user_with_role_fixture("read_only")
conn = conn_with_user("/admin/roles", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "normal_user cannot access /admin/roles" do
user = Fixtures.user_with_role_fixture("normal_user")
conn = conn_with_user("/admin/roles", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "read_only cannot access /members/new" do
user = Fixtures.user_with_role_fixture("read_only")
conn = conn_with_user("/members/new", user) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
end
describe "wildcard" do
test "admin with \"*\" permission can access any page" do
user = Fixtures.user_with_role_fixture("admin")
conn = conn_with_user("/admin/roles", user) |> CheckPagePermission.call([])
refute conn.halted
end
test "admin can access \"/members/999/edit\"" do
user = Fixtures.user_with_role_fixture("admin")
conn = conn_with_user("/members/999/edit", user) |> CheckPagePermission.call([])
refute conn.halted
end
end
describe "unauthenticated user" do
test "nil current_user is denied and redirected to \"/sign-in\"" do
conn = conn_without_user("/members") |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/sign-in"
assert Phoenix.Flash.get(conn.assigns[:flash] || %{}, :error) ==
"You don't have permission to access this page."
end
end
describe "public paths" do
test "unauthenticated user can access /auth/sign-in (no redirect)" do
conn = conn_without_user("/auth/sign-in") |> CheckPagePermission.call([])
refute conn.halted
end
test "unauthenticated user can access /register" do
conn = conn_without_user("/register") |> 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")
user_without_role = %{user | role: nil}
conn = conn_with_user("/members", user_without_role) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
test "user with invalid permission_set_name is denied" do
user = Fixtures.user_with_role_fixture("admin")
bad_role = %{user.role | permission_set_name: "invalid_set"}
user_bad_role = %{user | role: bad_role}
conn = conn_with_user("/members", user_bad_role) |> CheckPagePermission.call([])
assert conn.halted
assert redirected_to(conn) == "/users/#{user.id}"
end
end
# Integration: dispatch through full router (endpoint) so pipeline and load_from_session run.
# These tests ensure a Mitglied (own_data) user is denied on every forbidden path.
describe "integration: Mitglied (own_data) denied on all forbidden paths via full router" do
@tag role: :member
test "GET /members redirects to user profile with error flash", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/members")
assert redirected_to(conn) == "/users/#{user.id}"
assert Phoenix.Flash.get(conn.assigns[:flash] || %{}, :error) =~
"don't have permission"
end
@tag role: :member
test "GET /members/new redirects to user profile", %{conn: conn, current_user: user} do
assert user.role.permission_set_name == "own_data",
"setup must provide Mitglied (own_data) user"
conn = get(conn, "/members/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /users redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/users")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /users/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/users/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /settings redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/settings")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /membership_fee_settings redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_settings")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /membership_fee_types redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/membership_fee_types")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /membership_fee_types/new redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_types/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /groups redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/groups")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /groups/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/groups/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /admin/roles redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/admin/roles")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /admin/roles/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/admin/roles/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
end
# Dynamic routes need a valid path segment; use a real UUID from fixtures.
describe "integration: Mitglied denied on dynamic forbidden paths via full router" do
setup %{conn: conn, current_user: current_user} do
member = Mv.Fixtures.member_fixture()
role = Mv.Fixtures.role_fixture("admin")
{:ok, conn: conn, current_user: current_user, member_id: member.id, role_id: role.id}
end
@tag role: :member
test "GET /members/:id/edit redirects to user profile", %{
conn: conn,
member_id: id,
current_user: user
} do
conn = get(conn, "/members/#{id}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /members/:id/show/edit redirects to user profile", %{
conn: conn,
member_id: id,
current_user: user
} do
conn = get(conn, "/members/#{id}/show/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /members/:id (unlinked member show) redirects to user profile", %{
conn: conn,
member_id: id,
current_user: user
} do
conn = get(conn, "/members/#{id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /users/:id redirects to user profile", %{conn: conn, current_user: user} do
other_user = Mv.Fixtures.user_with_role_fixture("admin")
conn = get(conn, "/users/#{other_user.id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /users/:id/edit redirects to user profile", %{conn: conn, current_user: user} do
other_user = Mv.Fixtures.user_with_role_fixture("admin")
conn = get(conn, "/users/#{other_user.id}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /users/:id/show/edit redirects to user profile", %{conn: conn, current_user: user} do
other_user = Mv.Fixtures.user_with_role_fixture("admin")
conn = get(conn, "/users/#{other_user.id}/show/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /membership_fee_types/:id/edit redirects to user profile", %{
conn: conn,
current_user: user
} do
type =
Mv.MembershipFees.MembershipFeeType
|> Ash.Query.limit(1)
|> Ash.read!(actor: Mv.Helpers.SystemActor.get_system_actor())
|> List.first()
if type do
conn = get(conn, "/membership_fee_types/#{type.id}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
end
@tag role: :member
test "GET /groups/:slug redirects to user profile", %{conn: conn, current_user: user} do
group = Mv.Membership.Group |> Ash.Query.limit(1) |> Ash.read!() |> List.first()
if group,
do: assert(redirected_to(get(conn, "/groups/#{group.slug}")) == "/users/#{user.id}")
end
@tag role: :member
test "GET /admin/roles/:id redirects to user profile", %{
conn: conn,
role_id: id,
current_user: user
} do
conn = get(conn, "/admin/roles/#{id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /admin/roles/:id/edit redirects to user profile", %{
conn: conn,
role_id: id,
current_user: user
} do
conn = get(conn, "/admin/roles/#{id}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
end
describe "integration: Mitglied (own_data) can access allowed paths via full router" do
@tag role: :member
test "GET / redirects to user profile (root not allowed for own_data)", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /users/:id (own profile) returns 200", %{conn: conn, current_user: user} do
conn = get(conn, "/users/#{user.id}")
assert conn.status == 200
end
@tag role: :member
test "GET /users/:id/edit (own profile edit) returns 200", %{conn: conn, current_user: user} do
conn = get(conn, "/users/#{user.id}/edit")
assert conn.status == 200
end
@tag role: :member
test "GET /users/:id/show/edit (own profile show edit) returns 200", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/users/#{user.id}/show/edit")
assert conn.status == 200
end
# Full-router test: session may not preserve member_id; plug logic covered by unit test "own_data user with linked member can access /members/:id/edit (plug direct call)"
@tag role: :member
@tag :skip
test "GET /members/:id/edit (linked member edit) returns 200 when user has linked member", %{
conn: conn,
current_user: user
} do
member = Mv.Fixtures.member_fixture()
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, user_after_update} =
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.force_set_argument(:member, %{id: member.id})
|> Ash.update(actor: system_actor)
user_with_member =
user_after_update
|> Ash.load!([:role], domain: Mv.Accounts)
|> Mv.Authorization.Actor.ensure_loaded()
|> Map.put(:member_id, member.id)
conn = conn_with_password_user(conn, user_with_member)
conn = get(conn, "/members/#{member.id}/edit")
assert conn.status == 200
end
@tag role: :member
@tag :skip
test "GET /members/:id/show/edit (linked member show edit) returns 200 when user has linked member",
%{
conn: conn,
current_user: user
} do
member = Mv.Fixtures.member_fixture()
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, user_after_update} =
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.force_set_argument(:member, %{id: member.id})
|> Ash.update(actor: system_actor)
user_with_member =
user_after_update
|> Ash.load!([:role], domain: Mv.Accounts)
|> Mv.Authorization.Actor.ensure_loaded()
|> Map.put(:member_id, member.id)
conn = conn_with_password_user(conn, user_with_member)
conn = get(conn, "/members/#{member.id}/show/edit")
assert conn.status == 200
end
# Skipped: MemberLive.Show requires membership fee cycle data; plug allows access (page loads then LiveView may error).
@tag role: :member
@tag :skip
test "GET /members/:id for linked member returns 200", %{conn: conn, current_user: user} do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
member = Mv.Fixtures.member_fixture()
user =
user
|> Ash.Changeset.for_update(:update_user, %{})
|> Ash.Changeset.force_set_argument(:member, %{id: member.id})
|> Ash.update(actor: system_actor)
|> case do
{:ok, u} -> Ash.load!(u, :role, domain: Mv.Accounts, actor: system_actor)
{:error, _} -> user
end
conn =
conn
|> MvWeb.ConnCase.conn_with_password_user(user)
|> get("/members/#{member.id}")
assert conn.status == 200
end
end
# read_only (Vorstand/Buchhaltung): allowed /, /members, /members/:id, /groups, /groups/:slug
describe "integration: read_only (Vorstand/Buchhaltung) allowed paths via full router" do
setup %{conn: conn, current_user: current_user} do
member = Mv.Fixtures.member_fixture()
group = Mv.Fixtures.group_fixture()
{:ok, conn: conn, current_user: current_user, member_id: member.id, group_slug: group.slug}
end
@tag role: :read_only
test "GET / returns 200", %{conn: conn} do
conn = get(conn, "/")
assert conn.status == 200
end
@tag role: :read_only
test "GET /members returns 200", %{conn: conn} do
conn = get(conn, "/members")
assert conn.status == 200
end
@tag role: :read_only
test "GET /members/:id returns 200", %{conn: conn, member_id: id} do
conn = get(conn, "/members/#{id}")
assert conn.status == 200
end
@tag role: :read_only
test "GET /groups returns 200", %{conn: conn} do
conn = get(conn, "/groups")
assert conn.status == 200
end
@tag role: :read_only
test "GET /groups/:slug returns 200", %{conn: conn, group_slug: slug} do
conn = get(conn, "/groups/#{slug}")
assert conn.status == 200
end
end
describe "integration: read_only denied paths via full router" do
setup %{conn: conn, current_user: current_user} do
member = Mv.Fixtures.member_fixture()
role = Mv.Fixtures.role_fixture("admin")
group = Mv.Fixtures.group_fixture()
type =
Mv.MembershipFees.MembershipFeeType
|> Ash.Query.limit(1)
|> Ash.read!(actor: Mv.Helpers.SystemActor.get_system_actor())
|> List.first()
{:ok,
conn: conn,
current_user: current_user,
member_id: member.id,
role_id: role.id,
group_slug: group.slug,
fee_type_id: type && type.id}
end
@tag role: :read_only
test "GET /members/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/members/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /members/:id/edit redirects to user profile", %{
conn: conn,
member_id: id,
current_user: user
} do
conn = get(conn, "/members/#{id}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /users redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/users")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /users/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/users/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /settings redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/settings")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /membership_fee_settings redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_settings")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /membership_fee_types redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_types")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /groups/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/groups/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /groups/:slug/edit redirects to user profile", %{
conn: conn,
current_user: user,
group_slug: slug
} do
conn = get(conn, "/groups/#{slug}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /admin/roles redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/admin/roles")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only
test "GET /admin/roles/:id redirects to user profile", %{
conn: conn,
role_id: id,
current_user: user
} do
conn = get(conn, "/admin/roles/#{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
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()
{:ok, conn: conn, current_user: current_user, member_id: member.id, group_slug: group.slug}
end
@tag role: :normal_user
test "GET / returns 200", %{conn: conn} do
conn = get(conn, "/")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /members returns 200", %{conn: conn} do
conn = get(conn, "/members")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /members/new returns 200", %{conn: conn} do
conn = get(conn, "/members/new")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /members/:id returns 200", %{conn: conn, member_id: id} do
conn = get(conn, "/members/#{id}")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /members/:id/edit returns 200", %{conn: conn, member_id: id} do
conn = get(conn, "/members/#{id}/edit")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /groups returns 200", %{conn: conn} do
conn = get(conn, "/groups")
assert conn.status == 200
end
@tag role: :normal_user
test "GET /groups/:slug returns 200", %{conn: conn, group_slug: slug} do
conn = get(conn, "/groups/#{slug}")
assert conn.status == 200
end
end
describe "integration: normal_user denied paths via full router" do
setup %{conn: conn, current_user: current_user} do
other_user = Mv.Fixtures.user_with_role_fixture("admin")
role = Mv.Fixtures.role_fixture("admin")
group = Mv.Fixtures.group_fixture()
{:ok,
conn: conn,
current_user: current_user,
other_user_id: other_user.id,
role_id: role.id,
group_slug: group.slug}
end
@tag role: :normal_user
test "GET /users redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/users")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /users/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/users/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /users/:id redirects to user profile", %{
conn: conn,
current_user: user,
other_user_id: id
} do
conn = get(conn, "/users/#{id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /settings redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/settings")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /membership_fee_settings redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_settings")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /membership_fee_types redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_types")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /groups/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/groups/new")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /groups/:slug/edit redirects to user profile", %{
conn: conn,
current_user: user,
group_slug: slug
} do
conn = get(conn, "/groups/#{slug}/edit")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /admin/roles redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/admin/roles")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user
test "GET /admin/roles/:id redirects to user profile", %{
conn: conn,
role_id: id,
current_user: user
} do
conn = get(conn, "/admin/roles/#{id}")
assert redirected_to(conn) == "/users/#{user.id}"
end
end
describe "integration: admin can access all protected routes via full router" do
setup %{conn: conn, current_user: current_user} do
member = Mv.Fixtures.member_fixture()
role = Mv.Fixtures.role_fixture("admin")
group = Mv.Fixtures.group_fixture()
{:ok,
conn: conn,
current_user: current_user,
member_id: member.id,
role_id: role.id,
group_slug: group.slug}
end
@tag role: :admin
test "GET / returns 200", %{conn: conn} do
conn = get(conn, "/")
assert conn.status == 200
end
@tag role: :admin
test "GET /members returns 200", %{conn: conn} do
conn = get(conn, "/members")
assert conn.status == 200
end
@tag role: :admin
test "GET /users returns 200", %{conn: conn} do
conn = get(conn, "/users")
assert conn.status == 200
end
@tag role: :admin
test "GET /settings returns 200", %{conn: conn} do
conn = get(conn, "/settings")
assert conn.status == 200
end
@tag role: :admin
test "GET /membership_fee_settings returns 200", %{conn: conn} do
conn = get(conn, "/membership_fee_settings")
assert conn.status == 200
end
@tag role: :admin
test "GET /admin/roles returns 200", %{conn: conn} do
conn = get(conn, "/admin/roles")
assert conn.status == 200
end
@tag role: :admin
test "GET /members/:id returns 200", %{conn: conn, member_id: id} do
conn = get(conn, "/members/#{id}")
assert conn.status == 200
end
@tag role: :admin
test "GET /admin/roles/:id returns 200", %{conn: conn, role_id: id} do
conn = get(conn, "/admin/roles/#{id}")
assert conn.status == 200
end
@tag role: :admin
test "GET /groups/:slug returns 200", %{conn: conn, group_slug: slug} do
conn = get(conn, "/groups/#{slug}")
assert conn.status == 200
end
end
end

View file

@ -175,6 +175,18 @@ defmodule MvWeb.ConnCase do
authenticated_conn = conn_with_password_user(conn, member_user)
{authenticated_conn, member_user}
:read_only ->
# Vorstand/Buchhaltung: can read members, groups; cannot edit or access admin/settings
read_only_user = Mv.Fixtures.user_with_role_fixture("read_only")
authenticated_conn = conn_with_password_user(conn, read_only_user)
{authenticated_conn, read_only_user}
:normal_user ->
# Kassenwart: can read/update members, groups; cannot access users/settings/admin
normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
authenticated_conn = conn_with_password_user(conn, normal_user)
{authenticated_conn, normal_user}
:unauthenticated ->
# No authentication for unauthenticated tests
{conn, nil}