Ability to toggle activation status and permission group for a group of users
authorMaxim Filippov <colixer@gmail.com>
Wed, 9 Oct 2019 14:03:54 +0000 (17:03 +0300)
committerMaxim Filippov <colixer@gmail.com>
Wed, 9 Oct 2019 14:03:54 +0000 (17:03 +0300)
CHANGELOG.md
docs/API/admin_api.md
lib/pleroma/moderation_log.ex
lib/pleroma/user.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/admin_api/views/account_view.ex
lib/pleroma/web/router.ex
test/moderation_log_test.exs
test/web/admin_api/admin_api_controller_test.exs

index 8b24db7f4b37002a15c4729e6dfaf335ebcd7912..584c917f4d060a5a1c895bcb091a90165949d18c 100644 (file)
@@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Changed
 - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
 - **Breaking:** Admin API: Return link alongside with token on password reset
+- **Breaking:** Admin API: `/users/:nickname/toggle_activation` endpoint was split into two: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
+- **Breaking:** Admin API: `POST /users/permission_group/:permission_group` / `DELETE /users/permission_group/:permission_group` now accept `nicknames` array
 - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
 - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
 - Admin API: Return `total` when querying for reports
@@ -40,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Removed
 - **Breaking:** GNU Social API with Qvitter extensions support
+- **Breaking:** Admin API: `/users/:nickname/activation_status` was removed in favor of `/users/activate`, `/users/deactivate`
 - Emoji: Remove longfox emojis.
 - Remove `Reply-To` header from report emails for admins.
 
