Merge pull request 'Apply UI Authorization to Existing LiveViews closes #400' (#403) from feature/400_ui_authorization into main
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #403
This commit is contained in:
moritz 2026-02-03 17:30:15 +01:00
commit c2bafe4acf
12 changed files with 488 additions and 59 deletions

View file

@ -97,12 +97,18 @@ defmodule MvWeb.Authorization do
@doc """ @doc """
Checks if user can access a specific page. Checks if user can access a specific page.
Nil-safe: returns false when user is nil (e.g. unauthenticated or layout
assigns regression), so callers do not need to guard.
## Examples ## Examples
iex> admin = %{role: %{permission_set_name: "admin"}} iex> admin = %{role: %{permission_set_name: "admin"}}
iex> can_access_page?(admin, "/admin/roles") iex> can_access_page?(admin, "/admin/roles")
true true
iex> can_access_page?(nil, "/members")
false
iex> mitglied = %{role: %{permission_set_name: "own_data"}} iex> mitglied = %{role: %{permission_set_name: "own_data"}}
iex> can_access_page?(mitglied, "/members") iex> can_access_page?(mitglied, "/members")
false false

View file

@ -97,12 +97,13 @@ defmodule MvWeb.CoreComponents do
<.button navigate={~p"/"}>Home</.button> <.button navigate={~p"/"}>Home</.button>
<.button disabled={true}>Disabled</.button> <.button disabled={true}>Disabled</.button>
""" """
attr :rest, :global, include: ~w(href navigate patch method) attr :rest, :global, include: ~w(href navigate patch method data-testid)
attr :variant, :string, values: ~w(primary) attr :variant, :string, values: ~w(primary)
attr :disabled, :boolean, default: false, doc: "Whether the button is disabled" attr :disabled, :boolean, default: false, doc: "Whether the button is disabled"
slot :inner_block, required: true slot :inner_block, required: true
def button(%{rest: rest} = assigns) do def button(assigns) do
rest = assigns.rest
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"} variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant])) assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant]))

View file

@ -4,6 +4,8 @@ defmodule MvWeb.Layouts.Sidebar do
""" """
use MvWeb, :html use MvWeb, :html
alias MvWeb.PagePaths
attr :current_user, :map, default: nil, doc: "The current user" attr :current_user, :map, default: nil, doc: "The current user"
attr :club_name, :string, required: true, doc: "The name of the club" attr :club_name, :string, required: true, doc: "The name of the club"
attr :mobile, :boolean, default: false, doc: "Whether this is mobile view" attr :mobile, :boolean, default: false, doc: "Whether this is mobile view"
@ -70,33 +72,56 @@ defmodule MvWeb.Layouts.Sidebar do
defp sidebar_menu(assigns) do defp sidebar_menu(assigns) do
~H""" ~H"""
<ul class="menu flex-1 w-full p-2" role="menubar"> <ul class="menu flex-1 w-full p-2" role="menubar">
<.menu_item <%= if can_access_page?(@current_user, PagePaths.members()) do %>
href={~p"/members"} <.menu_item
icon="hero-users" href={~p"/members"}
label={gettext("Members")} icon="hero-users"
/> label={gettext("Members")}
<.menu_item
href={~p"/membership_fee_types"}
icon="hero-currency-euro"
label={gettext("Fee Types")}
/>
<!-- Nested Admin Menu -->
<.menu_group icon="hero-cog-6-tooth" label={gettext("Administration")}>
<.menu_subitem href={~p"/users"} label={gettext("Users")} />
<.menu_subitem href={~p"/groups"} label={gettext("Groups")} />
<.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
<.menu_subitem
href={~p"/membership_fee_settings"}
label={gettext("Fee Settings")}
/> />
<.menu_subitem href={~p"/settings"} label={gettext("Settings")} /> <% end %>
</.menu_group>
<%= if can_access_page?(@current_user, PagePaths.membership_fee_types()) do %>
<.menu_item
href={~p"/membership_fee_types"}
icon="hero-currency-euro"
label={gettext("Fee Types")}
/>
<% end %>
<%= if admin_menu_visible?(@current_user) do %>
<.menu_group
icon="hero-cog-6-tooth"
label={gettext("Administration")}
testid="sidebar-administration"
>
<%= if can_access_page?(@current_user, PagePaths.users()) do %>
<.menu_subitem href={~p"/users"} label={gettext("Users")} />
<% end %>
<%= if can_access_page?(@current_user, PagePaths.groups()) do %>
<.menu_subitem href={~p"/groups"} label={gettext("Groups")} />
<% end %>
<%= if can_access_page?(@current_user, PagePaths.admin_roles()) do %>
<.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
<% end %>
<%= if can_access_page?(@current_user, PagePaths.membership_fee_settings()) do %>
<.menu_subitem
href={~p"/membership_fee_settings"}
label={gettext("Fee Settings")}
/>
<% end %>
<%= if can_access_page?(@current_user, PagePaths.settings()) do %>
<.menu_subitem href={~p"/settings"} label={gettext("Settings")} />
<% end %>
</.menu_group>
<% end %>
</ul> </ul>
""" """
end end
defp admin_menu_visible?(user) do
Enum.any?(PagePaths.admin_menu_paths(), &can_access_page?(user, &1))
end
attr :href, :string, required: true, doc: "Navigation path" attr :href, :string, required: true, doc: "Navigation path"
attr :icon, :string, required: true, doc: "Heroicon name" attr :icon, :string, required: true, doc: "Heroicon name"
attr :label, :string, required: true, doc: "Menu item label" attr :label, :string, required: true, doc: "Menu item label"
@ -119,12 +144,13 @@ defmodule MvWeb.Layouts.Sidebar do
attr :icon, :string, required: true, doc: "Heroicon name for the menu group" attr :icon, :string, required: true, doc: "Heroicon name for the menu group"
attr :label, :string, required: true, doc: "Menu group label" attr :label, :string, required: true, doc: "Menu group label"
attr :testid, :string, default: nil, doc: "data-testid for stable test selectors"
slot :inner_block, required: true, doc: "Submenu items" slot :inner_block, required: true, doc: "Submenu items"
defp menu_group(assigns) do defp menu_group(assigns) do
~H""" ~H"""
<!-- Expanded Mode: Always open div structure --> <!-- Expanded Mode: Always open div structure -->
<li role="none" class="expanded-menu-group"> <li role="none" class="expanded-menu-group" data-testid={@testid}>
<div <div
class="flex items-center gap-3" class="flex items-center gap-3"
role="group" role="group"
@ -138,7 +164,7 @@ defmodule MvWeb.Layouts.Sidebar do
</ul> </ul>
</li> </li>
<!-- Collapsed Mode: Dropdown --> <!-- Collapsed Mode: Dropdown -->
<div class="collapsed-menu-group dropdown dropdown-right"> <div class="collapsed-menu-group dropdown dropdown-right" data-testid={@testid}>
<button <button
type="button" type="button"
tabindex="0" tabindex="0"

View file

@ -23,9 +23,11 @@
<.icon name="hero-envelope" /> <.icon name="hero-envelope" />
{gettext("Open in email program")} {gettext("Open in email program")}
</.button> </.button>
<.button variant="primary" navigate={~p"/members/new"}> <%= if can?(@current_user, :create, Mv.Membership.Member) do %>
<.icon name="hero-plus" /> {gettext("New Member")} <.button variant="primary" navigate={~p"/members/new"} data-testid="member-new">
</.button> <.icon name="hero-plus" /> {gettext("New Member")}
</.button>
<% end %>
</:actions> </:actions>
</.header> </.header>
@ -84,6 +86,7 @@
<.table <.table
id="members" id="members"
rows={@members} rows={@members}
row_id={fn member -> "row-#{member.id}" end}
row_click={fn member -> JS.navigate(~p"/members/#{member}") end} row_click={fn member -> JS.navigate(~p"/members/#{member}") end}
dynamic_cols={@dynamic_cols} dynamic_cols={@dynamic_cols}
sort_field={@sort_field} sort_field={@sort_field}
@ -297,16 +300,23 @@
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link> <.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
</div> </div>
<.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")}</.link> <%= if can?(@current_user, :update, member) do %>
<.link navigate={~p"/members/#{member}/edit"} data-testid="member-edit">
{gettext("Edit")}
</.link>
<% end %>
</:action> </:action>
<:action :let={member}> <:action :let={member}>
<.link <%= if can?(@current_user, :destroy, member) do %>
phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")} <.link
data-confirm={gettext("Are you sure?")} phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")}
> data-confirm={gettext("Are you sure?")}
{gettext("Delete")} data-testid="member-delete"
</.link> >
{gettext("Delete")}
</.link>
<% end %>
</:action> </:action>
</.table> </.table>
</Layouts.app> </Layouts.app>

View file

@ -39,9 +39,15 @@ defmodule MvWeb.MemberLive.Show do
{MvWeb.Helpers.MemberHelpers.display_name(@member)} {MvWeb.Helpers.MemberHelpers.display_name(@member)}
</h1> </h1>
<.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}> <%= if can?(@current_user, :update, @member) do %>
{gettext("Edit Member")} <.button
</.button> variant="primary"
navigate={~p"/members/#{@member}/edit?return_to=show"}
data-testid="member-edit"
>
{gettext("Edit Member")}
</.button>
<% end %>
</div> </div>
<%!-- Tab Navigation --%> <%!-- Tab Navigation --%>

