diff --git a/test/mv_web/live/role_live/show_test.exs b/test/mv_web/live/role_live/show_test.exs new file mode 100644 index 0000000..2c56347 --- /dev/null +++ b/test/mv_web/live/role_live/show_test.exs @@ -0,0 +1,274 @@ +defmodule MvWeb.RoleLive.ShowTest do + @moduledoc """ + Tests for the role show page. + + Tests cover: + - Displaying role information + - System role badge display + - User count display + - Navigation + - Error handling + - Delete functionality + """ + use MvWeb.ConnCase, async: false + import Phoenix.LiveViewTest + require Ash.Query + use Gettext, backend: MvWeb.Gettext + + alias Mv.Authorization + alias Mv.Authorization.Role + + # Helper to create a role + defp create_role(attrs \\ %{}) do + default_attrs = %{ + name: "Test Role #{System.unique_integer([:positive])}", + description: "Test description", + permission_set_name: "read_only" + } + + attrs = Map.merge(default_attrs, attrs) + + case Authorization.create_role(attrs) do + {:ok, role} -> role + {:error, error} -> raise "Failed to create role: #{inspect(error)}" + end + end + + # Helper to create admin user with admin role + defp create_admin_user(conn) do + # Create admin role + admin_role = + case Authorization.list_roles() do + {:ok, roles} -> + case Enum.find(roles, &(&1.name == "Admin")) do + nil -> + # Create admin role if it doesn't exist + create_role(%{ + name: "Admin", + description: "Administrator with full access", + permission_set_name: "admin" + }) + + role -> + role + end + + _ -> + # Create admin role if list_roles fails + create_role(%{ + name: "Admin", + description: "Administrator with full access", + permission_set_name: "admin" + }) + end + + # Create 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() + + # Assign admin role using manage_relationship + {:ok, user} = + user + |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) + |> Ash.update() + + # Load role for authorization checks (must be loaded for can?/3 to work) + user_with_role = Ash.load!(user, :role, domain: Mv.Accounts) + + # Store user with role in session for LiveView + conn = conn_with_password_user(conn, user_with_role) + {conn, user_with_role, admin_role} + end + + describe "mount and display" do + setup %{conn: conn} do + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} + end + + test "mounts successfully with valid role ID", %{conn: conn} do + role = create_role() + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + assert html =~ role.name + end + + test "displays role name", %{conn: conn} do + role = create_role(%{name: "Test Role Name"}) + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + assert html =~ "Test Role Name" + assert html =~ gettext("Name") + end + + test "displays role description when present", %{conn: conn} do + role = create_role(%{description: "This is a test description"}) + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + assert html =~ "This is a test description" + assert html =~ gettext("Description") + end + + test "displays 'No description' when description is missing", %{conn: conn} do + role = create_role(%{description: nil}) + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + assert html =~ gettext("No description") + end + + test "displays permission set name", %{conn: conn} do + role = create_role(%{permission_set_name: "read_only"}) + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + assert html =~ "read_only" + assert html =~ gettext("Permission Set") + end + + test "displays system role badge when is_system_role is true", %{conn: conn} do + system_role = + Role + |> Ash.Changeset.for_create(:create_role, %{ + name: "System Role", + permission_set_name: "own_data" + }) + |> Ash.Changeset.force_change_attribute(:is_system_role, true) + |> Ash.create!() + + {:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}") + + assert html =~ gettext("System Role") + assert html =~ gettext("Yes") + end + + test "displays non-system role badge when is_system_role is false", %{conn: conn} do + role = create_role() + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + assert html =~ gettext("System Role") + assert html =~ gettext("No") + end + + test "displays user count", %{conn: conn} do + role = create_role() + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + # User count should be displayed (might be 0 or more) + assert html =~ gettext("User") || html =~ "0" || html =~ "users" + end + end + + describe "navigation" do + setup %{conn: conn} do + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} + end + + test "back button navigates to role list", %{conn: conn} do + role = create_role() + + {:ok, view, _html} = live(conn, "/admin/roles/#{role.id}") + + assert {:error, {:live_redirect, %{to: to}}} = + view + |> element( + "a[aria-label='#{gettext("Back to roles list")}'], button[aria-label='#{gettext("Back to roles list")}']" + ) + |> render_click() + + assert to == "/admin/roles" + end + + test "edit button navigates to edit form", %{conn: conn} do + role = create_role() + + {:ok, view, _html} = live(conn, "/admin/roles/#{role.id}") + + assert {:error, {:live_redirect, %{to: to}}} = + view + |> element( + "a[href='/admin/roles/#{role.id}/edit'], button[href='/admin/roles/#{role.id}/edit']" + ) + |> render_click() + + assert to == "/admin/roles/#{role.id}/edit" + end + end + + describe "error handling" do + setup %{conn: conn} do + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} + end + + test "redirects to role list with error for invalid role ID", %{conn: conn} do + invalid_id = Ecto.UUID.generate() + + # Should redirect to index with error message + result = live(conn, "/admin/roles/#{invalid_id}") + + assert match?({:error, {:redirect, %{to: "/admin/roles"}}}, result) or + match?({:error, {:live_redirect, %{to: "/admin/roles"}}}, result) + end + end + + describe "delete functionality" do + setup %{conn: conn} do + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} + end + + test "delete button is not shown for system roles", %{conn: conn} do + system_role = + Role + |> Ash.Changeset.for_create(:create_role, %{ + name: "System Role", + permission_set_name: "own_data" + }) + |> Ash.Changeset.force_change_attribute(:is_system_role, true) + |> Ash.create!() + + {:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}") + + # Delete button should not be visible for system roles + refute html =~ ~r/Delete.*Role.*#{system_role.id}/i + end + + test "delete button is shown for non-system roles", %{conn: conn} do + role = create_role() + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + # Delete button should be visible for non-system roles + assert html =~ gettext("Delete Role") || html =~ "delete" + end + end + + describe "page title" do + setup %{conn: conn} do + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} + end + + test "sets correct page title", %{conn: conn} do + role = create_role() + + {:ok, _view, html} = live(conn, "/admin/roles/#{role.id}") + + # Check that page title is set (might be in title tag or header) + assert html =~ gettext("Show Role") || html =~ role.name + end + end +end diff --git a/test/mv_web/live/user_live/show_test.exs b/test/mv_web/live/user_live/show_test.exs new file mode 100644 index 0000000..054640c --- /dev/null +++ b/test/mv_web/live/user_live/show_test.exs @@ -0,0 +1,155 @@ +defmodule MvWeb.UserLive.ShowTest do + @moduledoc """ + Tests for the user show page. + + Tests cover: + - Displaying user information + - Authentication status display + - Linked member display + - Navigation + - Error handling + """ + use MvWeb.ConnCase, async: true + import Phoenix.LiveViewTest + require Ash.Query + use Gettext, backend: MvWeb.Gettext + + alias Mv.Membership.Member + + setup do + # Create test user + user = create_test_user(%{email: "test@example.com", oidc_id: "test123"}) + %{user: user} + end + + describe "mount and display" do + test "mounts successfully with valid user ID", %{conn: conn, user: user} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") + + assert html =~ to_string(user.email) + end + + test "displays user email", %{conn: conn, user: user} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") + + assert html =~ to_string(user.email) + assert html =~ gettext("Email") + end + + test "displays password authentication status when enabled", %{conn: conn} do + user = create_test_user(%{email: "password-user@example.com", password: "test123"}) + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") + + assert html =~ gettext("Password Authentication") + assert html =~ gettext("Enabled") + end + + test "displays password authentication status when not enabled", %{conn: conn} do + # User without password (only OIDC) - create user with OIDC only + user = + create_test_user(%{ + email: "oidc-only#{System.unique_integer([:positive])}@example.com", + oidc_id: "oidc#{System.unique_integer([:positive])}", + hashed_password: nil + }) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") + + assert html =~ gettext("Password Authentication") + assert html =~ gettext("Not enabled") + end + + test "displays linked member when present", %{conn: conn} do + # Create member + {:ok, member} = + Member + |> Ash.Changeset.for_create(:create_member, %{ + first_name: "Alice", + last_name: "Smith", + email: "alice@example.com" + }) + |> Ash.create() + + # Create user and link to member + user = create_test_user(%{email: "user@example.com"}) + + {:ok, _updated_user} = + user + |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.manage_relationship(:member, member, type: :append_and_remove) + |> Ash.update() + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") + + assert html =~ gettext("Linked Member") + assert html =~ "Alice Smith" + assert html =~ ~r/href="[^"]*\/members\/#{member.id}"/ + end + + test "displays 'No member linked' when no member is linked", %{conn: conn, user: user} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") + + assert html =~ gettext("Linked Member") + assert html =~ gettext("No member linked") + end + end + + describe "navigation" do + test "back button navigates to user list", %{conn: conn, user: user} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, ~p"/users/#{user.id}") + + assert {:error, {:live_redirect, %{to: to}}} = + view + |> element( + "a[aria-label='#{gettext("Back to users list")}'], button[aria-label='#{gettext("Back to users list")}']" + ) + |> render_click() + + assert to == "/users" + end + + test "edit button navigates to edit form", %{conn: conn, user: user} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, ~p"/users/#{user.id}") + + assert {:error, {:live_redirect, %{to: to}}} = + view + |> element( + "a[href='/users/#{user.id}/edit?return_to=show'], button[href='/users/#{user.id}/edit?return_to=show']" + ) + |> render_click() + + assert to == "/users/#{user.id}/edit?return_to=show" + end + end + + describe "error handling" do + test "raises exception for invalid user ID", %{conn: conn} do + invalid_id = Ecto.UUID.generate() + conn = conn_with_oidc_user(conn) + + # The mount function uses Ash.get! which will raise an exception + # This is expected behavior - the LiveView doesn't handle this case + assert_raise Ash.Error.Invalid, fn -> + live(conn, ~p"/users/#{invalid_id}") + end + end + end + + describe "page title" do + test "sets correct page title", %{conn: conn, user: user} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") + + # Check that page title is set (might be in title tag or header) + assert html =~ gettext("Show User") || html =~ to_string(user.email) + end + end +end