mitgliederverwaltung/test/mv_web/live/join_live_test.exs
Simon 86c032004e
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/production Build is failing
fix: failing tests
2026-03-13 19:43:04 +01:00

179 lines
5.9 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule MvWeb.JoinLiveTest do
@moduledoc """
Tests for the public join page (Subtask 4: Public join page and anti-abuse).
Covers: public path /join (unauthenticated 200), 404 when join disabled,
submit creates JoinRequest and shows success copy, honeypot prevents create,
rate limiting rejects excess submits. Uses unauthenticated conn; no User/Member.
Honeypot: form param `"website"` (legit-sounding name per best practice; not "honeypot").
Field is hidden via CSS class in app.css (off-screen, no inline styles), type="text".
"""
# async: false so LiveView and test share sandbox (submit creates JoinRequest in LiveView process).
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Ecto.Query
alias Ecto.Adapters.SQL.Sandbox
alias Mv.Membership
alias Mv.Repo
describe "GET /join" do
@tag role: :unauthenticated
test "unauthenticated GET /join returns 200 when join form is enabled", %{conn: conn} do
enable_join_form(true)
conn = get(conn, "/join")
assert conn.status == 200
end
@tag role: :unauthenticated
test "unauthenticated GET /join returns 404 when join form is disabled", %{conn: conn} do
enable_join_form(false)
conn = get(conn, "/join")
assert conn.status == 404
end
end
describe "submit join form" do
setup :enable_join_form_for_test
@tag role: :unauthenticated
test "submit with valid allowlist data creates one JoinRequest and shows success copy", %{
conn: conn
} do
# Re-apply allowlist so this test is robust when run in parallel with others (Settings singleton).
enable_join_form_for_test(%{})
count_before = count_join_requests()
{:ok, view, _html} = live(conn, "/join")
# Allow LiveView process to use the same Ecto sandbox so the test sees the created JoinRequest.
Sandbox.allow(Repo, conn.private[:ecto_sandbox], view.pid)
view
|> form("#join-form", %{
"email" => "newuser#{System.unique_integer([:positive])}@example.com",
"first_name" => "Jane",
"last_name" => "Doe",
"website" => ""
})
|> render_submit()
# Anti-enumeration delay is applied in LiveView via send_after (100300 ms); wait for success UI.
Process.sleep(400)
assert count_join_requests() == count_before + 1
assert view |> element("[data-testid='join-success-message']") |> has_element?()
assert render(view) =~ "saved your details"
assert render(view) =~ "click the link"
end
@tag role: :unauthenticated
test "submit with honeypot filled does not create JoinRequest but shows same success copy", %{
conn: conn
} do
count_before = count_join_requests()
{:ok, view, _html} = live(conn, "/join")
view
|> form("#join-form", %{
"email" => "bot#{System.unique_integer([:positive])}@example.com",
"first_name" => "Bot",
"last_name" => "User",
"website" => "filled-by-bot"
})
|> render_submit()
assert count_join_requests() == count_before
assert view |> element("[data-testid='join-success-message']") |> has_element?()
end
@tag role: :unauthenticated
@tag :slow
test "after rate limit exceeded submit returns 429 or error and no new JoinRequest", %{
conn: conn
} do
# Reset rate limit state so this test is independent of others (same key in test)
try do
:ets.delete_all_objects(MvWeb.JoinRateLimit)
rescue
ArgumentError -> :ok
end
enable_join_form(true)
# Set allowlist so form has email, first_name, last_name
{:ok, settings} = Membership.get_settings()
Membership.update_settings(settings, %{
join_form_field_ids: ["email", "first_name", "last_name"],
join_form_field_required: %{"email" => true, "first_name" => false, "last_name" => false}
})
# Rely on test config: join rate limit low (e.g. 2 per window)
base_email = "ratelimit#{System.unique_integer([:positive])}@example.com"
count_before = count_join_requests()
sandbox = conn.private[:ecto_sandbox]
# Exhaust limit with 2 valid submits (each needs a fresh session because form disappears after submit)
for i <- 0..1 do
c =
build_conn()
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_private(:ecto_sandbox, sandbox)
{:ok, view, _} = live(c, "/join")
view
|> form("#join-form", %{
"email" => "#{i}-#{base_email}",
"first_name" => "User",
"last_name" => "Test",
"website" => ""
})
|> render_submit()
end
# Next submit (new session) should be rate limited
c =
build_conn()
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_private(:ecto_sandbox, sandbox)
{:ok, view, _} = live(c, "/join")
result =
view
|> form("#join-form", %{
"email" => "third-#{base_email}",
"first_name" => "Third",
"last_name" => "User",
"website" => ""
})
|> render_submit()
assert count_join_requests() == count_before + 2
assert result =~ "rate limit" or String.downcase(result) =~ "too many" or result =~ "429"
end
end
defp enable_join_form(enabled) do
{:ok, settings} = Membership.get_settings()
{:ok, _} = Membership.update_settings(settings, %{join_form_enabled: enabled})
end
defp enable_join_form_for_test(_context) do
{:ok, settings} = Membership.get_settings()
{:ok, _} =
Membership.update_settings(settings, %{
join_form_enabled: true,
join_form_field_ids: ["email", "first_name", "last_name"],
join_form_field_required: %{"email" => true, "first_name" => false, "last_name" => false}
})
:ok
end
defp count_join_requests do
Repo.one(from j in "join_requests", select: count(j.id)) || 0
end
end