308 lines
10 KiB
Elixir
308 lines
10 KiB
Elixir
defmodule MvWeb.GroupLive.ShowTest do
|
|
@moduledoc """
|
|
Tests for the group detail page.
|
|
|
|
Tests cover:
|
|
- Displaying group information
|
|
- Slug-based routing
|
|
- Member list display
|
|
- Navigation
|
|
- Error handling
|
|
- Delete functionality
|
|
"""
|
|
use MvWeb.ConnCase, async: false
|
|
import Phoenix.LiveViewTest
|
|
require Ash.Query
|
|
use Gettext, backend: MvWeb.Gettext
|
|
|
|
alias Mv.Membership
|
|
alias Mv.Fixtures
|
|
|
|
describe "mount and display" do
|
|
test "page renders successfully", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ group.name
|
|
end
|
|
|
|
test "displays group name", %{conn: conn} do
|
|
group = Fixtures.group_fixture(%{name: "Test Group Name"})
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ "Test Group Name"
|
|
end
|
|
|
|
test "displays group description when present", %{conn: conn} do
|
|
group = Fixtures.group_fixture(%{description: "This is a test description"})
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ "This is a test description"
|
|
end
|
|
|
|
test "displays member count", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
# Member count should be displayed (might be 0 or more)
|
|
assert html =~ "0" or html =~ gettext("Members") or html =~ "member" or html =~ "Mitglied"
|
|
end
|
|
|
|
test "displays list of members in group", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
member1 = Fixtures.member_fixture(%{first_name: "Alice", last_name: "Smith"})
|
|
member2 = Fixtures.member_fixture(%{first_name: "Bob", last_name: "Jones"})
|
|
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
|
|
Membership.create_member_group(%{member_id: member1.id, group_id: group.id},
|
|
actor: system_actor
|
|
)
|
|
|
|
Membership.create_member_group(%{member_id: member2.id, group_id: group.id},
|
|
actor: system_actor
|
|
)
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ "Alice" or html =~ "Smith"
|
|
assert html =~ "Bob" or html =~ "Jones"
|
|
end
|
|
|
|
test "displays edit button for admin users", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ gettext("Edit") or html =~ "edit" or html =~ "Bearbeiten"
|
|
end
|
|
|
|
test "displays delete button for admin users", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ gettext("Delete") or html =~ "delete" or html =~ "Löschen"
|
|
end
|
|
end
|
|
|
|
describe "slug-based routing" do
|
|
test "route /groups/:slug works correctly", %{conn: conn} do
|
|
group = Fixtures.group_fixture(%{name: "Board Members"})
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ "Board Members"
|
|
# Verify slug is in URL
|
|
assert html =~ group.slug or html =~ "board-members"
|
|
end
|
|
|
|
test "group is found by slug via unique_slug identity", %{conn: conn} do
|
|
group = Fixtures.group_fixture(%{name: "Test Group"})
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ group.name
|
|
end
|
|
|
|
test "non-existent slug returns 404", %{conn: conn} do
|
|
non_existent_slug = "non-existent-group-slug"
|
|
|
|
result = live(conn, "/groups/#{non_existent_slug}")
|
|
|
|
assert match?({:error, {:redirect, %{to: "/groups"}}}, result) ||
|
|
match?({:error, {:live_redirect, %{to: "/groups"}}}, result)
|
|
end
|
|
|
|
test "slug is case-sensitive (exact match required)", %{conn: conn} do
|
|
group = Fixtures.group_fixture(%{name: "Test Group"})
|
|
# Slug should be lowercase
|
|
wrong_case_slug = String.upcase(group.slug)
|
|
|
|
result = live(conn, "/groups/#{wrong_case_slug}")
|
|
|
|
# Should not find group with wrong case
|
|
assert match?({:error, {:redirect, %{to: "/groups"}}}, result) ||
|
|
match?({:error, {:live_redirect, %{to: "/groups"}}}, result)
|
|
end
|
|
|
|
test "URL is readable and user-friendly", %{conn: conn} do
|
|
group = Fixtures.group_fixture(%{name: "Board Members"})
|
|
|
|
{:ok, _view, _html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
# URL should contain readable slug, not UUID
|
|
assert group.slug == "board-members"
|
|
refute String.contains?(group.slug, "-") == false
|
|
end
|
|
end
|
|
|
|
describe "edge cases" do
|
|
test "displays empty group correctly (0 members)", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ "0" or html =~ gettext("No members") or html =~ "empty" or
|
|
html =~ "Keine Mitglieder"
|
|
end
|
|
|
|
test "handles group without description correctly", %{conn: conn} do
|
|
group = Fixtures.group_fixture(%{description: nil})
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
# Should not crash, description should be optional
|
|
assert html =~ group.name
|
|
end
|
|
|
|
test "handles slug with special characters correctly", %{conn: conn} do
|
|
# Create group with name that generates slug with hyphens
|
|
group = Fixtures.group_fixture(%{name: "Test-Group-Name"})
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ "Test-Group-Name" or html =~ group.name
|
|
end
|
|
end
|
|
|
|
describe "security" do
|
|
@tag role: :member
|
|
test "read-only users can view group detail page", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
read_only_user = Fixtures.user_with_role_fixture("read_only")
|
|
conn = conn_with_password_user(conn, read_only_user)
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ group.name
|
|
# Should NOT see edit/delete buttons
|
|
refute html =~ gettext("Edit") or html =~ gettext("Delete")
|
|
end
|
|
|
|
@tag role: :unauthenticated
|
|
test "unauthenticated users are redirected to login", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
result = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert match?({:error, {:redirect, %{to: "/sign-in"}}}, result) ||
|
|
match?({:error, {:live_redirect, %{to: "/sign-in"}}}, result)
|
|
end
|
|
|
|
test "slug injection attempts are prevented", %{conn: conn} do
|
|
# Try to inject SQL or other malicious content in slug
|
|
malicious_slug = "'; DROP TABLE groups; --"
|
|
|
|
result = live(conn, "/groups/#{malicious_slug}")
|
|
|
|
# Should not execute SQL, should return 404 or error
|
|
assert match?({:error, {:redirect, %{to: "/groups"}}}, result) ||
|
|
match?({:error, {:live_redirect, %{to: "/groups"}}}, result)
|
|
end
|
|
|
|
test "user cannot delete group with non-existent slug", %{conn: conn} do
|
|
# This test verifies that delete action (if accessed via URL manipulation)
|
|
# cannot be performed with non-existent slug
|
|
# Note: Actual delete functionality will be tested when delete modal is implemented
|
|
non_existent_slug = "non-existent-group-slug"
|
|
|
|
# Attempting to access show page with non-existent slug should fail
|
|
result = live(conn, "/groups/#{non_existent_slug}")
|
|
|
|
assert match?({:error, {:redirect, %{to: "/groups"}}}, result) ||
|
|
match?({:error, {:live_redirect, %{to: "/groups"}}}, result)
|
|
end
|
|
end
|
|
|
|
describe "performance" do
|
|
@describetag :slow
|
|
test "member list is loaded efficiently (no N+1 queries)", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
# Create multiple members
|
|
members = Enum.map(1..5, fn _ -> Fixtures.member_fixture() end)
|
|
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
|
|
# Add all members to group
|
|
Enum.each(members, fn member ->
|
|
Membership.create_member_group(%{member_id: member.id, group_id: group.id},
|
|
actor: system_actor
|
|
)
|
|
end)
|
|
|
|
# Count queries using Telemetry
|
|
query_count_agent = Agent.start_link(fn -> 0 end) |> elem(1)
|
|
|
|
handler = fn _event, _measurements, _metadata, _config ->
|
|
Agent.update(query_count_agent, &(&1 + 1))
|
|
end
|
|
|
|
handler_id = "test-query-counter-#{System.unique_integer([:positive])}"
|
|
:telemetry.attach(handler_id, [:ash, :query, :start], handler, nil)
|
|
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
final_count = Agent.get(query_count_agent, & &1)
|
|
:telemetry.detach(handler_id)
|
|
|
|
# All members should be displayed
|
|
Enum.each(members, fn member ->
|
|
assert html =~ member.first_name or html =~ member.last_name
|
|
end)
|
|
|
|
# Verify query count is reasonable (should avoid N+1 queries)
|
|
# Expected: 1 query for group lookup + 1 query for members (with preload) + member_count aggregate
|
|
# Allow overhead for authorization, LiveView setup, and other initialization queries
|
|
# Note: member_count aggregate and authorization checks may add additional queries
|
|
assert final_count <= 20,
|
|
"Expected max 20 queries (group + members preload + member_count aggregate + LiveView setup + auth), got #{final_count}. This suggests N+1 query problem."
|
|
end
|
|
|
|
test "slug lookup is efficient (uses unique_slug index)", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
# Should use index for fast lookup
|
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert html =~ group.name
|
|
end
|
|
end
|
|
|
|
describe "navigation" do
|
|
test "back button navigates to groups list", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
view
|
|
|> element(
|
|
"a[aria-label*='Back'], button[aria-label*='Back'], a[aria-label*='groups list'], button[aria-label*='groups list']"
|
|
)
|
|
|> render_click()
|
|
|
|
assert to == "/groups"
|
|
end
|
|
|
|
test "edit button navigates to edit form", %{conn: conn} do
|
|
group = Fixtures.group_fixture()
|
|
|
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
|
|
|
# Button with navigate is rendered as <a> tag via <.link>
|
|
# Use element with text content to find the link
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
view
|
|
|> element("a", gettext("Edit"))
|
|
|> render_click()
|
|
|
|
assert to == "/groups/#{group.slug}/edit"
|
|
end
|
|
end
|
|
end
|