View file

@ -2,13 +2,20 @@
<.header> <.header>
{gettext("Listing Users")} {gettext("Listing Users")}
<:actions> <:actions>
<.button variant="primary" navigate={~p"/users/new"}> <%= if can?(@current_user, :create, Mv.Accounts.User) do %>
<.icon name="hero-plus" /> {gettext("New User")} <.button variant="primary" navigate={~p"/users/new"} data-testid="user-new">
</.button> <.icon name="hero-plus" /> {gettext("New User")}
</.button>
<% end %>
</:actions> </:actions>
</.header> </.header>
<.table id="users" rows={@users} row_click={fn user -> JS.navigate(~p"/users/#{user}") end}> <.table
id="users"
rows={@users}
row_id={fn user -> "row-#{user.id}" end}
row_click={fn user -> JS.navigate(~p"/users/#{user}") end}
>
<:col <:col
:let={user} :let={user}
label={ label={
@ -62,16 +69,23 @@
<.link navigate={~p"/users/#{user}"}>{gettext("Show")}</.link> <.link navigate={~p"/users/#{user}"}>{gettext("Show")}</.link>
</div> </div>
<.link navigate={~p"/users/#{user}/edit"}>{gettext("Edit")}</.link> <%= if can?(@current_user, :update, user) do %>
<.link navigate={~p"/users/#{user}/edit"} data-testid="user-edit">
{gettext("Edit")}
</.link>
<% end %>
</:action> </:action>
<:action :let={user}> <:action :let={user}>
<.link <%= if can?(@current_user, :destroy, user) do %>
phx-click={JS.push("delete", value: %{id: user.id}) |> hide("#row-#{user.id}")} <.link
data-confirm={gettext("Are you sure?")} phx-click={JS.push("delete", value: %{id: user.id}) |> hide("#row-#{user.id}")}
> data-confirm={gettext("Are you sure?")}
{gettext("Delete")} data-testid="user-delete"
</.link> >
{gettext("Delete")}
</.link>
<% end %>
</:action> </:action>
</.table> </.table>
</Layouts.app> </Layouts.app>

View file

@ -41,9 +41,15 @@ defmodule MvWeb.UserLive.Show do
<.icon name="hero-arrow-left" /> <.icon name="hero-arrow-left" />
<span class="sr-only">{gettext("Back to users list")}</span> <span class="sr-only">{gettext("Back to users list")}</span>
</.button> </.button>
<.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}> <%= if can?(@current_user, :update, @user) do %>
<.icon name="hero-pencil-square" /> {gettext("Edit User")} <.button
</.button> variant="primary"
navigate={~p"/users/#{@user}/edit?return_to=show"}
data-testid="user-edit"
>
<.icon name="hero-pencil-square" /> {gettext("Edit User")}
</.button>
<% end %>
</:actions> </:actions>
</.header> </.header>

42
lib/mv_web/page_paths.ex Normal file
View file

@ -0,0 +1,42 @@
defmodule MvWeb.PagePaths do
@moduledoc """
Central path strings for UI authorization and sidebar menu.
Keep in sync with `MvWeb.Router`. Used by Sidebar and `can_access_page?/2`
so route changes (prefix, rename) are updated in one place.
"""
# Sidebar top-level menu paths
@members "/members"
@membership_fee_types "/membership_fee_types"
# Administration submenu paths (all must match router)
@users "/users"
@groups "/groups"
@admin_roles "/admin/roles"
@membership_fee_settings "/membership_fee_settings"
@settings "/settings"
@admin_page_paths [
@users,
@groups,
@admin_roles,
@membership_fee_settings,
@settings
]
@doc "Path for Members index (sidebar and page permission check)."
def members, do: @members
@doc "Path for Membership Fee Types index (sidebar and page permission check)."
def membership_fee_types, do: @membership_fee_types
@doc "Paths for Administration menu; show group if user can access any of these."
def admin_menu_paths, do: @admin_page_paths
def users, do: @users
def groups, do: @groups
def admin_roles, do: @admin_roles
def membership_fee_settings, do: @membership_fee_settings
def settings, do: @settings
end

View file

@ -22,9 +22,14 @@ defmodule MvWeb.Layouts.SidebarTest do
# ============================================================================= # =============================================================================
# Returns assigns for an authenticated user with all required attributes. # Returns assigns for an authenticated user with all required attributes.
# User has admin role so can_access_page? returns true for all sidebar links.
defp authenticated_assigns(mobile \\ false) do defp authenticated_assigns(mobile \\ false) do
%{ %{
current_user: %{id: "user-123", email: "test@example.com"}, current_user: %{
id: "user-123",
email: "test@example.com",
role: %{permission_set_name: "admin"}
},
club_name: "Test Club", club_name: "Test Club",
mobile: mobile mobile: mobile
} }
@ -144,7 +149,9 @@ defmodule MvWeb.Layouts.SidebarTest do
assert menu_item_count > 0, "Should have at least one top-level menu item" assert menu_item_count > 0, "Should have at least one top-level menu item"
# Check that nested menu groups exist # Check that nested menu groups exist
assert html =~ ~s(<li role="none" class="expanded-menu-group">) assert html =~
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
assert html =~ ~s(role="group") assert html =~ ~s(role="group")
assert has_class?(html, "expanded-menu-group") assert has_class?(html, "expanded-menu-group")
@ -193,7 +200,9 @@ defmodule MvWeb.Layouts.SidebarTest do
html = render_sidebar(authenticated_assigns()) html = render_sidebar(authenticated_assigns())
# Check for nested menu structure # Check for nested menu structure
assert html =~ ~s(<li role="none" class="expanded-menu-group">) assert html =~
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
assert html =~ ~s(role="group") assert html =~ ~s(role="group")
assert html =~ ~s(aria-label="Administration") assert html =~ ~s(aria-label="Administration")
assert has_class?(html, "expanded-menu-group") assert has_class?(html, "expanded-menu-group")
@ -521,7 +530,9 @@ defmodule MvWeb.Layouts.SidebarTest do
assert html =~ ~s(role="menuitem") assert html =~ ~s(role="menuitem")
# Check that nested menus exist # Check that nested menus exist
assert html =~ ~s(<li role="none" class="expanded-menu-group">) assert html =~
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
assert html =~ ~s(role="group") assert html =~ ~s(role="group")
# Footer section # Footer section
@ -629,7 +640,9 @@ defmodule MvWeb.Layouts.SidebarTest do
html = render_sidebar(authenticated_assigns()) html = render_sidebar(authenticated_assigns())
# expanded-menu-group structure present # expanded-menu-group structure present
assert html =~ ~s(<li role="none" class="expanded-menu-group">) assert html =~
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
assert html =~ ~s(role="group") assert html =~ ~s(role="group")
assert html =~ ~s(aria-label="Administration") assert html =~ ~s(aria-label="Administration")
assert has_class?(html, "expanded-menu-group") assert has_class?(html, "expanded-menu-group")
@ -843,7 +856,9 @@ defmodule MvWeb.Layouts.SidebarTest do
# Expanded menu group should have correct structure # Expanded menu group should have correct structure
# (CSS handles hover effects, but we verify structure) # (CSS handles hover effects, but we verify structure)
assert html =~ ~s(<li role="none" class="expanded-menu-group">) assert html =~
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
assert html =~ ~s(role="group") assert html =~ ~s(role="group")
end end

View file

@ -0,0 +1,120 @@
defmodule MvWeb.SidebarAuthorizationTest do
@moduledoc """
Tests for sidebar menu visibility based on user permissions (can_access_page?).
"""
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import MvWeb.Layouts.Sidebar
alias Mv.Fixtures
defp render_sidebar(assigns) do
render_component(&sidebar/1, assigns)
end
defp sidebar_assigns(current_user, opts \\ []) do
mobile = Keyword.get(opts, :mobile, false)
club_name = Keyword.get(opts, :club_name, "Test Club")
%{
current_user: current_user,
club_name: club_name,
mobile: mobile
}
end
describe "sidebar menu with admin user" do
test "shows Members, Fee Types and Administration with all subitems" do
user = Fixtures.user_with_role_fixture("admin")
html = render_sidebar(sidebar_assigns(user))
assert html =~ ~s(href="/members")
assert html =~ ~s(href="/membership_fee_types")
assert html =~ ~s(data-testid="sidebar-administration")
assert html =~ ~s(href="/users")
assert html =~ ~s(href="/groups")
assert html =~ ~s(href="/admin/roles")
assert html =~ ~s(href="/membership_fee_settings")
assert html =~ ~s(href="/settings")
end
end
describe "sidebar menu with read_only user (Vorstand/Buchhaltung)" do
test "shows Members and Groups (from Administration)" do
user = Fixtures.user_with_role_fixture("read_only")
html = render_sidebar(sidebar_assigns(user))
assert html =~ ~s(href="/members")
assert html =~ ~s(href="/groups")
end
test "does not show Fee Types, Users, Roles or Settings" do
user = Fixtures.user_with_role_fixture("read_only")
html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/membership_fee_types")
refute html =~ ~s(href="/users")
refute html =~ ~s(href="/admin/roles")
refute html =~ ~s(href="/settings")
end
end
describe "sidebar menu with normal_user (Kassenwart)" do
test "shows Members and Groups" do
user = Fixtures.user_with_role_fixture("normal_user")
html = render_sidebar(sidebar_assigns(user))
assert html =~ ~s(href="/members")
assert html =~ ~s(href="/groups")
end
test "does not show Fee Types, Users, Roles or Settings" do
user = Fixtures.user_with_role_fixture("normal_user")
html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/membership_fee_types")
refute html =~ ~s(href="/users")
refute html =~ ~s(href="/admin/roles")
refute html =~ ~s(href="/settings")
end
end
describe "sidebar menu with own_data user (Mitglied)" do
test "does not show Members link (no /members page access)" do
user = Fixtures.user_with_role_fixture("own_data")
html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/members")
end
test "does not show Fee Types or Administration" do
user = Fixtures.user_with_role_fixture("own_data")
html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/membership_fee_types")
refute html =~ ~s(href="/users")
refute html =~ ~s(data-testid="sidebar-administration")
end
end
describe "sidebar with nil current_user" do
test "does not render menu items (only header and footer when present)" do
html = render_sidebar(sidebar_assigns(nil))
refute html =~ ~s(role="menubar")
refute html =~ ~s(href="/members")
end
end
describe "sidebar with user without role" do
test "does not show any navigation links" do
user = %{id: "user-no-role", email: "noreply@test.com", role: nil}
html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/members")
refute html =~ ~s(href="/membership_fee_types")
refute html =~ ~s(href="/users")
end
end
end

View file

@ -0,0 +1,102 @@
defmodule MvWeb.MemberLiveAuthorizationTest do
@moduledoc """
Tests for UI authorization on Member LiveViews (Index and Show).
"""
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
alias Mv.Fixtures
describe "Member Index - Vorstand (read_only)" do
@tag role: :read_only
test "sees member list but not New Member button", %{conn: conn} do
_member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members")
refute has_element?(view, "[data-testid=member-new]")
end
@tag role: :read_only
test "does not see Edit or Delete buttons in table", %{conn: conn} do
member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members")
refute has_element?(view, "#row-#{member.id} [data-testid=member-edit]")
refute has_element?(view, "#row-#{member.id} [data-testid=member-delete]")
end
end
describe "Member Index - Kassenwart (normal_user)" do
@tag role: :normal_user
test "sees New Member and Edit buttons", %{conn: conn} do
member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members")
assert has_element?(view, "[data-testid=member-new]")
assert has_element?(view, "#row-#{member.id} [data-testid=member-edit]")
end
@tag role: :normal_user
test "does not see Delete button", %{conn: conn} do
member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members")
refute has_element?(view, "#row-#{member.id} [data-testid=member-delete]")
end
end
describe "Member Index - Admin" do
@tag role: :admin
test "sees New Member, Edit and Delete buttons", %{conn: conn} do
member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members")
assert has_element?(view, "[data-testid=member-new]")
assert has_element?(view, "#row-#{member.id} [data-testid=member-edit]")
assert has_element?(view, "#row-#{member.id} [data-testid=member-delete]")
end
end
describe "Member Index - Mitglied (own_data)" do
@tag role: :member
test "is redirected when accessing /members", %{conn: conn, current_user: user} do
assert {:error, {:redirect, %{to: to}}} = live(conn, "/members")
assert to == "/users/#{user.id}"
end
end
describe "Member Show - Edit button visibility" do
@tag role: :admin
test "admin sees Edit button", %{conn: conn} do
member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members/#{member.id}")
assert has_element?(view, "[data-testid=member-edit]")
end
@tag role: :read_only
test "read_only does not see Edit button", %{conn: conn} do
member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members/#{member.id}")
refute has_element?(view, "[data-testid=member-edit]")
end
@tag role: :normal_user
test "normal_user sees Edit button", %{conn: conn} do
member = Fixtures.member_fixture()
{:ok, view, _html} = live(conn, "/members/#{member.id}")
assert has_element?(view, "[data-testid=member-edit]")
end
end
end

View file

@ -0,0 +1,81 @@
defmodule MvWeb.UserLiveAuthorizationTest do
@moduledoc """
Tests for UI authorization on User LiveViews (Index and Show).
"""
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
alias Mv.Fixtures
describe "User Index - Admin" do
@tag role: :admin
test "sees New User, Edit and Delete buttons", %{conn: conn} do
user = Fixtures.user_with_role_fixture("admin")
{:ok, view, _html} = live(conn, "/users")
assert has_element?(view, "[data-testid=user-new]")
assert has_element?(view, "#row-#{user.id} [data-testid=user-edit]")
assert has_element?(view, "#row-#{user.id} [data-testid=user-delete]")
end
end
describe "User Index - Non-Admin is redirected" do
@tag role: :read_only
test "read_only is redirected when accessing /users", %{conn: conn, current_user: user} do
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users")
assert to == "/users/#{user.id}"
end
@tag role: :member
test "member is redirected when accessing /users", %{conn: conn, current_user: user} do
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users")
assert to == "/users/#{user.id}"
end
@tag role: :normal_user
test "normal_user is redirected when accessing /users", %{conn: conn, current_user: user} do
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users")
assert to == "/users/#{user.id}"
end
end
describe "User Show - own profile" do
@tag role: :member
test "member sees Edit button on own profile", %{conn: conn, current_user: user} do
{:ok, view, _html} = live(conn, "/users/#{user.id}")
assert has_element?(view, "[data-testid=user-edit]")
end
@tag role: :read_only
test "read_only sees Edit button on own profile", %{conn: conn, current_user: user} do
{:ok, view, _html} = live(conn, "/users/#{user.id}")
assert has_element?(view, "[data-testid=user-edit]")
end
@tag role: :admin
test "admin sees Edit button on user show", %{conn: conn} do
user = Fixtures.user_with_role_fixture("read_only")
{:ok, view, _html} = live(conn, "/users/#{user.id}")
assert has_element?(view, "[data-testid=user-edit]")
end
end
describe "User Show - other user (non-admin redirected)" do
@tag role: :member
test "member is redirected when accessing other user's profile", %{
conn: conn,
current_user: current_user
} do
other_user = Fixtures.user_with_role_fixture("admin")
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users/#{other_user.id}")
assert to == "/users/#{current_user.id}"
end
end
end