test: add tests for join mail confirmation
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
ad6ef169ac
commit
3672ef0d03
5 changed files with 177 additions and 0 deletions
|
|
@ -38,6 +38,8 @@ This document lists all protected routes, which permission set may access them,
|
||||||
|
|
||||||
- `/auth*`, `/register`, `/reset`, `/sign-in`, `/sign-out`, `/confirm*`, `/password-reset*`, `/set_locale`
|
- `/auth*`, `/register`, `/reset`, `/sign-in`, `/sign-out`, `/confirm*`, `/password-reset*`, `/set_locale`
|
||||||
|
|
||||||
|
The join confirmation route `GET /confirm_join/:token` is public (matched by `/confirm*`). Unit tests: `test/mv_web/controllers/join_confirm_controller_test.exs` (stubbed callback, no integration).
|
||||||
|
|
||||||
## Test Coverage
|
## Test Coverage
|
||||||
|
|
||||||
**File:** `test/mv_web/plugs/check_page_permission_test.exs`
|
**File:** `test/mv_web/plugs/check_page_permission_test.exs`
|
||||||
|
|
|
||||||
45
lib/mv_web/controllers/join_confirm_controller.ex
Normal file
45
lib/mv_web/controllers/join_confirm_controller.ex
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
defmodule MvWeb.JoinConfirmController do
|
||||||
|
@moduledoc """
|
||||||
|
Handles GET /confirm_join/:token for the public join flow (double opt-in).
|
||||||
|
|
||||||
|
Calls a configurable callback (default Mv.Membership) so tests can stub the
|
||||||
|
dependency. Public route; no authentication required.
|
||||||
|
"""
|
||||||
|
use MvWeb, :controller
|
||||||
|
|
||||||
|
def confirm(conn, %{"token" => token}) when is_binary(token) do
|
||||||
|
callback = Application.get_env(:mv, :join_confirm_callback, Mv.Membership)
|
||||||
|
|
||||||
|
case callback.confirm_join_request(token, actor: nil) do
|
||||||
|
{:ok, _request} ->
|
||||||
|
success_response(conn)
|
||||||
|
|
||||||
|
{:error, :token_expired} ->
|
||||||
|
expired_response(conn)
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
invalid_response(conn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm(conn, _params), do: invalid_response(conn)
|
||||||
|
|
||||||
|
defp success_response(conn) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/html")
|
||||||
|
|> send_resp(200, gettext("Thank you, we have received your request."))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp expired_response(conn) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/html")
|
||||||
|
|> send_resp(200, gettext("This link has expired. Please submit the form again."))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp invalid_response(conn) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/html")
|
||||||
|
|> put_status(404)
|
||||||
|
|> send_resp(404, gettext("Invalid or expired link."))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -126,6 +126,9 @@ defmodule MvWeb.Router do
|
||||||
overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI],
|
overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI],
|
||||||
gettext_backend: {MvWeb.Gettext, "auth"}
|
gettext_backend: {MvWeb.Gettext, "auth"}
|
||||||
|
|
||||||
|
# Public join confirmation (double opt-in); /confirm* is already public in CheckPagePermission
|
||||||
|
get "/confirm_join/:token", JoinConfirmController, :confirm
|
||||||
|
|
||||||
# Remove this if you do not use the magic link strategy.
|
# Remove this if you do not use the magic link strategy.
|
||||||
# magic_sign_in_route(Mv.Accounts.User, :magic_link,
|
# magic_sign_in_route(Mv.Accounts.User, :magic_link,
|
||||||
# auth_routes_prefix: "/auth",
|
# auth_routes_prefix: "/auth",
|
||||||
|
|
|
||||||
34
test/membership/join_request_submit_email_test.exs
Normal file
34
test/membership/join_request_submit_email_test.exs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
defmodule Mv.Membership.JoinRequestSubmitEmailTest do
|
||||||
|
@moduledoc """
|
||||||
|
Unit tests for join request confirmation email on submit (Subtask 2).
|
||||||
|
|
||||||
|
Asserts that submit_join_request triggers sending exactly one confirmation email
|
||||||
|
(to the request email, with confirm link). Uses Swoosh.Adapters.Test; no integration.
|
||||||
|
Sender is wired in implementation; test fails until then (TDD).
|
||||||
|
"""
|
||||||
|
use Mv.DataCase, async: true
|
||||||
|
|
||||||
|
import Swoosh.TestAssertions
|
||||||
|
|
||||||
|
alias Mv.Membership
|
||||||
|
|
||||||
|
@valid_submit_attrs %{
|
||||||
|
email: "join#{System.unique_integer([:positive])}@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
describe "submit_join_request/2 sends confirmation email" do
|
||||||
|
test "sends exactly one email to the request email with confirm link" do
|
||||||
|
token = "email-test-token-#{System.unique_integer([:positive])}"
|
||||||
|
attrs = Map.put(@valid_submit_attrs, :confirmation_token, token)
|
||||||
|
email = attrs.email
|
||||||
|
|
||||||
|
assert {:ok, _request} = Membership.submit_join_request(attrs, actor: nil)
|
||||||
|
|
||||||
|
assert_email_sent(fn email_sent ->
|
||||||
|
to_addresses = Enum.map(email_sent.to, &elem(&1, 1))
|
||||||
|
to_string(email) in to_addresses and
|
||||||
|
(email_sent.html_body =~ "/confirm_join/" or email_sent.text_body =~ "/confirm_join/")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
93
test/mv_web/controllers/join_confirm_controller_test.exs
Normal file
93
test/mv_web/controllers/join_confirm_controller_test.exs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
defmodule MvWeb.JoinConfirmControllerTest do
|
||||||
|
@moduledoc """
|
||||||
|
Unit tests for JoinConfirmController (Subtask 2).
|
||||||
|
|
||||||
|
Stubs the join-confirm callback via Application config so no DB or domain is used.
|
||||||
|
Uses unauthenticated conn; route is public (/confirm*).
|
||||||
|
"""
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
# Stub modules for configurable callback (unit test: no real Membership calls)
|
||||||
|
defmodule JoinConfirmValidStub do
|
||||||
|
def confirm_join_request(_token, _opts), do: {:ok, %{}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule JoinConfirmExpiredStub do
|
||||||
|
def confirm_join_request(_token, _opts), do: {:error, :token_expired}
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule JoinConfirmInvalidStub do
|
||||||
|
def confirm_join_request(_token, _opts) do
|
||||||
|
{:error, Ash.Error.Query.NotFound.exception(resource: Mv.Membership.JoinRequest)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setup %{conn: conn} do
|
||||||
|
# Restore callback after each test so env does not leak
|
||||||
|
on_exit(fn ->
|
||||||
|
Application.delete_env(:mv, :join_confirm_callback)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Build unauthenticated conn for public confirm route
|
||||||
|
unauth_conn =
|
||||||
|
build_conn()
|
||||||
|
|> init_test_session(%{})
|
||||||
|
|> fetch_flash()
|
||||||
|
|> Plug.Conn.put_private(:ecto_sandbox, conn.private[:ecto_sandbox])
|
||||||
|
|
||||||
|
{:ok, conn: unauth_conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /confirm_join/:token" do
|
||||||
|
@tag role: :unauthenticated
|
||||||
|
test "valid token returns 200 and success message", %{conn: conn} do
|
||||||
|
Application.put_env(:mv, :join_confirm_callback, JoinConfirmValidStub)
|
||||||
|
|
||||||
|
conn = get(conn, "/confirm_join/any-valid-token")
|
||||||
|
|
||||||
|
assert response(conn, 200) =~ "received your request"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :unauthenticated
|
||||||
|
test "second request with same token still returns 200 (idempotent)", %{conn: conn} do
|
||||||
|
Application.put_env(:mv, :join_confirm_callback, JoinConfirmValidStub)
|
||||||
|
|
||||||
|
first = get(conn, "/confirm_join/same-token")
|
||||||
|
second = get(conn, "/confirm_join/same-token")
|
||||||
|
|
||||||
|
assert response(first, 200) =~ "received your request"
|
||||||
|
assert response(second, 200) =~ "received your request"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :unauthenticated
|
||||||
|
test "expired token returns 200 with expired message", %{conn: conn} do
|
||||||
|
Application.put_env(:mv, :join_confirm_callback, JoinConfirmExpiredStub)
|
||||||
|
|
||||||
|
conn = get(conn, "/confirm_join/expired-token")
|
||||||
|
|
||||||
|
assert response(conn, 200) =~ "expired"
|
||||||
|
assert response(conn, 200) =~ "submit"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :unauthenticated
|
||||||
|
test "unknown or invalid token returns 404 with error message", %{conn: conn} do
|
||||||
|
Application.put_env(:mv, :join_confirm_callback, JoinConfirmInvalidStub)
|
||||||
|
|
||||||
|
conn = get(conn, "/confirm_join/nonexistent-token")
|
||||||
|
|
||||||
|
assert response(conn, 404) =~ "Invalid"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :unauthenticated
|
||||||
|
test "route is public (unauthenticated request returns 200, not redirect to sign-in)", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
Application.put_env(:mv, :join_confirm_callback, JoinConfirmValidStub)
|
||||||
|
|
||||||
|
conn = get(conn, "/confirm_join/public-test-token")
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
refute redirected_to(conn) =~ "/sign-in"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue