diff --git a/config/.env.test b/config/.env.test index 9e1b3ab53..0f1a6f5d2 100644 --- a/config/.env.test +++ b/config/.env.test @@ -11,3 +11,5 @@ ADMIN_USER_PWD=fakepassword ENABLE_EMAIL_VERIFICATION=true SELFHOST=false SITE_LIMIT=3 +HCAPTCHA_SITEKEY=test +HCAPTCHA_SECRET=scottiger diff --git a/lib/plausible_web/captcha.ex b/lib/plausible_web/captcha.ex index dcb5616eb..aff593d88 100644 --- a/lib/plausible_web/captcha.ex +++ b/lib/plausible_web/captcha.ex @@ -4,18 +4,17 @@ defmodule PlausibleWeb.Captcha do @verify_endpoint "https://hcaptcha.com/siteverify" def enabled? do - !!sitekey() + is_binary(sitekey()) end def sitekey() do - Application.get_env(:plausible, :hcaptcha, []) - |> Keyword.fetch!(:sitekey) + Application.get_env(:plausible, :hcaptcha, [])[:sitekey] end def verify(token) do if enabled?() do res = - HTTPClient.post( + HTTPClient.impl().post( @verify_endpoint, [{"content-type", "application/x-www-form-urlencoded"}], %{ @@ -25,9 +24,8 @@ defmodule PlausibleWeb.Captcha do ) case res do - {:ok, %Finch.Response{status: 200, body: body}} -> - json = Jason.decode!(body) - json["success"] + {:ok, %Finch.Response{status: 200, body: %{"success" => success}}} -> + success _ -> false @@ -38,7 +36,6 @@ defmodule PlausibleWeb.Captcha do end defp secret() do - Application.get_env(:plausible, :hcaptcha, []) - |> Keyword.fetch!(:secret) + Application.get_env(:plausible, :hcaptcha, [])[:secret] end end diff --git a/test/plausible_web/captcha_test.exs b/test/plausible_web/captcha_test.exs new file mode 100644 index 000000000..fc458d3cc --- /dev/null +++ b/test/plausible_web/captcha_test.exs @@ -0,0 +1,66 @@ +defmodule PlausibleWeb.CaptchaTest do + use Plausible.DataCase + + import Mox + setup :verify_on_exit! + + alias PlausibleWeb.Captcha + + describe "mocked payloads" do + @failure Jason.decode!(~s/{"success":false,"error-codes":["invalid-input-response"]}/) + @success Jason.decode!(~s/{"success":true}/) + + test "returns false for non-success response" do + expect( + Plausible.HTTPClient.Mock, + :post, + fn "https://hcaptcha.com/siteverify", + [{"content-type", "application/x-www-form-urlencoded"}], + %{response: "bad", secret: "scottiger"} -> + {:ok, + %Finch.Response{ + status: 200, + headers: [{"content-type", "application/json"}], + body: @failure + }} + end + ) + + refute Captcha.verify("bad") + end + + test "returns true for successful response" do + expect( + Plausible.HTTPClient.Mock, + :post, + fn "https://hcaptcha.com/siteverify", + [{"content-type", "application/x-www-form-urlencoded"}], + %{response: "good", secret: "scottiger"} -> + {:ok, + %Finch.Response{ + status: 200, + headers: [{"content-type", "application/json"}], + body: @success + }} + end + ) + + assert Captcha.verify("good") + end + end + + describe "with patched application env" do + setup do + original_env = Application.get_env(:plausible, :hcaptcha) + Application.put_env(:plausible, :hcaptcha, sitekey: nil) + + on_exit(fn -> + Application.put_env(:plausible, :hcaptcha, original_env) + end) + end + + test "returns true when disabled" do + assert Captcha.verify("disabled") + end + end +end diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs index 078fe8106..f3734fc89 100644 --- a/test/plausible_web/controllers/auth_controller_test.exs +++ b/test/plausible_web/controllers/auth_controller_test.exs @@ -4,6 +4,9 @@ defmodule PlausibleWeb.AuthControllerTest do use Plausible.Repo import Plausible.TestUtils + import Mox + setup :verify_on_exit! + describe "GET /register" do test "shows the register form", %{conn: conn} do conn = get(conn, "/register") @@ -14,6 +17,8 @@ defmodule PlausibleWeb.AuthControllerTest do describe "POST /register" do test "registering sends an activation link", %{conn: conn} do + mock_captcha_success() + post(conn, "/register", user: %{ name: "Jane Doe", @@ -29,6 +34,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "user is redirected to activate page after registration", %{conn: conn} do + mock_captcha_success() + conn = post(conn, "/register", user: %{ @@ -43,6 +50,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "creates user record", %{conn: conn} do + mock_captcha_success() + post(conn, "/register", user: %{ name: "Jane Doe", @@ -57,6 +66,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "logs the user in", %{conn: conn} do + mock_captcha_success() + conn = post(conn, "/register", user: %{ @@ -71,6 +82,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "user is redirected to activation after registration", %{conn: conn} do + mock_captcha_success() + conn = post(conn, "/register", user: %{ @@ -83,6 +96,22 @@ defmodule PlausibleWeb.AuthControllerTest do assert redirected_to(conn) == "/activate" end + + test "renders captcha errors in case of captcha input verification failure", %{conn: conn} do + mock_captcha_failure() + + conn = + post(conn, "/register", + user: %{ + name: "Jane Doe", + email: "user@example.com", + password: "very-secret", + password_confirmation: "very-secret" + } + ) + + assert html_response(conn, 200) =~ "Please complete the captcha" + end end describe "GET /register/invitations/:invitation_id" do @@ -121,6 +150,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "registering sends an activation link", %{conn: conn, invitation: invitation} do + mock_captcha_success() + post(conn, "/register/invitation/#{invitation.invitation_id}", user: %{ name: "Jane Doe", @@ -139,6 +170,8 @@ defmodule PlausibleWeb.AuthControllerTest do conn: conn, invitation: invitation } do + mock_captcha_success() + conn = post(conn, "/register/invitation/#{invitation.invitation_id}", user: %{ @@ -153,6 +186,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "creates user record", %{conn: conn, invitation: invitation} do + mock_captcha_success() + post(conn, "/register/invitation/#{invitation.invitation_id}", user: %{ name: "Jane Doe", @@ -170,6 +205,8 @@ defmodule PlausibleWeb.AuthControllerTest do conn: conn, invitation: invitation } do + mock_captcha_success() + post(conn, "/register/invitation/#{invitation.invitation_id}", user: %{ name: "Jane Doe", @@ -184,6 +221,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "logs the user in", %{conn: conn, invitation: invitation} do + mock_captcha_success() + conn = post(conn, "/register/invitation/#{invitation.invitation_id}", user: %{ @@ -198,6 +237,8 @@ defmodule PlausibleWeb.AuthControllerTest do end test "user is redirected to activation after registration", %{conn: conn} do + mock_captcha_success() + conn = post(conn, "/register", user: %{ @@ -210,6 +251,25 @@ defmodule PlausibleWeb.AuthControllerTest do assert redirected_to(conn) == "/activate" end + + test "renders captcha errors in case of captcha input verification failure", %{ + conn: conn, + invitation: invitation + } do + mock_captcha_failure() + + conn = + post(conn, "/register/invitation/#{invitation.invitation_id}", + user: %{ + name: "Jane Doe", + email: "user@example.com", + password: "very-secret", + password_confirmation: "very-secret" + } + ) + + assert html_response(conn, 200) =~ "Please complete the captcha" + end end describe "GET /activate" do @@ -416,12 +476,21 @@ defmodule PlausibleWeb.AuthControllerTest do end test "email is present and exists - sends password reset email", %{conn: conn} do + mock_captcha_success() user = insert(:user) conn = post(conn, "/password/request-reset", %{email: user.email}) assert html_response(conn, 200) =~ "Success!" assert_email_delivered_with(subject: "Plausible password reset") end + + test "renders captcha errors in case of captcha input verification failure", %{conn: conn} do + mock_captcha_failure() + user = insert(:user) + conn = post(conn, "/password/request-reset", %{email: user.email}) + + assert html_response(conn, 200) =~ "Please complete the captcha" + end end describe "GET /password/reset" do @@ -690,4 +759,27 @@ defmodule PlausibleWeb.AuthControllerTest do assert Repo.get(ApiKey, api_key.id) end end + + defp mock_captcha_success() do + mock_captcha(true) + end + + defp mock_captcha_failure() do + mock_captcha(false) + end + + defp mock_captcha(success) do + expect( + Plausible.HTTPClient.Mock, + :post, + fn _, _, _ -> + {:ok, + %Finch.Response{ + status: 200, + headers: [{"content-type", "application/json"}], + body: %{"success" => success} + }} + end + ) + end end