mitgliederverwaltung/test/mv_web/live/group_live/show_test.exs
Simon ea3bdcaa65
All checks were successful
continuous-integration/drone/push Build is passing
refactor: apply review comments
2026-01-28 14:42:16 +01:00

307 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)
# Allow some overhead for LiveView setup queries
assert final_count <= 5,
"Expected max 5 queries (group + members preload + LiveView setup), 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