index ee9e68cb1456c79c3231fa9a259e9dddbdfbbacb..55f8749e18cfeabe00b5df00fc74a3d9defe70e0 100644 (file)
@@ -154,31 +154,62 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 }
 ```
 
+## `POST /api/pleroma/admin/users/permission_group/:permission_group`
+
 ### Add user in permission group
 
-- Method: `POST`
-- Params: none
+- Params:
+  - `nicknames`: nicknames array
 - Response:
   - On failure: `{"error": "…"}`
   - On success: JSON of the `user.info`
 
+## `DELETE /api/pleroma/admin/users/permission_group/:permission_group`
+
 ### Remove user from permission group
 
-- Method: `DELETE`
-- Params: none
+- Params:
+  - `nicknames`: nicknames array
 - Response:
   - On failure: `{"error": "…"}`
   - On success: JSON of the `user.info`
 - Note: An admin cannot revoke their own admin status.
 
-## `/api/pleroma/admin/users/:nickname/activation_status`
+## `PATCH /api/pleroma/admin/users/activate`
 
-### Active or deactivate a user
+### Activate user
 
-- Method: `PUT`
 - Params:
-  - `nickname`
-  - `status` BOOLEAN field, false value means deactivation.
+  - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+  users: [
+    {
+      // user object
+    }
+  ]
+}
+```
+
+## `PATCH /api/pleroma/admin/users/deactivate`
+
+### Deactivate user
+
+- Params:
+  - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+  users: [
+    {
+      // user object
+    }
+  ]
+}
+```
 
 ## `/api/pleroma/admin/users/:nickname_or_id`
 
index 352cad4335a40f9c1beea6ac5c2b258191527b72..42649ff02b6a421646dbaf25a8deee15f309f211 100644 (file)
@@ -86,18 +86,18 @@ defmodule Pleroma.ModerationLog do
     parsed_datetime
   end
 
-  @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
+  @spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
           {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
-        subject: %User{} = subject,
+        subject: subjects,
         action: action,
         permission: permission
       }) do
     %ModerationLog{
       data: %{
         "actor" => user_to_map(actor),
-        "subject" => user_to_map(subject),
+        "subject" => user_to_map(subjects),
         "action" => action,
         "permission" => permission,
         "message" => ""
@@ -303,13 +303,16 @@ defmodule Pleroma.ModerationLog do
   end
 
   @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
-
   defp insert_log_entry_with_message(entry) do
     entry.data["message"]
     |> put_in(get_log_entry_message(entry))
     |> Repo.insert()
   end
 
+  defp user_to_map(users) when is_list(users) do
+    users |> Enum.map(&user_to_map/1)
+  end
+
   defp user_to_map(%User{} = user) do
     user
     |> Map.from_struct()
@@ -363,12 +366,7 @@ defmodule Pleroma.ModerationLog do
           "subjects" => subjects
         }
       }) do
-    nicknames =
-      subjects
-      |> Enum.map(&"@#{&1["nickname"]}")
-      |> Enum.join(", ")
-
-    "@#{actor_nickname} created users: #{nicknames}"
+    "@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
   end
 
   @spec get_log_entry_message(ModerationLog) :: String.t()
@@ -376,10 +374,10 @@ defmodule Pleroma.ModerationLog do
         data: %{
           "actor" => %{"nickname" => actor_nickname},
           "action" => "activate",
-          "subject" => %{"nickname" => subject_nickname, "type" => "user"}
+          "subject" => users
         }
       }) do
-    "@#{actor_nickname} activated user @#{subject_nickname}"
+    "@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
   end
 
   @spec get_log_entry_message(ModerationLog) :: String.t()
@@ -387,10 +385,10 @@ defmodule Pleroma.ModerationLog do
         data: %{
           "actor" => %{"nickname" => actor_nickname},
           "action" => "deactivate",
-          "subject" => %{"nickname" => subject_nickname, "type" => "user"}
+          "subject" => users
         }
       }) do
-    "@#{actor_nickname} deactivated user @#{subject_nickname}"
+    "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
   end
 
   @spec get_log_entry_message(ModerationLog) :: String.t()
@@ -402,14 +400,9 @@ defmodule Pleroma.ModerationLog do
           "action" => "tag"
         }
       }) do
-    nicknames_string =
-      nicknames
-      |> Enum.map(&"@#{&1}")
-      |> Enum.join(", ")
-
     tags_string = tags |> Enum.join(", ")
 
-    "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}"
+    "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
   end
 
   @spec get_log_entry_message(ModerationLog) :: String.t()
@@ -421,14 +414,9 @@ defmodule Pleroma.ModerationLog do
           "action" => "untag"
         }
       }) do
-    nicknames_string =
-      nicknames
-      |> Enum.map(&"@#{&1}")
-      |> Enum.join(", ")
-
     tags_string = tags |> Enum.join(", ")
 
-    "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}"
+    "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
   end
 
   @spec get_log_entry_message(ModerationLog) :: String.t()
@@ -436,11 +424,11 @@ defmodule Pleroma.ModerationLog do
         data: %{
           "actor" => %{"nickname" => actor_nickname},
           "action" => "grant",
-          "subject" => %{"nickname" => subject_nickname},
+          "subject" => users,
           "permission" => permission
         }
       }) do
-    "@#{actor_nickname} made @#{subject_nickname} #{permission}"
+    "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
   end
 
   @spec get_log_entry_message(ModerationLog) :: String.t()
@@ -448,11 +436,11 @@ defmodule Pleroma.ModerationLog do
         data: %{
           "actor" => %{"nickname" => actor_nickname},
           "action" => "revoke",
-          "subject" => %{"nickname" => subject_nickname},
+          "subject" => users,
           "permission" => permission
         }
       }) do
-    "@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}"
+    "@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
   end
 
   @spec get_log_entry_message(ModerationLog) :: String.t()
@@ -551,4 +539,16 @@ defmodule Pleroma.ModerationLog do
       }) do
     "@#{actor_nickname} deleted status ##{subject_id}"
   end
+
+  defp nicknames_to_string(nicknames) do
+    nicknames
+    |> Enum.map(&"@#{&1}")
+    |> Enum.join(", ")
+  end
+
+  defp users_to_nicknames_string(users) do
+    users
+    |> Enum.map(&"@#{&1["nickname"]}")
+    |> Enum.join(", ")
+  end
 end
index 2cfb13a8c0164b2b4f1989fae7bdc6a666202c0e..a76a5ad702415717d88855deefcb2f1ed9a4aca4 100644 (file)
@@ -1059,7 +1059,15 @@ defmodule Pleroma.User do
     BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
   end
 
-  def deactivate(%User{} = user, status \\ true) do
+  def deactivate(user, status \\ true)
+
+  def deactivate(users, status) when is_list(users) do
+    Repo.transaction(fn ->
+      for user <- users, do: deactivate(user, status)
+    end)
+  end
+
+  def deactivate(%User{} = user, status) do
     with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
       Enum.each(get_followers(user), &invalidate_cache/1)
       Enum.each(get_friends(user), &update_follower_count/1)
@@ -1625,6 +1633,12 @@ defmodule Pleroma.User do
 
   `fun` is called with the `user.info`.
   """
+  def update_info(users, fun) when is_list(users) do
+    Repo.transaction(fn ->
+      for user <- users, do: update_info(user, fun)
+    end)
+  end
+
   def update_info(user, fun) do
     user
     |> change_info(fun)
index 513bae80060bc5506a1708bee93b99f357bb6ce6..d825a5d281dd45d559cd4d16a3357d026b3216ab 100644 (file)
@@ -231,22 +231,34 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     end
   end
 
-  def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
-    user = User.get_cached_by_nickname(nickname)
+  def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+    {:ok, updated_users} = User.deactivate(users, false)
 
-    {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
+    ModerationLog.insert_log(%{
+      actor: admin,
+      subject: users,
+      action: "activate"
+    })
 
-    action = if user.info.deactivated, do: "activate", else: "deactivate"
+    conn
+    |> put_view(AccountView)
+    |> render("index.json", %{users: Keyword.values(updated_users)})
+  end
+
+  def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+    {:ok, updated_users} = User.deactivate(users, true)
 
     ModerationLog.insert_log(%{
       actor: admin,
-      subject: user,
-      action: action
+      subject: users,
+      action: "deactivate"
     })
 
     conn
     |> put_view(AccountView)
-    |> render("show.json", %{user: updated_user})
+    |> render("index.json", %{users: Keyword.values(updated_users)})
   end
 
   def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
@@ -315,20 +327,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
 
   def right_add(%{assigns: %{user: admin}} = conn, %{
         "permission_group" => permission_group,
-        "nickname" => nickname
+        "nicknames" => nicknames
       })
       when permission_group in ["moderator", "admin"] do
     info = Map.put(%{}, "is_" <> permission_group, true)
 
-    {:ok, user} =
-      nickname
-      |> User.get_cached_by_nickname()
-      |> User.update_info(&User.Info.admin_api_update(&1, info))
+    users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+
+    User.update_info(users, &User.Info.admin_api_update(&1, info))
 
     ModerationLog.insert_log(%{
       action: "grant",
       actor: admin,
-      subject: user,
+      subject: users,
       permission: permission_group
     })
 
@@ -349,58 +360,38 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     })
   end
 
-  def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
-    render_error(conn, :forbidden, "You can't revoke your own admin status.")
-  end
-
   def right_delete(
-        %{assigns: %{user: admin}} = conn,
+        %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
         %{
           "permission_group" => permission_group,
-          "nickname" => nickname
+          "nicknames" => nicknames
         }
       )
       when permission_group in ["moderator", "admin"] do
-    info = Map.put(%{}, "is_" <> permission_group, false)
+    with false <- Enum.member?(nicknames, admin_nickname) do
+      info = Map.put(%{}, "is_" <> permission_group, false)
 
-    {:ok, user} =
-      nickname
-      |> User.get_cached_by_nickname()
-      |> User.update_info(&User.Info.admin_api_update(&1, info))
+      users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
 
-    ModerationLog.insert_log(%{
-      action: "revoke",
-      actor: admin,
-      subject: user,
-      permission: permission_group
-    })
-
-    json(conn, info)
-  end
-
-  def right_delete(conn, _) do
-    render_error(conn, :not_found, "No such permission_group")
-  end
-
-  def set_activation_status(%{assigns: %{user: admin}} = conn, %{
-        "nickname" => nickname,
-        "status" => status
-      }) do
-    with {:ok, status} <- Ecto.Type.cast(:boolean, status),
-         %User{} = user <- User.get_cached_by_nickname(nickname),
-         {:ok, _} <- User.deactivate(user, !status) do
-      action = if(user.info.deactivated, do: "activate", else: "deactivate")
+      User.update_info(users, &User.Info.admin_api_update(&1, info))
 
       ModerationLog.insert_log(%{
+        action: "revoke",
         actor: admin,
-        subject: user,
-        action: action
+        subject: users,
+        permission: permission_group
       })
 
-      json_response(conn, :no_content, "")
+      json(conn, info)
+    else
+      _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
     end
   end
 
+  def right_delete(conn, _) do
+    render_error(conn, :not_found, "No such permission_group")
+  end
+
   def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
     with {:ok, _message} <- Relay.follow(target) do
       ModerationLog.insert_log(%{
index a96affd40cbe4800b0b213c287144688f505ac62..441269162dfc700841ccf4dcf3014a5d96156172 100644 (file)
@@ -19,6 +19,12 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
     }
   end
 
+  def render("index.json", %{users: users}) do
+    %{
+      users: render_many(users, AccountView, "show.json", as: :user)
+    }
+  end
+
   def render("show.json", %{user: user}) do
     avatar = User.avatar_url(user) |> MediaProxy.url()
     display_name = HTML.strip_tags(user.name || user.nickname)
index ae799b8ac36650112f23e7756d0cf475754557a0..89437535782b3ab80fb0490e7aefa2b83c6ae28c 100644 (file)
@@ -136,21 +136,15 @@ defmodule Pleroma.Web.Router do
 
     delete("/users", AdminAPIController, :user_delete)
     post("/users", AdminAPIController, :users_create)
-    patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
+    patch("/users/activate", AdminAPIController, :user_activate)
+    patch("/users/deactivate", AdminAPIController, :user_deactivate)
     put("/users/tag", AdminAPIController, :tag_users)
     delete("/users/tag", AdminAPIController, :untag_users)
 
     get("/users/:nickname/permission_group", AdminAPIController, :right_get)
     get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
-    post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
-
-    delete(
-      "/users/:nickname/permission_group/:permission_group",
-      AdminAPIController,
-      :right_delete
-    )
-
-    put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status)
+    post("/users/permission_group/:permission_group", AdminAPIController, :right_add)
+    delete("/users/permission_group/:permission_group", AdminAPIController, :right_delete)
 
     post("/relay", AdminAPIController, :relay_follow)
     delete("/relay", AdminAPIController, :relay_unfollow)
index a39a00e0221ff2b3e8265540da28ab53980ec4a1..ead97e94886d4c8dd0fe9feaae45fdf696526fa3 100644 (file)
@@ -128,7 +128,7 @@ defmodule Pleroma.ModerationLogTest do
       {:ok, _} =
         ModerationLog.insert_log(%{
           actor: moderator,
-          subject: subject1,
+          subject: [subject1],
           action: "grant",
           permission: "moderator"
         })
@@ -142,7 +142,7 @@ defmodule Pleroma.ModerationLogTest do
       {:ok, _} =
         ModerationLog.insert_log(%{
           actor: moderator,
-          subject: subject1,
+          subject: [subject1],
           action: "revoke",
           permission: "moderator"
         })
index b5c355e66f3eb9c36f6ad41bf676306f8f518c26..c57c7120392d476cbec336814cd978b7ea38c08a 100644 (file)
@@ -386,13 +386,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
 
     test "/:right POST, can add to a permission group" do
       admin = insert(:user, info: %{is_admin: true})
-      user = insert(:user)
+      user_one = insert(:user)
+      user_two = insert(:user)
 
       conn =
         build_conn()
         |> assign(:user, admin)
         |> put_req_header("accept", "application/json")
-        |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
+        |> post("/api/pleroma/admin/users/permission_group/admin", %{
+          nicknames: [user_one.nickname, user_two.nickname]
+        })
 
       assert json_response(conn, 200) == %{
                "is_admin" => true
@@ -401,18 +404,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       log_entry = Repo.one(ModerationLog)
 
       assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} made @#{user.nickname} admin"
+               "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
     end
 
     test "/:right DELETE, can remove from a permission group" do
       admin = insert(:user, info: %{is_admin: true})
-      user = insert(:user, info: %{is_admin: true})
+      user_one = insert(:user, info: %{is_admin: true})
+      user_two = insert(:user, info: %{is_admin: true})
 
       conn =
         build_conn()
         |> assign(:user, admin)
         |> put_req_header("accept", "application/json")
-        |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
+        |> delete("/api/pleroma/admin/users/permission_group/admin", %{
+          nicknames: [user_one.nickname, user_two.nickname]
+        })
 
       assert json_response(conn, 200) == %{
                "is_admin" => false
@@ -421,65 +427,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       log_entry = Repo.one(ModerationLog)
 
       assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} revoked admin role from @#{user.nickname}"
-    end
-  end
-
-  describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
-    setup %{conn: conn} do
-      admin = insert(:user, info: %{is_admin: true})
-
-      conn =
-        conn
-        |> assign(:user, admin)
-        |> put_req_header("accept", "application/json")
-
-      %{conn: conn, admin: admin}
-    end
-
-    test "deactivates the user", %{conn: conn, admin: admin} do
-      user = insert(:user)
-
-      conn =
-        conn
-        |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
-
-      user = User.get_cached_by_id(user.id)
-      assert user.info.deactivated == true
-      assert json_response(conn, :no_content)
-
-      log_entry = Repo.one(ModerationLog)
-
-      assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} deactivated user @#{user.nickname}"
-    end
-
-    test "activates the user", %{conn: conn, admin: admin} do
-      user = insert(:user, info: %{deactivated: true})
-
-      conn =
-        conn
-        |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
-
-      user = User.get_cached_by_id(user.id)
-      assert user.info.deactivated == false
-      assert json_response(conn, :no_content)
-
-      log_entry = Repo.one(ModerationLog)
-
-      assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} activated user @#{user.nickname}"
-    end
-
-    test "returns 403 when requested by a non-admin", %{conn: conn} do
-      user = insert(:user)
-
-      conn =
-        conn
-        |> assign(:user, user)
-        |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
-
-      assert json_response(conn, :forbidden)
+               "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
+                 user_two.nickname
+               }"
     end
   end
 
@@ -1029,31 +979,48 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
   end
 
-  test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
+  test "PATCH /api/pleroma/admin/users/activate" do
     admin = insert(:user, info: %{is_admin: true})
-    user = insert(:user)
+    user_one = insert(:user, info: %{deactivated: true})
+    user_two = insert(:user, info: %{deactivated: true})
 
     conn =
       build_conn()
       |> assign(:user, admin)
-      |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
-
-    assert json_response(conn, 200) ==
-             %{
-               "deactivated" => !user.info.deactivated,
-               "id" => user.id,
-               "nickname" => user.nickname,
-               "roles" => %{"admin" => false, "moderator" => false},
-               "local" => true,
-               "tags" => [],
-               "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-               "display_name" => HTML.strip_tags(user.name || user.nickname)
-             }
+      |> patch(
+        "/api/pleroma/admin/users/activate",
+        %{nicknames: [user_one.nickname, user_two.nickname]}
+      )
+
+    response = json_response(conn, 200)
+    assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
+
+    log_entry = Repo.one(ModerationLog)
+
+    assert ModerationLog.get_log_entry_message(log_entry) ==
+             "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
+  end
+
+  test "PATCH /api/pleroma/admin/users/deactivate" do
+    admin = insert(:user, info: %{is_admin: true})
+    user_one = insert(:user, info: %{deactivated: false})
+    user_two = insert(:user, info: %{deactivated: false})
+
+    conn =
+      build_conn()
+      |> assign(:user, admin)
+      |> patch(
+        "/api/pleroma/admin/users/deactivate",
+        %{nicknames: [user_one.nickname, user_two.nickname]}
+      )
+
+    response = json_response(conn, 200)
+    assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
 
     log_entry = Repo.one(ModerationLog)
 
     assert ModerationLog.get_log_entry_message(log_entry) ==
-             "@#{admin.nickname} deactivated user @#{user.nickname}"
+             "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
   end
 
   describe "POST /api/pleroma/admin/users/invite_token" do