mitgliederverwaltung/test/mv_web/live/join_live_test.exs
Simon f1d0526209
Some checks failed
continuous-integration/drone/push Build is failing
feat: add join form
2026-03-10 18:25:17 +01:00

169 lines
5.3 KiB
Elixir

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".
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Ecto.Query
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
count_before = count_join_requests()
{:ok, view, _html} = live(conn, "/join")
view
|> form("#join-form", %{
"email" => "newuser#{System.unique_integer([:positive])}@example.com",
"first_name" => "Jane",
"last_name" => "Doe",
"website" => ""
})
|> render_submit()
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