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 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