Merge pull request 'Link to userdate from profile button closes #170' (#173) from 170-userdata-for-profile-button into main
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #173 Reviewed-by: carla <carla@noreply.git.local-it.org> Reviewed-by: rafael <rafael@noreply.git.local-it.org>
This commit is contained in:
commit
ce9878791e
20 changed files with 336 additions and 27 deletions
|
|
@ -89,7 +89,7 @@
|
|||
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||
# set this value to 0 (zero).
|
||||
#
|
||||
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||
{Credo.Check.Design.TagTODO, [exit_status: 0]},
|
||||
|
||||
#
|
||||
## Readability Checks
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ defmodule MvWeb.Layouts do
|
|||
embed_templates "layouts/*"
|
||||
|
||||
@doc """
|
||||
Renders the app layout
|
||||
Renders the app layout. Can be used with or without a current_user.
|
||||
When current_user is present, it will show the navigation bar.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -22,9 +23,15 @@ defmodule MvWeb.Layouts do
|
|||
<h1>Content</h1>
|
||||
</Layout.app>
|
||||
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<h1>Authenticated Content</h1>
|
||||
</Layout.app>
|
||||
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
|
||||
attr :current_user, :map, default: nil, doc: "the current user, if authenticated"
|
||||
|
||||
attr :current_scope, :map,
|
||||
default: nil,
|
||||
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
|
||||
|
|
@ -33,7 +40,9 @@ defmodule MvWeb.Layouts do
|
|||
|
||||
def app(assigns) do
|
||||
~H"""
|
||||
<.navbar />
|
||||
<%= if @current_user do %>
|
||||
<.navbar current_user={@current_user} />
|
||||
<% end %>
|
||||
<main class="px-4 py-20 sm:px-6 lg:px-16">
|
||||
<div class="mx-auto max-full space-y-4">
|
||||
{render_slot(@inner_block)}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ defmodule MvWeb.Layouts.Navbar do
|
|||
"""
|
||||
use Phoenix.Component
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
use MvWeb, :verified_routes
|
||||
|
||||
attr :current_user, :map,
|
||||
required: true,
|
||||
doc: "The current user - navbar is only shown when user is present"
|
||||
|
||||
def navbar(assigns) do
|
||||
~H"""
|
||||
|
|
@ -65,12 +70,14 @@ defmodule MvWeb.Layouts.Navbar do
|
|||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
<li>
|
||||
<a>
|
||||
<.link navigate={~p"/users/#{@current_user.id}"}>
|
||||
{gettext("Profil")}
|
||||
</a>
|
||||
</.link>
|
||||
</li>
|
||||
<li><a>{gettext("Settings")}</a></li>
|
||||
<li><a href="sign-out">{gettext("Logout")}</a></li>
|
||||
<li>
|
||||
<.link href={~p"/sign-out"}>{gettext("Logout")}</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.MemberLive.Form do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{gettext("Members")}
|
||||
<:actions>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ defmodule MvWeb.MemberLive.Show do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{@member.first_name} {@member.last_name}
|
||||
<:subtitle>{gettext("This is a member record from your database.")}</:subtitle>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.PropertyLive.Form do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>{gettext("Use this form to manage property records in your database.")}</:subtitle>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.PropertyLive.Index do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Listing Properties
|
||||
<:actions>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.PropertyLive.Show do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Property {@property.id}
|
||||
<:subtitle>This is a property record from your database.</:subtitle>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.PropertyTypeLive.Form do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.PropertyTypeLive.Index do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Listing Property types
|
||||
<:actions>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.PropertyTypeLive.Show do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Property type {@property_type.id}
|
||||
<:subtitle>This is a property_type record from your database.</:subtitle>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.UserLive.Form do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{gettext("Listing Users")}
|
||||
<:actions>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule MvWeb.UserLive.Show do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{gettext("User")} {@user.email}
|
||||
<:subtitle>{gettext("This is a user record from your database.")}</:subtitle>
|
||||
|
|
|
|||
|
|
@ -28,11 +28,16 @@ defmodule MvWeb.LiveUserAuth do
|
|||
end
|
||||
end
|
||||
|
||||
def on_mount(:live_user_required, _params, _session, socket) do
|
||||
if socket.assigns[:current_user] do
|
||||
{:cont, socket}
|
||||
else
|
||||
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
|
||||
def on_mount(:live_user_required, _params, session, socket) do
|
||||
socket = AshAuthentication.Phoenix.LiveSession.assign_new_resources(socket, session)
|
||||
|
||||
case socket.assigns do
|
||||
%{current_user: %{} = user} ->
|
||||
{:cont, assign(socket, :current_user, user)}
|
||||
|
||||
_ ->
|
||||
socket = Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
88
test/mv_web/components/layouts/navbar_test.exs
Normal file
88
test/mv_web/components/layouts/navbar_test.exs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
defmodule MvWeb.Layouts.NavbarTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
describe "navbar profile section" do
|
||||
test "renders profile button with correct attributes", %{conn: _conn} do
|
||||
# Setup: Create a user
|
||||
user = create_test_user(%{email: "test@example.com"})
|
||||
|
||||
html =
|
||||
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
|
||||
current_user: user
|
||||
})
|
||||
|
||||
# Test dropdown structure
|
||||
assert html =~ "dropdown-content"
|
||||
assert html =~ "dropdown-end"
|
||||
assert html =~ ~s(role="button")
|
||||
|
||||
# Test profile link
|
||||
assert html =~ ~s(href="/users/#{user.id}")
|
||||
assert html =~ "Profil"
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
# TODO: Implement user initials in navbar avatar - see issue #170
|
||||
test "shows user initials in avatar", %{conn: _conn} do
|
||||
# Setup: Create a user with specific email for testing initials
|
||||
user = create_test_user(%{email: "test.user@example.com"})
|
||||
|
||||
html =
|
||||
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
|
||||
current_user: user
|
||||
})
|
||||
|
||||
# Initials from test.user@example.com
|
||||
assert html =~ "<span>TU</span>"
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
# TODO: Implement user initials in navbar avatar - see issue #170
|
||||
test "shows different initials for OIDC user", %{conn: _conn} do
|
||||
# Setup: Create OIDC user
|
||||
user_info = %{
|
||||
"sub" => "oidc_123",
|
||||
"preferred_username" => "oidc.user@example.com"
|
||||
}
|
||||
|
||||
oauth_tokens = %{
|
||||
"access_token" => "test_token",
|
||||
"id_token" => "test_id_token"
|
||||
}
|
||||
|
||||
user =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_rauthy, %{
|
||||
user_info: user_info,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create!(domain: Mv.Accounts)
|
||||
|
||||
html =
|
||||
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
|
||||
current_user: user
|
||||
})
|
||||
|
||||
# Initials from oidc.user@example.com
|
||||
assert html =~ "<span>OU</span>"
|
||||
end
|
||||
|
||||
test "includes all required navigation items", %{conn: _conn} do
|
||||
user = create_test_user(%{email: "test@example.com"})
|
||||
|
||||
html =
|
||||
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
|
||||
current_user: user
|
||||
})
|
||||
|
||||
# Check for all required menu items
|
||||
assert html =~ "Profil"
|
||||
assert html =~ "Settings"
|
||||
assert html =~ "Logout"
|
||||
|
||||
# Check for correct logout path
|
||||
assert html =~ ~s(href="/sign-out")
|
||||
end
|
||||
end
|
||||
end
|
||||
182
test/mv_web/live/profile_navigation_test.exs
Normal file
182
test/mv_web/live/profile_navigation_test.exs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
defmodule MvWeb.ProfileNavigationTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
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"})
|
||||
conn = conn_with_password_user(conn, user)
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Click the profile button
|
||||
view |> element("a", "Profil") |> render_click()
|
||||
|
||||
# Verify we're on the profile page
|
||||
assert_redirected(view, "/users/#{user.id}")
|
||||
end
|
||||
|
||||
test "profile navigation shows correct user data", %{conn: conn} do
|
||||
# Setup: Create and login a user
|
||||
user = create_test_user(%{email: "test@example.com"})
|
||||
conn = conn_with_password_user(conn, user)
|
||||
|
||||
# Navigate to profile
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
view |> element("a", "Profil") |> render_click()
|
||||
|
||||
# Verify profile data
|
||||
{:ok, _profile_view, html} = live(conn, "/users/#{user.id}")
|
||||
assert html =~ to_string(user.email)
|
||||
assert html =~ "Password Authentication"
|
||||
assert html =~ "Enabled"
|
||||
end
|
||||
end
|
||||
|
||||
describe "navbar" 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"})
|
||||
conn = conn_with_password_user(conn, user)
|
||||
{:ok, _view, html} = live(conn, "/")
|
||||
|
||||
assert html =~ ~s(role="button")
|
||||
assert html =~ "dropdown-content"
|
||||
assert html =~ "avatar"
|
||||
assert html =~ "Profil"
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
# TODO: Implement user initials in navbar avatar - see issue #170
|
||||
test "shows user initials in avatar", %{conn: conn} do
|
||||
# Setup: Create and login a user
|
||||
user = create_test_user(%{email: "test.user@example.com"})
|
||||
conn = conn_with_password_user(conn, user)
|
||||
{:ok, _view, html} = live(conn, "/")
|
||||
|
||||
# Initials from test.user@example.com
|
||||
assert html =~ "<span>TU</span>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "profile navigation with OIDC user" do
|
||||
test "shows correct profile data for OIDC user", %{conn: conn} do
|
||||
# Setup: Create OIDC user with sub claim
|
||||
user_info = %{
|
||||
"sub" => "oidc_123",
|
||||
"preferred_username" => "oidc.user@example.com"
|
||||
}
|
||||
|
||||
oauth_tokens = %{
|
||||
"access_token" => "test_token",
|
||||
"id_token" => "test_id_token"
|
||||
}
|
||||
|
||||
user =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_rauthy, %{
|
||||
user_info: user_info,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create!(domain: Mv.Accounts)
|
||||
|
||||
# Login user via OIDC
|
||||
conn = sign_in_user_via_oidc(conn, user)
|
||||
|
||||
# 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)
|
||||
# OIDC ID should be visible
|
||||
assert html =~ "oidc_123"
|
||||
# Password auth should be disabled for OIDC users
|
||||
assert html =~ "Not enabled"
|
||||
end
|
||||
|
||||
test "profile navigation works across different authentication methods", %{conn: conn} do
|
||||
# Create password user
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
email: "password2@example.com",
|
||||
password: "test_password123"
|
||||
})
|
||||
|
||||
# Create OIDC user
|
||||
user_info = %{
|
||||
"sub" => "oidc_789",
|
||||
"preferred_username" => "oidc@example.com"
|
||||
}
|
||||
|
||||
oauth_tokens = %{
|
||||
"access_token" => "test_token",
|
||||
"id_token" => "test_id_token"
|
||||
}
|
||||
|
||||
oidc_user =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_rauthy, %{
|
||||
user_info: user_info,
|
||||
oauth_tokens: oauth_tokens
|
||||
})
|
||||
|> Ash.create!(domain: Mv.Accounts)
|
||||
|
||||
# Test with password user
|
||||
conn_password = conn_with_password_user(conn, password_user)
|
||||
{:ok, view_password, _html} = live(conn_password, "/")
|
||||
view_password |> element("a", "Profil") |> render_click()
|
||||
assert_redirected(view_password, "/users/#{password_user.id}")
|
||||
|
||||
# Test with OIDC user
|
||||
conn_oidc = sign_in_user_via_oidc(conn, oidc_user)
|
||||
{:ok, view_oidc, _html} = live(conn_oidc, "/")
|
||||
view_oidc |> element("a", "Profil") |> render_click()
|
||||
assert_redirected(view_oidc, "/users/#{oidc_user.id}")
|
||||
end
|
||||
end
|
||||
|
||||
describe "authenticated views" do
|
||||
setup %{conn: conn} do
|
||||
user = create_test_user(%{email: "test@example.com"})
|
||||
conn = conn_with_password_user(conn, user)
|
||||
{:ok, conn: conn, user: user}
|
||||
end
|
||||
|
||||
@authenticated_paths [
|
||||
"/",
|
||||
"/members",
|
||||
"/members/new",
|
||||
"/properties",
|
||||
"/properties/new",
|
||||
"/property_types",
|
||||
"/property_types/new",
|
||||
"/users",
|
||||
"/users/new"
|
||||
]
|
||||
|
||||
for path <- @authenticated_paths do
|
||||
@path path
|
||||
test "layout shows user data on #{path}", %{conn: conn, user: user} do
|
||||
{:ok, _view, html} = live(conn, @path)
|
||||
# The navbar (which requires current_user) should be visible
|
||||
assert html =~ "navbar"
|
||||
# Profile button should be visible
|
||||
assert html =~ "Profil"
|
||||
# User ID should be in profile link
|
||||
assert html =~ ~p"/users/#{user.id}"
|
||||
end
|
||||
end
|
||||
|
||||
test "layout shows user data on user profile page", %{conn: conn, user: user} do
|
||||
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||
# The navbar (which requires current_user) should be visible
|
||||
assert html =~ "navbar"
|
||||
# Profile button should be visible
|
||||
assert html =~ "Profil"
|
||||
# User ID should be in profile link
|
||||
assert html =~ ~p"/users/#{user.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -288,7 +288,7 @@ defmodule MvWeb.UserLive.IndexTest do
|
|||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Select second user
|
||||
# Select second user
|
||||
html = view |> element("input[type='checkbox'][name='#{user2.id}']") |> render_click()
|
||||
|
||||
# Now select all should be automatically checked (all individual users are selected)
|
||||
|
|
@ -388,7 +388,8 @@ defmodule MvWeb.UserLive.IndexTest do
|
|||
assert html =~ "Email"
|
||||
assert html =~ "OIDC ID"
|
||||
# Should show the authenticated user at minimum
|
||||
assert html =~ "user@example.com"
|
||||
# Matches the generated email pattern oidc.user{unique_id}@example.com
|
||||
assert html =~ "oidc.user"
|
||||
end
|
||||
|
||||
test "handles users with missing OIDC ID", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -100,11 +100,28 @@ defmodule MvWeb.ConnCase do
|
|||
Signs in a user via OIDC and returns a connection with the user authenticated.
|
||||
By default creates a user with "user@example.com" for consistency.
|
||||
"""
|
||||
def conn_with_oidc_user(conn, user_attrs \\ %{email: "user@example.com"}) do
|
||||
user = create_test_user(user_attrs)
|
||||
def conn_with_oidc_user(conn, user_attrs \\ %{}) do
|
||||
# Ensure unique email for OIDC users
|
||||
unique_id = System.unique_integer([:positive])
|
||||
|
||||
default_attrs = %{
|
||||
email: "oidc.user#{unique_id}@example.com",
|
||||
oidc_id: "oidc_#{unique_id}"
|
||||
}
|
||||
|
||||
user = create_test_user(Map.merge(default_attrs, user_attrs))
|
||||
sign_in_user_via_oidc(conn, user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Signs in a user via password authentication and returns a connection with the user authenticated.
|
||||
"""
|
||||
def conn_with_password_user(conn, user) do
|
||||
conn
|
||||
|> Phoenix.ConnTest.init_test_session(%{})
|
||||
|> AshAuthentication.Plug.Helpers.store_in_session(user)
|
||||
end
|
||||
|
||||
setup tags do
|
||||
Mv.DataCase.setup_sandbox(tags)
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue