From 3607e453e47884f59d24b0c54f3172ff854e7d87 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 13 Oct 2020 22:20:26 +0300 Subject: [PATCH] updated registration API responses --- lib/pleroma/captcha/error.ex | 43 ++ .../web/api_spec/errors/registration_user_error.ex | 33 + .../web/api_spec/operations/account_operation.ex | 31 +- lib/pleroma/web/api_spec/render_error.ex | 64 +- .../mastodon_api/controllers/account_controller.ex | 29 +- lib/pleroma/web/twitter_api/twitter_api.ex | 39 +- .../controllers/account_controller_test.exs | 539 ---------------- .../web/mastodon_api/registration_user_test.exs | 691 +++++++++++++++++++++ test/pleroma/web/twitter_api/twitter_api_test.exs | 20 +- test/support/factory.ex | 9 + 10 files changed, 880 insertions(+), 618 deletions(-) create mode 100644 lib/pleroma/captcha/error.ex create mode 100644 lib/pleroma/web/api_spec/errors/registration_user_error.ex create mode 100644 test/pleroma/web/mastodon_api/registration_user_test.exs diff --git a/lib/pleroma/captcha/error.ex b/lib/pleroma/captcha/error.ex new file mode 100644 index 000000000..ec77f1336 --- /dev/null +++ b/lib/pleroma/captcha/error.ex @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Captcha.Error do + import Pleroma.Web.Gettext + + def message(_reason, opts \\ []) + + def message(:missing_field, %{name: name}) do + dgettext( + "errors", + "Invalid CAPTCHA (Missing parameter: %{name})", + name: name + ) + end + + def message(:captcha_error, _) do + dgettext("errors", "CAPTCHA Error") + end + + def message(:invalid, _) do + dgettext("errors", "Invalid CAPTCHA") + end + + def message(:kocaptcha_service_unavailable, _) do + dgettext("errors", "Kocaptcha service unavailable") + end + + def message(:expired, _) do + dgettext("errors", "CAPTCHA expired") + end + + def message(:already_used, _) do + dgettext("errors", "CAPTCHA already used") + end + + def message(:invalid_answer_data, _) do + dgettext("errors", "Invalid answer data") + end + + def message(error, _), do: error +end diff --git a/lib/pleroma/web/api_spec/errors/registration_user_error.ex b/lib/pleroma/web/api_spec/errors/registration_user_error.ex new file mode 100644 index 000000000..1e48e12c9 --- /dev/null +++ b/lib/pleroma/web/api_spec/errors/registration_user_error.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Errors.RegistrationUserError do + @behaviour Plug + + import Plug.Conn, only: [put_status: 2] + import Phoenix.Controller, only: [json: 2] + alias Pleroma.Web.ApiSpec.RenderError + + @impl Plug + def init(opts), do: opts + + @impl Plug + + def call(conn, errors) do + field_errors = + errors + |> Enum.group_by(& &1.name) + |> Enum.into(%{}, fn {field, field_errors} -> + {field, Enum.map(field_errors, &RenderError.message/1)} + end) + + conn + |> put_status(:bad_request) + |> json(%{ + error: "Please review the submission", + identifier: "review_submission", + fields: field_errors + }) + end +end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index d90ddb787..d68501bb8 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -34,7 +34,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do requestBody: request_body("Parameters", create_request(), required: true), responses: %{ 200 => Operation.response("Account", "application/json", create_response()), - 400 => Operation.response("Error", "application/json", ApiError), + 400 => Operation.response("Error", "application/json", error_response()), 403 => Operation.response("Error", "application/json", ApiError), 429 => Operation.response("Error", "application/json", ApiError) } @@ -453,6 +453,35 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do } end + defp error_response do + %Schema{ + title: "AccountCreateErrorResponse", + description: "Response schema for errors", + type: :object, + properties: %{ + identifier: %Schema{type: :string}, + message: %Schema{type: :string}, + fields: %Schema{ + type: :object, + properties: %{ + captcha: %Schema{type: :array, items: %Schema{type: :string}}, + email: %Schema{type: :array, items: %Schema{type: :string}}, + invite: %Schema{type: :array, items: %Schema{type: :string}}, + password: %Schema{type: :array, items: %Schema{type: :string}}, + username: %Schema{type: :array, items: %Schema{type: :string}} + } + } + }, + example: %{ + "error" => "Please review the submission", + "identifier" => "review_submission", + "fields" => %{ + "captcha" => ["Invalid CAPTCHA"] + } + } + } + end + # Note: this is a token response (if login succeeds!), but there's no oauth operation file yet. defp create_response do %Schema{ diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex index d476b8ef3..59154b39e 100644 --- a/lib/pleroma/web/api_spec/render_error.ex +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -47,14 +47,14 @@ defmodule Pleroma.Web.ApiSpec.RenderError do } end - defp message(%{reason: :invalid_schema_type, type: type, name: name}) do + def message(%{reason: :invalid_schema_type, type: type, name: name}) do gettext("%{name} - Invalid schema.type. Got: %{type}.", name: name, type: inspect(type) ) end - defp message(%{reason: :null_value, name: name} = error) do + def message(%{reason: :null_value, name: name} = error) do case error.type do nil -> gettext("%{name} - null value.", name: name) @@ -67,42 +67,42 @@ defmodule Pleroma.Web.ApiSpec.RenderError do end end - defp message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do + def message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do gettext( "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed.", invalid_schema: invalid_schema ) end - defp message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do + def message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do gettext("Failed to cast value using any of: %{failed_schemas}.", failed_schemas: failed_schemas ) end - defp message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do + def message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do gettext("Failed to cast value to one of: %{failed_schemas}.", failed_schemas: failed_schemas) end - defp message(%{reason: :min_length, length: length, name: name}) do + def message(%{reason: :min_length, length: length, name: name}) do gettext("%{name} - String length is smaller than minLength: %{length}.", name: name, length: length ) end - defp message(%{reason: :max_length, length: length, name: name}) do + def message(%{reason: :max_length, length: length, name: name}) do gettext("%{name} - String length is larger than maxLength: %{length}.", name: name, length: length ) end - defp message(%{reason: :unique_items, name: name}) do + def message(%{reason: :unique_items, name: name}) do gettext("%{name} - Array items must be unique.", name: name) end - defp message(%{reason: :min_items, length: min, value: array, name: name}) do + def message(%{reason: :min_items, length: min, value: array, name: name}) do gettext("%{name} - Array length %{length} is smaller than minItems: %{min}.", name: name, length: length(array), @@ -110,7 +110,7 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :max_items, length: max, value: array, name: name}) do + def message(%{reason: :max_items, length: max, value: array, name: name}) do gettext("%{name} - Array length %{length} is larger than maxItems: %{}.", name: name, length: length(array), @@ -118,7 +118,7 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do + def message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do gettext("%{name} - %{count} is not a multiple of %{multiple}.", name: name, count: count, @@ -126,8 +126,8 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :exclusive_max, length: max, value: value, name: name}) - when value >= max do + def message(%{reason: :exclusive_max, length: max, value: value, name: name}) + when value >= max do gettext("%{name} - %{value} is larger than exclusive maximum %{max}.", name: name, value: value, @@ -135,8 +135,8 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :maximum, length: max, value: value, name: name}) - when value > max do + def message(%{reason: :maximum, length: max, value: value, name: name}) + when value > max do gettext("%{name} - %{value} is larger than inclusive maximum %{max}.", name: name, value: value, @@ -144,8 +144,8 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :exclusive_multiple, length: min, value: value, name: name}) - when value <= min do + def message(%{reason: :exclusive_multiple, length: min, value: value, name: name}) + when value <= min do gettext("%{name} - %{value} is smaller than exclusive minimum %{min}.", name: name, value: value, @@ -153,8 +153,8 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :minimum, length: min, value: value, name: name}) - when value < min do + def message(%{reason: :minimum, length: min, value: value, name: name}) + when value < min do gettext("%{name} - %{value} is smaller than inclusive minimum %{min}.", name: name, value: value, @@ -162,7 +162,7 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :invalid_type, type: type, value: value, name: name}) do + def message(%{reason: :invalid_type, type: type, value: value, name: name}) do gettext("%{name} - Invalid %{type}. Got: %{value}.", name: name, value: OpenApiSpex.TermType.type(value), @@ -170,49 +170,49 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :invalid_format, format: format, name: name}) do + def message(%{reason: :invalid_format, format: format, name: name}) do gettext("%{name} - Invalid format. Expected %{format}.", name: name, format: inspect(format)) end - defp message(%{reason: :invalid_enum, name: name}) do + def message(%{reason: :invalid_enum, name: name}) do gettext("%{name} - Invalid value for enum.", name: name) end - defp message(%{reason: :polymorphic_failed, type: polymorphic_type}) do + def message(%{reason: :polymorphic_failed, type: polymorphic_type}) do gettext("Failed to cast to any schema in %{polymorphic_type}", polymorphic_type: polymorphic_type ) end - defp message(%{reason: :unexpected_field, name: name}) do + def message(%{reason: :unexpected_field, name: name}) do gettext("Unexpected field: %{name}.", name: safe_string(name)) end - defp message(%{reason: :no_value_for_discriminator, name: field}) do + def message(%{reason: :no_value_for_discriminator, name: field}) do gettext("Value used as discriminator for `%{field}` matches no schemas.", name: field) end - defp message(%{reason: :invalid_discriminator_value, name: field}) do + def message(%{reason: :invalid_discriminator_value, name: field}) do gettext("No value provided for required discriminator `%{field}`.", name: field) end - defp message(%{reason: :unknown_schema, name: name}) do + def message(%{reason: :unknown_schema, name: name}) do gettext("Unknown schema: %{name}.", name: name) end - defp message(%{reason: :missing_field, name: name}) do + def message(%{reason: :missing_field, name: name}) do gettext("Missing field: %{name}.", name: name) end - defp message(%{reason: :missing_header, name: name}) do + def message(%{reason: :missing_header, name: name}) do gettext("Missing header: %{name}.", name: name) end - defp message(%{reason: :invalid_header, name: name}) do + def message(%{reason: :invalid_header, name: name}) do gettext("Invalid value for header: %{name}.", name: name) end - defp message(%{reason: :max_properties, meta: meta}) do + def message(%{reason: :max_properties, meta: meta}) do gettext( "Object property count %{property_count} is greater than maxProperties: %{max_properties}.", property_count: meta.property_count, @@ -220,7 +220,7 @@ defmodule Pleroma.Web.ApiSpec.RenderError do ) end - defp message(%{reason: :min_properties, meta: meta}) do + def message(%{reason: :min_properties, meta: meta}) do gettext( "Object property count %{property_count} is less than minProperties: %{min_properties}", property_count: meta.property_count, diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 4f9696d52..2047e2439 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline + alias Pleroma.Web.ApiSpec.Errors.RegistrationUserError alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.MastodonAPI @@ -31,7 +32,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.TwitterAPI.TwitterAPI - plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug( + Pleroma.Web.ApiSpec.CastAndValidate, + [render_error: RegistrationUserError] when action in [:create] + ) + + plug(Pleroma.Web.ApiSpec.CastAndValidate when action not in [:create]) plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create) @@ -101,8 +107,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do with :ok <- validate_email_param(params), :ok <- TwitterAPI.validate_captcha(app, params), {:ok, user} <- TwitterAPI.register_user(params), - {_, {:ok, token}} <- - {:login, OAuthController.login(user, app, app.scopes)} do + {_, {:ok, token}} <- {:login, OAuthController.login(user, app, app.scopes)} do json(conn, OAuthView.render("token.json", %{user: user, token: token})) else {:login, {:account_status, :confirmation_pending}} -> @@ -126,8 +131,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do identifier: "manual_login_required" }) - {:error, error} -> - json_response(conn, :bad_request, %{error: error}) + {:error, errors} -> + json_response(conn, :bad_request, %{ + identifier: "review_submission", + error: "Please review the submission", + fields: errors + }) end end @@ -143,8 +152,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do defp validate_email_param(_) do case Pleroma.Config.get([:instance, :account_activation_required]) do - true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")} - _ -> :ok + true -> + {:error, + %{ + email: [dgettext("errors", "Missing parameter: %{name}", name: "email")] + }} + + _ -> + :ok end end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 5d7948507..92b071a81 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -3,14 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.TwitterAPI.TwitterAPI do - import Pleroma.Web.Gettext - alias Pleroma.Emails.Mailer alias Pleroma.Emails.UserEmail - alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserInviteToken + @spec register_user(map(), keyword()) :: {:ok, User.t()} | {:error, map()} def register_user(params, opts \\ []) do params = params @@ -28,18 +26,20 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do end end + @spec create_user_with_invite(map(), keyword()) :: {:ok, User.t()} | {:error, map()} defp create_user_with_invite(params, opts) do with %{token: token} when is_binary(token) <- params, - %UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, %{token: token}), + {:ok, invite} <- UserInviteToken.find_by_token(token), true <- UserInviteToken.valid_invite?(invite) do UserInviteToken.update_usage!(invite) create_user(params, opts) else - nil -> {:error, "Invalid token"} - _ -> {:error, "Expired token"} + nil -> {:error, %{invite: ["Invalid token"]}} + _ -> {:error, %{invite: ["Expired token"]}} end end + @spec create_user(map(), keyword()) :: {:ok, User.t()} | {:error, map()} defp create_user(params, opts) do changeset = User.register_changeset(%User{}, params, opts) @@ -52,7 +52,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do errors = changeset |> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end) - |> Jason.encode!() {:error, errors} end @@ -104,26 +103,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do ) do :ok else - {:error, :captcha_error} -> - captcha_error(dgettext("errors", "CAPTCHA Error")) - - {:error, :invalid} -> - captcha_error(dgettext("errors", "Invalid CAPTCHA")) - - {:error, :kocaptcha_service_unavailable} -> - captcha_error(dgettext("errors", "Kocaptcha service unavailable")) - - {:error, :expired} -> - captcha_error(dgettext("errors", "CAPTCHA expired")) - - {:error, :already_used} -> - captcha_error(dgettext("errors", "CAPTCHA already used")) - - {:error, :invalid_answer_data} -> - captcha_error(dgettext("errors", "Invalid answer data")) - {:error, error} -> - captcha_error(error) + {:error, %{captcha: [Pleroma.Captcha.Error.message(error)]}} end end @@ -131,12 +112,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do [:captcha_solution, :captcha_token, :captcha_answer_data] |> Enum.find_value(:ok, fn key -> unless is_binary(params[key]) do - error = dgettext("errors", "Invalid CAPTCHA (Missing parameter: %{name})", name: key) - {:error, error} + {:error, Pleroma.Captcha.Error.message(:missing_field, %{name: key})} end end) end - - # For some reason FE expects error message to be a serialized JSON - defp captcha_error(error), do: {:error, Jason.encode!(%{captcha: [error]})} end diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index f7f1369e4..69b5c6fb7 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -5,12 +5,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth.Token import Pleroma.Factory @@ -888,543 +886,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert %{"id" => _id, "blocking" => false} = json_response_and_validate_schema(conn, 200) end - describe "create account by app" do - setup do - valid_params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true - } - - [valid_params: valid_params] - end - - test "registers and logs in without :account_activation_required / :account_approval_required", - %{conn: conn} do - clear_config([:instance, :account_activation_required], false) - clear_config([:instance, :account_approval_required], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/apps", %{ - client_name: "client_name", - redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - scopes: "read, write, follow" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - clear_config([User, :email_blacklist], ["example.org"]) - - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true - } - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", params) - - assert %{"error" => "{\"email\":[\"Invalid email\"]}"} = - json_response_and_validate_schema(conn, 400) - - Pleroma.Config.put([User, :email_blacklist], []) - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", params) - - %{ - "access_token" => token, - "created_at" => _created_at, - "scope" => ^scope, - "token_type" => "Bearer" - } = json_response_and_validate_schema(conn, 200) - - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - user = Repo.preload(token_from_db, :user).user - - assert user - refute user.confirmation_pending - refute user.approval_pending - end - - test "registers but does not log in with :account_activation_required", %{conn: conn} do - clear_config([:instance, :account_activation_required], true) - clear_config([:instance, :account_approval_required], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/apps", %{ - client_name: "client_name", - redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - scopes: "read, write, follow" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true - }) - - response = json_response_and_validate_schema(conn, 200) - assert %{"identifier" => "missing_confirmed_email"} = response - refute response["access_token"] - refute response["token_type"] - - user = Repo.get_by(User, email: "lain@example.org") - assert user.confirmation_pending - end - - test "registers but does not log in with :account_approval_required", %{conn: conn} do - clear_config([:instance, :account_approval_required], true) - clear_config([:instance, :account_activation_required], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/apps", %{ - client_name: "client_name", - redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - scopes: "read, write, follow" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true, - reason: "I'm a cool dude, bro" - }) - - response = json_response_and_validate_schema(conn, 200) - assert %{"identifier" => "awaiting_approval"} = response - refute response["access_token"] - refute response["token_type"] - - user = Repo.get_by(User, email: "lain@example.org") - - assert user.approval_pending - assert user.registration_reason == "I'm a cool dude, bro" - end - - test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do - _user = insert(:user, email: "lain@example.org") - app_token = insert(:oauth_token, user: nil) - - res = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts", valid_params) - - assert json_response_and_validate_schema(res, 400) == %{ - "error" => "{\"email\":[\"has already been taken\"]}" - } - end - - test "returns bad_request if missing required params", %{ - conn: conn, - valid_params: valid_params - } do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - - res = post(conn, "/api/v1/accounts", valid_params) - assert json_response_and_validate_schema(res, 200) - - [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}] - |> Stream.zip(Map.delete(valid_params, :email)) - |> Enum.each(fn {ip, {attr, _}} -> - res = - conn - |> Map.put(:remote_ip, ip) - |> post("/api/v1/accounts", Map.delete(valid_params, attr)) - |> json_response_and_validate_schema(400) - - assert res == %{ - "error" => "Missing field: #{attr}.", - "errors" => [ - %{ - "message" => "Missing field: #{attr}", - "source" => %{"pointer" => "/#{attr}"}, - "title" => "Invalid value" - } - ] - } - end) - end - - test "returns bad_request if missing email params when :account_activation_required is enabled", - %{conn: conn, valid_params: valid_params} do - clear_config([:instance, :account_activation_required], true) - - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - - res = - conn - |> Map.put(:remote_ip, {127, 0, 0, 5}) - |> post("/api/v1/accounts", Map.delete(valid_params, :email)) - - assert json_response_and_validate_schema(res, 400) == - %{"error" => "Missing parameter: email"} - - res = - conn - |> Map.put(:remote_ip, {127, 0, 0, 6}) - |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) - - assert json_response_and_validate_schema(res, 400) == %{ - "error" => "{\"email\":[\"can't be blank\"]}" - } - end - - test "allow registration without an email", %{conn: conn, valid_params: valid_params} do - app_token = insert(:oauth_token, user: nil) - conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) - - res = - conn - |> put_req_header("content-type", "application/json") - |> Map.put(:remote_ip, {127, 0, 0, 7}) - |> post("/api/v1/accounts", Map.delete(valid_params, :email)) - - assert json_response_and_validate_schema(res, 200) - end - - test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do - app_token = insert(:oauth_token, user: nil) - conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) - - res = - conn - |> put_req_header("content-type", "application/json") - |> Map.put(:remote_ip, {127, 0, 0, 8}) - |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) - - assert json_response_and_validate_schema(res, 200) - end - - test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do - res = - conn - |> put_req_header("authorization", "Bearer " <> "invalid-token") - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts", valid_params) - - assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"} - end - - test "registration from trusted app" do - clear_config([Pleroma.Captcha, :enabled], true) - app = insert(:oauth_app, trusted: true, scopes: ["read", "write", "follow", "push"]) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "client_credentials", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token, "token_type" => "Bearer"} = json_response(conn, 200) - - response = - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer " <> token) - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts", %{ - nickname: "nickanme", - agreement: true, - email: "email@example.com", - fullname: "Lain", - username: "Lain", - password: "some_password", - confirm: "some_password" - }) - |> json_response_and_validate_schema(200) - - assert %{ - "access_token" => access_token, - "created_at" => _, - "scope" => "read write follow push", - "token_type" => "Bearer" - } = response - - response = - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token) - |> get("/api/v1/accounts/verify_credentials") - |> json_response_and_validate_schema(200) - - assert %{ - "acct" => "Lain", - "bot" => false, - "display_name" => "Lain", - "follow_requests_count" => 0, - "followers_count" => 0, - "following_count" => 0, - "locked" => false, - "note" => "", - "source" => %{ - "fields" => [], - "note" => "", - "pleroma" => %{ - "actor_type" => "Person", - "discoverable" => false, - "no_rich_text" => false, - "show_role" => true - }, - "privacy" => "public", - "sensitive" => false - }, - "statuses_count" => 0, - "username" => "Lain" - } = response - end - end - - describe "create account by app / rate limit" do - setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2}) - - test "respects rate limit setting", %{conn: conn} do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> Map.put(:remote_ip, {15, 15, 15, 15}) - |> put_req_header("content-type", "multipart/form-data") - - for i <- 1..2 do - conn = - conn - |> post("/api/v1/accounts", %{ - username: "#{i}lain", - email: "#{i}lain@example.org", - password: "PlzDontHackLain", - agreement: true - }) - - %{ - "access_token" => token, - "created_at" => _created_at, - "scope" => _scope, - "token_type" => "Bearer" - } = json_response_and_validate_schema(conn, 200) - - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - token_from_db = Repo.preload(token_from_db, :user) - assert token_from_db.user - end - - conn = - post(conn, "/api/v1/accounts", %{ - username: "6lain", - email: "6lain@example.org", - password: "PlzDontHackLain", - agreement: true - }) - - assert json_response_and_validate_schema(conn, :too_many_requests) == %{ - "error" => "Throttled" - } - end - end - - describe "create account with enabled captcha" do - setup %{conn: conn} do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "multipart/form-data") - - [conn: conn] - end - - setup do: clear_config([Pleroma.Captcha, :enabled], true) - - test "creates an account and returns 200 if captcha is valid", %{conn: conn} do - %{token: token, answer_data: answer_data} = Pleroma.Captcha.new() - - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: Pleroma.Captcha.Mock.solution(), - captcha_token: token, - captcha_answer_data: answer_data - } - - assert %{ - "access_token" => access_token, - "created_at" => _, - "scope" => "read", - "token_type" => "Bearer" - } = - conn - |> post("/api/v1/accounts", params) - |> json_response_and_validate_schema(:ok) - - assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user) - - Cachex.del(:used_captcha_cache, token) - end - - test "returns 400 if any captcha field is not provided", %{conn: conn} do - captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data] - - valid_params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: "xx", - captcha_token: "xx", - captcha_answer_data: "xx" - } - - for field <- captcha_fields do - expected = %{ - "error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}" - } - - assert expected == - conn - |> post("/api/v1/accounts", Map.delete(valid_params, field)) - |> json_response_and_validate_schema(:bad_request) - end - end - - test "returns an error if captcha is invalid", %{conn: conn} do - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: "cofe", - captcha_token: "cofe", - captcha_answer_data: "cofe" - } - - assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} == - conn - |> post("/api/v1/accounts", params) - |> json_response_and_validate_schema(:bad_request) - end - end - describe "GET /api/v1/accounts/:id/lists - account_lists" do test "returns lists to which the account belongs" do %{user: user, conn: conn} = oauth_access(["read:lists"]) diff --git a/test/pleroma/web/mastodon_api/registration_user_test.exs b/test/pleroma/web/mastodon_api/registration_user_test.exs new file mode 100644 index 000000000..086d29832 --- /dev/null +++ b/test/pleroma/web/mastodon_api/registration_user_test.exs @@ -0,0 +1,691 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.RegistrationUserTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserInviteToken + alias Pleroma.Web.OAuth.Token + + import Pleroma.Factory + + describe "create account by app" do + setup do + valid_params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true + } + + [valid_params: valid_params] + end + + test "registers and logs in without :account_activation_required / :account_approval_required", + %{conn: conn} do + clear_config([:instance, :account_activation_required], false) + clear_config([:instance, :account_approval_required], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + clear_config([User, :email_blacklist], ["example.org"]) + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true + } + + response = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(400) + + assert response == %{ + "error" => "Please review the submission", + "fields" => %{"email" => ["Invalid email"]}, + "identifier" => "review_submission" + } + + Pleroma.Config.put([User, :email_blacklist], []) + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", params) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => ^scope, + "token_type" => "Bearer" + } = json_response_and_validate_schema(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + user = Repo.preload(token_from_db, :user).user + + assert user + refute user.confirmation_pending + refute user.approval_pending + end + + test "registers but does not log in with :account_activation_required", %{conn: conn} do + clear_config([:instance, :account_activation_required], true) + clear_config([:instance, :account_approval_required], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true + }) + + response = json_response_and_validate_schema(conn, 200) + assert %{"identifier" => "missing_confirmed_email"} = response + refute response["access_token"] + refute response["token_type"] + + user = Repo.get_by(User, email: "lain@example.org") + assert user.confirmation_pending + end + + test "registers but does not log in with :account_approval_required", %{conn: conn} do + clear_config([:instance, :account_approval_required], true) + clear_config([:instance, :account_activation_required], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true, + reason: "I'm a cool dude, bro" + }) + + response = json_response_and_validate_schema(conn, 200) + assert %{"identifier" => "awaiting_approval"} = response + refute response["access_token"] + refute response["token_type"] + + user = Repo.get_by(User, email: "lain@example.org") + + assert user.approval_pending + assert user.registration_reason == "I'm a cool dude, bro" + end + + test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do + _user = insert(:user, email: "lain@example.org") + app_token = insert(:oauth_token, user: nil) + + res = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts", valid_params) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Please review the submission", + "fields" => %{"email" => ["has already been taken"]}, + "identifier" => "review_submission" + } + end + + test "returns bad_request if missing required params", %{ + conn: conn, + valid_params: valid_params + } do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + + res = post(conn, "/api/v1/accounts", valid_params) + assert json_response_and_validate_schema(res, 200) + + [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}] + |> Stream.zip(Map.delete(valid_params, :email)) + |> Enum.each(fn {ip, {attr, _}} -> + res = + conn + |> Map.put(:remote_ip, ip) + |> post("/api/v1/accounts", Map.delete(valid_params, attr)) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Please review the submission", + "fields" => %{"#{attr}" => ["Missing field: #{attr}."]}, + "identifier" => "review_submission" + } + end) + end + + test "returns bad_request if missing email params when :account_activation_required is enabled", + %{conn: conn, valid_params: valid_params} do + clear_config([:instance, :account_activation_required], true) + + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 5}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Please review the submission", + "fields" => %{"email" => ["Missing parameter: email"]}, + "identifier" => "review_submission" + } + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 6}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Please review the submission", + "fields" => %{"email" => ["can't be blank"]}, + "identifier" => "review_submission" + } + end + + test "allow registration without an email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> put_req_header("content-type", "application/json") + |> Map.put(:remote_ip, {127, 0, 0, 7}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + + assert json_response_and_validate_schema(res, 200) + end + + test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> put_req_header("content-type", "application/json") + |> Map.put(:remote_ip, {127, 0, 0, 8}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + + assert json_response_and_validate_schema(res, 200) + end + + test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do + res = + conn + |> put_req_header("authorization", "Bearer " <> "invalid-token") + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts", valid_params) + + assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"} + end + + test "registration from trusted app" do + clear_config([Pleroma.Captcha, :enabled], true) + app = insert(:oauth_app, trusted: true, scopes: ["read", "write", "follow", "push"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "client_credentials", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "token_type" => "Bearer"} = json_response(conn, 200) + + response = + build_conn() + |> Plug.Conn.put_req_header("authorization", "Bearer " <> token) + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts", %{ + nickname: "nickanme", + agreement: true, + email: "email@example.com", + fullname: "Lain", + username: "Lain", + password: "some_password", + confirm: "some_password" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "access_token" => access_token, + "created_at" => _, + "scope" => "read write follow push", + "token_type" => "Bearer" + } = response + + response = + build_conn() + |> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token) + |> get("/api/v1/accounts/verify_credentials") + |> json_response_and_validate_schema(200) + + assert %{ + "acct" => "Lain", + "bot" => false, + "display_name" => "Lain", + "follow_requests_count" => 0, + "followers_count" => 0, + "following_count" => 0, + "locked" => false, + "note" => "", + "source" => %{ + "fields" => [], + "note" => "", + "pleroma" => %{ + "actor_type" => "Person", + "discoverable" => false, + "no_rich_text" => false, + "show_role" => true + }, + "privacy" => "public", + "sensitive" => false + }, + "statuses_count" => 0, + "username" => "Lain" + } = response + end + end + + describe "create account by app / rate limit" do + setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2}) + + test "respects rate limit setting", %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> Map.put(:remote_ip, {15, 15, 15, 15}) + |> put_req_header("content-type", "multipart/form-data") + + for i <- 1..2 do + conn = + conn + |> post("/api/v1/accounts", %{ + username: "#{i}lain", + email: "#{i}lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => _scope, + "token_type" => "Bearer" + } = json_response_and_validate_schema(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + token_from_db = Repo.preload(token_from_db, :user) + assert token_from_db.user + end + + conn = + post(conn, "/api/v1/accounts", %{ + username: "6lain", + email: "6lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + assert json_response_and_validate_schema(conn, :too_many_requests) == %{ + "error" => "Throttled" + } + end + end + + describe "create account via invite" do + setup %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "multipart/form-data") + + [conn: conn] + end + + setup do: clear_config([:instance, :registrations_open], false) + setup do: clear_config([Pleroma.Captcha, :enabled], false) + + test "creates an account", %{conn: conn} do + invite = insert(:user_invite_token, %{invite_type: "one_time"}) + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + token: invite.token + } + + res = + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:ok) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + + assert %{ + "access_token" => access_token, + "created_at" => _, + "scope" => "read", + "token_type" => "Bearer" + } = res + + user = Repo.get_by(Token, token: access_token) |> Repo.preload(:user) |> Map.get(:user) + assert user.email == "lain@example.org" + end + + test "returns error when already used", %{conn: conn} do + invite = insert(:user_invite_token, %{used: true, invite_type: "one_time"}) + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + token: invite.token + } + + res = + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Please review the submission", + "fields" => %{"invite" => ["Expired token"]}, + "identifier" => "review_submission" + } + end + + test "returns errors when invite is invalid", %{conn: conn} do + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + token: "fake-token" + } + + res = + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Please review the submission", + "fields" => %{"invite" => ["Invalid token"]}, + "identifier" => "review_submission" + } + end + end + + describe "create account with enabled captcha" do + setup %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "multipart/form-data") + + [conn: conn] + end + + setup do: clear_config([Pleroma.Captcha, :enabled], true) + + test "creates an account and returns 200 if captcha is valid", %{conn: conn} do + %{token: token, answer_data: answer_data} = Pleroma.Captcha.new() + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: Pleroma.Captcha.Mock.solution(), + captcha_token: token, + captcha_answer_data: answer_data + } + + assert %{ + "access_token" => access_token, + "created_at" => _, + "scope" => "read", + "token_type" => "Bearer" + } = + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:ok) + + assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user) + + Cachex.del(:used_captcha_cache, token) + end + + test "returns 400 if any captcha field is not provided", %{conn: conn} do + captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data] + + valid_params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: "xx", + captcha_token: "xx", + captcha_answer_data: "xx" + } + + for field <- captcha_fields do + expected = %{ + "error" => "Please review the submission", + "fields" => %{"captcha" => ["Invalid CAPTCHA (Missing parameter: #{field})"]}, + "identifier" => "review_submission" + } + + assert expected == + conn + |> post("/api/v1/accounts", Map.delete(valid_params, field)) + |> json_response_and_validate_schema(:bad_request) + end + end + + test "returns an error if captcha is invalid", %{conn: conn} do + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: "cofe", + captcha_token: "cofe", + captcha_answer_data: "cofe" + } + + assert %{ + "error" => "Please review the submission", + "fields" => %{"captcha" => ["Invalid answer data"]}, + "identifier" => "review_submission" + } == + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:bad_request) + end + end + + describe "api spec errors" do + setup %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "multipart/form-data") + + [conn: conn] + end + + setup do: clear_config([:instance, :registrations_open], true) + setup do: clear_config([Pleroma.Captcha, :enabled], false) + + test "returns errors when missed required field", %{conn: conn} do + params = %{ + email: "lain@example.org", + agreement: true + } + + assert %{ + "error" => "Please review the submission", + "fields" => %{ + "password" => ["Missing field: password."], + "username" => ["Missing field: username."] + }, + "identifier" => "review_submission" + } == + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:bad_request) + end + end +end diff --git a/test/pleroma/web/twitter_api/twitter_api_test.exs b/test/pleroma/web/twitter_api/twitter_api_test.exs index 20a45cb6f..d433b507b 100644 --- a/test/pleroma/web/twitter_api/twitter_api_test.exs +++ b/test/pleroma/web/twitter_api/twitter_api_test.exs @@ -183,7 +183,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, msg} = TwitterAPI.register_user(data) - assert msg == "Invalid token" + assert msg == %{invite: ["Invalid token"]} refute User.get_cached_by_nickname("GrimReaper") end @@ -203,7 +203,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, msg} = TwitterAPI.register_user(data) - assert msg == "Expired token" + assert msg == %{invite: ["Expired token"]} refute User.get_cached_by_nickname("GrimReaper") end end @@ -258,7 +258,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, msg} = TwitterAPI.register_user(data) - assert msg == "Expired token" + assert msg == %{invite: ["Expired token"]} refute User.get_cached_by_nickname("vinny") invite = Repo.get_by(UserInviteToken, token: invite.token) @@ -302,7 +302,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, msg} = TwitterAPI.register_user(data) - assert msg == "Expired token" + assert msg == %{invite: ["Expired token"]} refute User.get_cached_by_nickname("GrimReaper") end end @@ -363,7 +363,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, msg} = TwitterAPI.register_user(data) - assert msg == "Expired token" + assert msg == %{invite: ["Expired token"]} refute User.get_cached_by_nickname("GrimReaper") end @@ -383,7 +383,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, msg} = TwitterAPI.register_user(data) - assert msg == "Expired token" + assert msg == %{invite: ["Expired token"]} refute User.get_cached_by_nickname("GrimReaper") end @@ -405,7 +405,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, msg} = TwitterAPI.register_user(data) - assert msg == "Expired token" + assert msg == %{invite: ["Expired token"]} refute User.get_cached_by_nickname("GrimReaper") end end @@ -420,7 +420,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, error} = TwitterAPI.register_user(data) - assert is_binary(error) + assert error == %{ + password: ["can't be blank"], + password_confirmation: ["can't be blank"] + } + refute User.get_cached_by_nickname("lain") end diff --git a/test/support/factory.ex b/test/support/factory.ex index fb82be0c4..b4f745413 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -442,4 +442,13 @@ defmodule Pleroma.Factory do phrase: "cofe" } end + + def user_invite_token_factory do + %Pleroma.UserInviteToken{ + token: Base.url_encode64(:crypto.strong_rand_bytes(32)), + used: false, + invite_type: "one_time", + uses: 0 + } + end end