Move invite actions to AdminAPI.InviteTokenController
authorEgor Kislitsyn <egor@kislitsyn.com>
Tue, 26 May 2020 09:13:39 +0000 (13:13 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Tue, 26 May 2020 11:03:07 +0000 (15:03 +0400)
lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
lib/pleroma/web/admin_api/controllers/invite_token_controller.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
test/web/admin_api/controllers/admin_api_controller_test.exs
test/web/admin_api/controllers/invite_token_controller_test.exs [new file with mode: 0644]

index 6b1d64a2eb3152f3ae0f916283f6af2fc7cf2162..95582b0088907e69039e9137ac7c2ff6f73da17a 100644 (file)
@@ -16,7 +16,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   alias Pleroma.ReportNote
   alias Pleroma.Stats
   alias Pleroma.User
-  alias Pleroma.UserInviteToken
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.Pipeline
@@ -69,14 +68,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
          ]
   )
 
-  plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
-
-  plug(
-    OAuthScopesPlug,
-    %{scopes: ["write:invites"], admin: true}
-    when action in [:create_invite_token, :revoke_invite, :email_invite]
-  )
-
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:follows"], admin: true}
@@ -575,69 +566,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     end
   end
 
-  @doc "Sends registration invite via email"
-  def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
-    with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
-         {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
-         {:ok, invite_token} <- UserInviteToken.create_invite(),
-         email <-
-           Pleroma.Emails.UserEmail.user_invitation_email(
-             user,
-             invite_token,
-             email,
-             params["name"]
-           ),
-         {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
-      json_response(conn, :no_content, "")
-    else
-      {:registrations_open, _} ->
-        {:error, "To send invites you need to set the `registrations_open` option to false."}
-
-      {:invites_enabled, _} ->
-        {:error, "To send invites you need to set the `invites_enabled` option to true."}
-    end
-  end
-
-  @doc "Create an account registration invite token"
-  def create_invite_token(conn, params) do
-    opts = %{}
-
-    opts =
-      if params["max_use"],
-        do: Map.put(opts, :max_use, params["max_use"]),
-        else: opts
-
-    opts =
-      if params["expires_at"],
-        do: Map.put(opts, :expires_at, params["expires_at"]),
-        else: opts
-
-    {:ok, invite} = UserInviteToken.create_invite(opts)
-
-    json(conn, AccountView.render("invite.json", %{invite: invite}))
-  end
-
-  @doc "Get list of created invites"
-  def invites(conn, _params) do
-    invites = UserInviteToken.list_invites()
-
-    conn
-    |> put_view(AccountView)
-    |> render("invites.json", %{invites: invites})
-  end
-
-  @doc "Revokes invite by token"
-  def revoke_invite(conn, %{"token" => token}) do
-    with {:ok, invite} <- UserInviteToken.find_by_token(token),
-         {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
-      conn
-      |> put_view(AccountView)
-      |> render("invite.json", %{invite: updated_invite})
-    else
-      nil -> {:error, :not_found}
-    end
-  end
-
   @doc "Get a password reset token (base64 string) for given nickname"
   def get_password_reset(conn, %{"nickname" => nickname}) do
     (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
diff --git a/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex
new file mode 100644 (file)
index 0000000..a0291e9
--- /dev/null
@@ -0,0 +1,88 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.InviteTokenController do
+  use Pleroma.Web, :controller
+
+  import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+  alias Pleroma.Config
+  alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.UserInviteToken
+  alias Pleroma.Web.AdminAPI.AccountView
+
+  require Logger
+
+  plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :index)
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:invites"], admin: true} when action in [:create, :revoke, :email]
+  )
+
+  action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+  @doc "Get list of created invites"
+  def index(conn, _params) do
+    invites = UserInviteToken.list_invites()
+
+    conn
+    |> put_view(AccountView)
+    |> render("invites.json", %{invites: invites})
+  end
+
+  @doc "Create an account registration invite token"
+  def create(conn, params) do
+    opts = %{}
+
+    opts =
+      if params["max_use"],
+        do: Map.put(opts, :max_use, params["max_use"]),
+        else: opts
+
+    opts =
+      if params["expires_at"],
+        do: Map.put(opts, :expires_at, params["expires_at"]),
+        else: opts
+
+    {:ok, invite} = UserInviteToken.create_invite(opts)
+
+    json(conn, AccountView.render("invite.json", %{invite: invite}))
+  end
+
+  @doc "Revokes invite by token"
+  def revoke(conn, %{"token" => token}) do
+    with {:ok, invite} <- UserInviteToken.find_by_token(token),
+         {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
+      conn
+      |> put_view(AccountView)
+      |> render("invite.json", %{invite: updated_invite})
+    else
+      nil -> {:error, :not_found}
+    end
+  end
+
+  @doc "Sends registration invite via email"
+  def email(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
+    with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
+         {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
+         {:ok, invite_token} <- UserInviteToken.create_invite(),
+         email <-
+           Pleroma.Emails.UserEmail.user_invitation_email(
+             user,
+             invite_token,
+             email,
+             params["name"]
+           ),
+         {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
+      json_response(conn, :no_content, "")
+    else
+      {:registrations_open, _} ->
+        {:error, "To send invites you need to set the `registrations_open` option to false."}
+
+      {:invites_enabled, _} ->
+        {:error, "To send invites you need to set the `invites_enabled` option to true."}
+    end
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex
new file mode 100644 (file)
index 0000000..09a7735
--- /dev/null
@@ -0,0 +1,165 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.InviteTokenOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Schemas.Account
+  alias Pleroma.Web.ApiSpec.Schemas.ApiError
+  alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+  alias Pleroma.Web.ApiSpec.Schemas.Status
+  alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+  import Pleroma.Web.ApiSpec.Helpers
+  import Pleroma.Web.ApiSpec.StatusOperation, only: [id_param: 0]
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def index_operation do
+    %Operation{
+      tags: ["Admin", "Statuses"],
+      operationId: "AdminAPI.StatusController.index",
+      security: [%{"oAuth" => ["read:statuses"]}],
+      parameters: [
+        Operation.parameter(
+          :godmode,
+          :query,
+          %Schema{type: :boolean, default: false},
+          "Allows to see private statuses"
+        ),
+        Operation.parameter(
+          :local_only,
+          :query,
+          %Schema{type: :boolean, default: false},
+          "Excludes remote statuses"
+        ),
+        Operation.parameter(
+          :with_reblogs,
+          :query,
+          %Schema{type: :boolean, default: false},
+          "Allows to see reblogs"
+        ),
+        Operation.parameter(
+          :page,
+          :query,
+          %Schema{type: :integer, default: 1},
+          "Page"
+        ),
+        Operation.parameter(
+          :page_size,
+          :query,
+          %Schema{type: :integer, default: 50},
+          "Number of statuses to return"
+        )
+      ],
+      responses: %{
+        200 =>
+          Operation.response("Array of statuses", "application/json", %Schema{
+            type: :array,
+            items: status()
+          })
+      }
+    }
+  end
+
+  def show_operation do
+    %Operation{
+      tags: ["Admin", "Statuses"],
+      summary: "Show Status",
+      operationId: "AdminAPI.StatusController.show",
+      parameters: [id_param()],
+      security: [%{"oAuth" => ["read:statuses"]}],
+      responses: %{
+        200 => Operation.response("Status", "application/json", Status),
+        404 => Operation.response("Not Found", "application/json", ApiError)
+      }
+    }
+  end
+
+  def update_operation do
+    %Operation{
+      tags: ["Admin", "Statuses"],
+      summary: "Change the scope of an individual reported status",
+      operationId: "AdminAPI.StatusController.update",
+      parameters: [id_param()],
+      security: [%{"oAuth" => ["write:statuses"]}],
+      requestBody: request_body("Parameters", update_request(), required: true),
+      responses: %{
+        200 => Operation.response("Status", "application/json", Status),
+        400 => Operation.response("Error", "application/json", ApiError)
+      }
+    }
+  end
+
+  def delete_operation do
+    %Operation{
+      tags: ["Admin", "Statuses"],
+      summary: "Delete an individual reported status",
+      operationId: "AdminAPI.StatusController.delete",
+      parameters: [id_param()],
+      security: [%{"oAuth" => ["write:statuses"]}],
+      responses: %{
+        200 => empty_object_response(),
+        404 => Operation.response("Not Found", "application/json", ApiError)
+      }
+    }
+  end
+
+  defp status do
+    %Schema{
+      anyOf: [
+        Status,
+        %Schema{
+          type: :object,
+          properties: %{
+            account: %Schema{allOf: [Account, admin_account()]}
+          }
+        }
+      ]
+    }
+  end
+
+  defp admin_account do
+    %Schema{
+      type: :object,
+      properties: %{
+        id: FlakeID,
+        avatar: %Schema{type: :string},
+        nickname: %Schema{type: :string},
+        display_name: %Schema{type: :string},
+        deactivated: %Schema{type: :boolean},
+        local: %Schema{type: :boolean},
+        roles: %Schema{
+          type: :object,
+          properties: %{
+            admin: %Schema{type: :boolean},
+            moderator: %Schema{type: :boolean}
+          }
+        },
+        tags: %Schema{type: :string},
+        confirmation_pending: %Schema{type: :string}
+      }
+    }
+  end
+
+  defp update_request do
+    %Schema{
+      type: :object,
+      properties: %{
+        sensitive: %Schema{
+          type: :boolean,
+          description: "Mark status and attached media as sensitive?"
+        },
+        visibility: VisibilityScope
+      },
+      example: %{
+        "visibility" => "private",
+        "sensitive" => "false"
+      }
+    }
+  end
+end
index e493a41534bb0f8b3a0a11e0eb6ec7714e4fd803..fe36f01896784fa4f6dcb68e9a5be1e985f97883 100644 (file)
@@ -164,10 +164,10 @@ defmodule Pleroma.Web.Router do
     post("/relay", AdminAPIController, :relay_follow)
     delete("/relay", AdminAPIController, :relay_unfollow)
 
-    post("/users/invite_token", AdminAPIController, :create_invite_token)
-    get("/users/invites", AdminAPIController, :invites)
-    post("/users/revoke_invite", AdminAPIController, :revoke_invite)
-    post("/users/email_invite", AdminAPIController, :email_invite)
+    post("/users/invite_token", InviteTokenController, :create)
+    get("/users/invites", InviteTokenController, :index)
+    post("/users/revoke_invite", InviteTokenController, :revoke)
+    post("/users/email_invite", InviteTokenController, :email)
 
     get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
     patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
index 321840a8c31ff34222ca991ca3459ba6073a4859..f7e163f57a69a49520909195bf7966d797125af5 100644 (file)
@@ -20,7 +20,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
   alias Pleroma.ReportNote
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
-  alias Pleroma.UserInviteToken
   alias Pleroma.Web
   alias Pleroma.Web.ActivityPub.Relay
   alias Pleroma.Web.CommonAPI
@@ -588,122 +587,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
   end
 
-  describe "POST /api/pleroma/admin/email_invite, with valid config" do
-    setup do: clear_config([:instance, :registrations_open], false)
-    setup do: clear_config([:instance, :invites_enabled], true)
-
-    test "sends invitation and returns 204", %{admin: admin, conn: conn} do
-      recipient_email = "foo@bar.com"
-      recipient_name = "J. D."
-
-      conn =
-        post(
-          conn,
-          "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
-        )
-
-      assert json_response(conn, :no_content)
-
-      token_record = List.last(Repo.all(Pleroma.UserInviteToken))
-      assert token_record
-      refute token_record.used
-
-      notify_email = Config.get([:instance, :notify_email])
-      instance_name = Config.get([:instance, :name])
-
-      email =
-        Pleroma.Emails.UserEmail.user_invitation_email(
-          admin,
-          token_record,
-          recipient_email,
-          recipient_name
-        )
-
-      Swoosh.TestAssertions.assert_email_sent(
-        from: {instance_name, notify_email},
-        to: {recipient_name, recipient_email},
-        html_body: email.html_body
-      )
-    end
-
-    test "it returns 403 if requested by a non-admin" do
-      non_admin_user = insert(:user)
-      token = insert(:oauth_token, user: non_admin_user)
-
-      conn =
-        build_conn()
-        |> assign(:user, non_admin_user)
-        |> assign(:token, token)
-        |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
-
-      assert json_response(conn, :forbidden)
-    end
-
-    test "email with +", %{conn: conn, admin: admin} do
-      recipient_email = "foo+bar@baz.com"
-
-      conn
-      |> put_req_header("content-type", "application/json;charset=utf-8")
-      |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email})
-      |> json_response(:no_content)
-
-      token_record =
-        Pleroma.UserInviteToken
-        |> Repo.all()
-        |> List.last()
-
-      assert token_record
-      refute token_record.used
-
-      notify_email = Config.get([:instance, :notify_email])
-      instance_name = Config.get([:instance, :name])
-
-      email =
-        Pleroma.Emails.UserEmail.user_invitation_email(
-          admin,
-          token_record,
-          recipient_email
-        )
-
-      Swoosh.TestAssertions.assert_email_sent(
-        from: {instance_name, notify_email},
-        to: recipient_email,
-        html_body: email.html_body
-      )
-    end
-  end
-
-  describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
-    setup do: clear_config([:instance, :registrations_open])
-    setup do: clear_config([:instance, :invites_enabled])
-
-    test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
-      Config.put([:instance, :registrations_open], false)
-      Config.put([:instance, :invites_enabled], false)
-
-      conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
-
-      assert json_response(conn, :bad_request) ==
-               %{
-                 "error" =>
-                   "To send invites you need to set the `invites_enabled` option to true."
-               }
-    end
-
-    test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
-      Config.put([:instance, :registrations_open], true)
-      Config.put([:instance, :invites_enabled], true)
-
-      conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
-
-      assert json_response(conn, :bad_request) ==
-               %{
-                 "error" =>
-                   "To send invites you need to set the `registrations_open` option to false."
-               }
-    end
-  end
-
   test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
     user = insert(:user)
 
@@ -1318,112 +1201,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
   end
 
-  describe "POST /api/pleroma/admin/users/invite_token" do
-    test "without options", %{conn: conn} do
-      conn = post(conn, "/api/pleroma/admin/users/invite_token")
-
-      invite_json = json_response(conn, 200)
-      invite = UserInviteToken.find_by_token!(invite_json["token"])
-      refute invite.used
-      refute invite.expires_at
-      refute invite.max_use
-      assert invite.invite_type == "one_time"
-    end
-
-    test "with expires_at", %{conn: conn} do
-      conn =
-        post(conn, "/api/pleroma/admin/users/invite_token", %{
-          "expires_at" => Date.to_string(Date.utc_today())
-        })
-
-      invite_json = json_response(conn, 200)
-      invite = UserInviteToken.find_by_token!(invite_json["token"])
-
-      refute invite.used
-      assert invite.expires_at == Date.utc_today()
-      refute invite.max_use
-      assert invite.invite_type == "date_limited"
-    end
-
-    test "with max_use", %{conn: conn} do
-      conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
-
-      invite_json = json_response(conn, 200)
-      invite = UserInviteToken.find_by_token!(invite_json["token"])
-      refute invite.used
-      refute invite.expires_at
-      assert invite.max_use == 150
-      assert invite.invite_type == "reusable"
-    end
-
-    test "with max use and expires_at", %{conn: conn} do
-      conn =
-        post(conn, "/api/pleroma/admin/users/invite_token", %{
-          "max_use" => 150,
-          "expires_at" => Date.to_string(Date.utc_today())
-        })
-
-      invite_json = json_response(conn, 200)
-      invite = UserInviteToken.find_by_token!(invite_json["token"])
-      refute invite.used
-      assert invite.expires_at == Date.utc_today()
-      assert invite.max_use == 150
-      assert invite.invite_type == "reusable_date_limited"
-    end
-  end
-
-  describe "GET /api/pleroma/admin/users/invites" do
-    test "no invites", %{conn: conn} do
-      conn = get(conn, "/api/pleroma/admin/users/invites")
-
-      assert json_response(conn, 200) == %{"invites" => []}
-    end
-
-    test "with invite", %{conn: conn} do
-      {:ok, invite} = UserInviteToken.create_invite()
-
-      conn = get(conn, "/api/pleroma/admin/users/invites")
-
-      assert json_response(conn, 200) == %{
-               "invites" => [
-                 %{
-                   "expires_at" => nil,
-                   "id" => invite.id,
-                   "invite_type" => "one_time",
-                   "max_use" => nil,
-                   "token" => invite.token,
-                   "used" => false,
-                   "uses" => 0
-                 }
-               ]
-             }
-    end
-  end
-
-  describe "POST /api/pleroma/admin/users/revoke_invite" do
-    test "with token", %{conn: conn} do
-      {:ok, invite} = UserInviteToken.create_invite()
-
-      conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
-
-      assert json_response(conn, 200) == %{
-               "expires_at" => nil,
-               "id" => invite.id,
-               "invite_type" => "one_time",
-               "max_use" => nil,
-               "token" => invite.token,
-               "used" => true,
-               "uses" => 0
-             }
-    end
-
-    test "with invalid token", %{conn: conn} do
-      conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
-
-      assert json_response(conn, :not_found) == %{"error" => "Not found"}
-    end
-  end
-
   describe "GET /api/pleroma/admin/reports/:id" do
     test "returns report by its id", %{conn: conn} do
       [reporter, target_user] = insert_pair(:user)
diff --git a/test/web/admin_api/controllers/invite_token_controller_test.exs b/test/web/admin_api/controllers/invite_token_controller_test.exs
new file mode 100644 (file)
index 0000000..eb57b4d
--- /dev/null
@@ -0,0 +1,247 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.InviteTokenControllerTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  import Pleroma.Factory
+
+  alias Pleroma.Config
+  alias Pleroma.Repo
+  alias Pleroma.UserInviteToken
+
+  setup do
+    admin = insert(:user, is_admin: true)
+    token = insert(:oauth_admin_token, user: admin)
+
+    conn =
+      build_conn()
+      |> assign(:user, admin)
+      |> assign(:token, token)
+
+    {:ok, %{admin: admin, token: token, conn: conn}}
+  end
+
+  describe "POST /api/pleroma/admin/users/email_invite, with valid config" do
+    setup do: clear_config([:instance, :registrations_open], false)
+    setup do: clear_config([:instance, :invites_enabled], true)
+
+    test "sends invitation and returns 204", %{admin: admin, conn: conn} do
+      recipient_email = "foo@bar.com"
+      recipient_name = "J. D."
+
+      conn =
+        post(
+          conn,
+          "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
+        )
+
+      assert json_response(conn, :no_content)
+
+      token_record = List.last(Repo.all(Pleroma.UserInviteToken))
+      assert token_record
+      refute token_record.used
+
+      notify_email = Config.get([:instance, :notify_email])
+      instance_name = Config.get([:instance, :name])
+
+      email =
+        Pleroma.Emails.UserEmail.user_invitation_email(
+          admin,
+          token_record,
+          recipient_email,
+          recipient_name
+        )
+
+      Swoosh.TestAssertions.assert_email_sent(
+        from: {instance_name, notify_email},
+        to: {recipient_name, recipient_email},
+        html_body: email.html_body
+      )
+    end
+
+    test "it returns 403 if requested by a non-admin" do
+      non_admin_user = insert(:user)
+      token = insert(:oauth_token, user: non_admin_user)
+
+      conn =
+        build_conn()
+        |> assign(:user, non_admin_user)
+        |> assign(:token, token)
+        |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
+
+      assert json_response(conn, :forbidden)
+    end
+
+    test "email with +", %{conn: conn, admin: admin} do
+      recipient_email = "foo+bar@baz.com"
+
+      conn
+      |> put_req_header("content-type", "application/json;charset=utf-8")
+      |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email})
+      |> json_response(:no_content)
+
+      token_record =
+        Pleroma.UserInviteToken
+        |> Repo.all()
+        |> List.last()
+
+      assert token_record
+      refute token_record.used
+
+      notify_email = Config.get([:instance, :notify_email])
+      instance_name = Config.get([:instance, :name])
+
+      email =
+        Pleroma.Emails.UserEmail.user_invitation_email(
+          admin,
+          token_record,
+          recipient_email
+        )
+
+      Swoosh.TestAssertions.assert_email_sent(
+        from: {instance_name, notify_email},
+        to: recipient_email,
+        html_body: email.html_body
+      )
+    end
+  end
+
+  describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
+    setup do: clear_config([:instance, :registrations_open])
+    setup do: clear_config([:instance, :invites_enabled])
+
+    test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
+      Config.put([:instance, :registrations_open], false)
+      Config.put([:instance, :invites_enabled], false)
+
+      conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
+
+      assert json_response(conn, :bad_request) ==
+               %{
+                 "error" =>
+                   "To send invites you need to set the `invites_enabled` option to true."
+               }
+    end
+
+    test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
+      Config.put([:instance, :registrations_open], true)
+      Config.put([:instance, :invites_enabled], true)
+
+      conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
+
+      assert json_response(conn, :bad_request) ==
+               %{
+                 "error" =>
+                   "To send invites you need to set the `registrations_open` option to false."
+               }
+    end
+  end
+
+  describe "POST /api/pleroma/admin/users/invite_token" do
+    test "without options", %{conn: conn} do
+      conn = post(conn, "/api/pleroma/admin/users/invite_token")
+
+      invite_json = json_response(conn, 200)
+      invite = UserInviteToken.find_by_token!(invite_json["token"])
+      refute invite.used
+      refute invite.expires_at
+      refute invite.max_use
+      assert invite.invite_type == "one_time"
+    end
+
+    test "with expires_at", %{conn: conn} do
+      conn =
+        post(conn, "/api/pleroma/admin/users/invite_token", %{
+          "expires_at" => Date.to_string(Date.utc_today())
+        })
+
+      invite_json = json_response(conn, 200)
+      invite = UserInviteToken.find_by_token!(invite_json["token"])
+
+      refute invite.used
+      assert invite.expires_at == Date.utc_today()
+      refute invite.max_use
+      assert invite.invite_type == "date_limited"
+    end
+
+    test "with max_use", %{conn: conn} do
+      conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
+
+      invite_json = json_response(conn, 200)
+      invite = UserInviteToken.find_by_token!(invite_json["token"])
+      refute invite.used
+      refute invite.expires_at
+      assert invite.max_use == 150
+      assert invite.invite_type == "reusable"
+    end
+
+    test "with max use and expires_at", %{conn: conn} do
+      conn =
+        post(conn, "/api/pleroma/admin/users/invite_token", %{
+          "max_use" => 150,
+          "expires_at" => Date.to_string(Date.utc_today())
+        })
+
+      invite_json = json_response(conn, 200)
+      invite = UserInviteToken.find_by_token!(invite_json["token"])
+      refute invite.used
+      assert invite.expires_at == Date.utc_today()
+      assert invite.max_use == 150
+      assert invite.invite_type == "reusable_date_limited"
+    end
+  end
+
+  describe "GET /api/pleroma/admin/users/invites" do
+    test "no invites", %{conn: conn} do
+      conn = get(conn, "/api/pleroma/admin/users/invites")
+
+      assert json_response(conn, 200) == %{"invites" => []}
+    end
+
+    test "with invite", %{conn: conn} do
+      {:ok, invite} = UserInviteToken.create_invite()
+
+      conn = get(conn, "/api/pleroma/admin/users/invites")
+
+      assert json_response(conn, 200) == %{
+               "invites" => [
+                 %{
+                   "expires_at" => nil,
+                   "id" => invite.id,
+                   "invite_type" => "one_time",
+                   "max_use" => nil,
+                   "token" => invite.token,
+                   "used" => false,
+                   "uses" => 0
+                 }
+               ]
+             }
+    end
+  end
+
+  describe "POST /api/pleroma/admin/users/revoke_invite" do
+    test "with token", %{conn: conn} do
+      {:ok, invite} = UserInviteToken.create_invite()
+
+      conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
+
+      assert json_response(conn, 200) == %{
+               "expires_at" => nil,
+               "id" => invite.id,
+               "invite_type" => "one_time",
+               "max_use" => nil,
+               "token" => invite.token,
+               "used" => true,
+               "uses" => 0
+             }
+    end
+
+    test "with invalid token", %{conn: conn} do
+      conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
+
+      assert json_response(conn, :not_found) == %{"error" => "Not found"}
+    end
+  end
+end