Merge branch 'develop' into feature/report-notes
[akkoma] / test / web / admin_api / admin_api_controller_test.exs
index a1027e2e25933e90c4438f21a9e831b18fbbbe36..49ff005b66fe2002f012e4f955c81129a47e93bc 100644 (file)
@@ -10,15 +10,78 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
   alias Pleroma.HTML
   alias Pleroma.ModerationLog
   alias Pleroma.Repo
+  alias Pleroma.ReportNote
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
   alias Pleroma.UserInviteToken
+  alias Pleroma.Web.ActivityPub.Relay
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.MastodonAPI.StatusView
   alias Pleroma.Web.MediaProxy
   import Pleroma.Factory
 
-  describe "/api/pleroma/admin/users" do
-    test "Delete" do
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+    :ok
+  end
+
+  clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+    Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+  end
+
+  describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
+    clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+      Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
+    end
+
+    test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do
+      user = insert(:user)
+      admin = insert(:user, is_admin: true)
+      url = "/api/pleroma/admin/users/#{user.nickname}"
+
+      good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
+      good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
+      good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
+
+      bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
+      bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
+      bad_token3 = nil
+
+      for good_token <- [good_token1, good_token2, good_token3] do
+        conn =
+          build_conn()
+          |> assign(:user, admin)
+          |> assign(:token, good_token)
+          |> get(url)
+
+        assert json_response(conn, 200)
+      end
+
+      for good_token <- [good_token1, good_token2, good_token3] do
+        conn =
+          build_conn()
+          |> assign(:user, nil)
+          |> assign(:token, good_token)
+          |> get(url)
+
+        assert json_response(conn, :forbidden)
+      end
+
+      for bad_token <- [bad_token1, bad_token2, bad_token3] do
+        conn =
+          build_conn()
+          |> assign(:user, admin)
+          |> assign(:token, bad_token)
+          |> get(url)
+
+        assert json_response(conn, :forbidden)
+      end
+    end
+  end
+
+  describe "DELETE /api/pleroma/admin/users" do
+    test "single user" do
       admin = insert(:user, is_admin: true)
       user = insert(:user)
 
@@ -30,15 +93,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
 
       log_entry = Repo.one(ModerationLog)
 
-      assert log_entry.data["subject"]["nickname"] == user.nickname
-      assert log_entry.data["action"] == "delete"
-
       assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} deleted user @#{user.nickname}"
+               "@#{admin.nickname} deleted users: @#{user.nickname}"
 
       assert json_response(conn, 200) == user.nickname
     end
 
+    test "multiple users" do
+      admin = insert(:user, is_admin: true)
+      user_one = insert(:user)
+      user_two = insert(:user)
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> put_req_header("accept", "application/json")
+        |> delete("/api/pleroma/admin/users", %{
+          nicknames: [user_one.nickname, user_two.nickname]
+        })
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
+
+      response = json_response(conn, 200)
+      assert response -- [user_one.nickname, user_two.nickname] == []
+    end
+  end
+
+  describe "/api/pleroma/admin/users" do
     test "Create" do
       admin = insert(:user, is_admin: true)
 
@@ -69,7 +153,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
     end
 
-    test "Cannot create user with exisiting email" do
+    test "Cannot create user with existing email" do
       admin = insert(:user, is_admin: true)
       user = insert(:user)
 
@@ -100,7 +184,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
              ]
     end
 
-    test "Cannot create user with exisiting nickname" do
+    test "Cannot create user with existing nickname" do
       admin = insert(:user, is_admin: true)
       user = insert(:user)
 
@@ -197,7 +281,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         "roles" => %{"admin" => false, "moderator" => false},
         "tags" => [],
         "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-        "display_name" => HTML.strip_tags(user.name || user.nickname)
+        "display_name" => HTML.strip_tags(user.name || user.nickname),
+        "confirmation_pending" => false
       }
 
       assert expected == json_response(conn, 200)
@@ -404,82 +489,72 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                "@#{admin.nickname} made @#{user.nickname} admin"
     end
 
-    test "/:right DELETE, can remove from a permission group" do
+    test "/:right POST, can add to a permission group (multiple)" do
       admin = insert(:user, is_admin: true)
-      user = insert(:user, is_admin: true)
+      user_one = insert(:user)
+      user_two = insert(:user)
 
       conn =
         build_conn()
         |> assign(:user, admin)
         |> put_req_header("accept", "application/json")
-        |> delete("/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" => false
+               "is_admin" => true
              }
 
       log_entry = Repo.one(ModerationLog)
 
       assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} revoked admin role from @#{user.nickname}"
+               "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
     end
-  end
 
-  describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
-    setup %{conn: conn} do
+    test "/:right DELETE, can remove from a permission group" do
       admin = insert(:user, is_admin: true)
+      user = insert(:user, is_admin: true)
 
       conn =
-        conn
+        build_conn()
         |> assign(:user, admin)
         |> put_req_header("accept", "application/json")
+        |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
 
-      %{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.deactivated == true
-      assert json_response(conn, :no_content)
+      assert json_response(conn, 200) == %{
+               "is_admin" => false
+             }
 
       log_entry = Repo.one(ModerationLog)
 
       assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} deactivated user @#{user.nickname}"
+               "@#{admin.nickname} revoked admin role from @#{user.nickname}"
     end
 
-    test "activates the user", %{conn: conn, admin: admin} do
-      user = insert(:user, deactivated: true)
+    test "/:right DELETE, can remove from a permission group (multiple)" do
+      admin = insert(:user, is_admin: true)
+      user_one = insert(:user, is_admin: true)
+      user_two = insert(:user, is_admin: true)
 
       conn =
-        conn
-        |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
+        build_conn()
+        |> assign(:user, admin)
+        |> put_req_header("accept", "application/json")
+        |> delete("/api/pleroma/admin/users/permission_group/admin", %{
+          nicknames: [user_one.nickname, user_two.nickname]
+        })
 
-      user = User.get_cached_by_id(user.id)
-      assert user.deactivated == false
-      assert json_response(conn, :no_content)
+      assert json_response(conn, 200) == %{
+               "is_admin" => false
+             }
 
       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
 
@@ -616,7 +691,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => true,
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+            "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => user.deactivated,
@@ -626,7 +702,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => false,
             "tags" => ["foo", "bar"],
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user.name || user.nickname)
+            "display_name" => HTML.strip_tags(user.name || user.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -667,7 +744,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -691,7 +769,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -715,7 +794,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -739,7 +819,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -763,7 +844,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -787,7 +869,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -806,7 +889,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user2.name || user2.nickname)
+                   "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -835,7 +919,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -862,7 +947,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => true,
             "tags" => [],
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user.name || user.nickname)
+            "display_name" => HTML.strip_tags(user.name || user.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => admin.deactivated,
@@ -872,7 +958,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => true,
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+            "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => false,
@@ -882,7 +969,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "roles" => %{"admin" => true, "moderator" => false},
             "tags" => [],
             "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname)
+            "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -911,7 +999,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => admin.local,
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+            "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => false,
@@ -921,7 +1010,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => second_admin.local,
             "tags" => [],
             "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname)
+            "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -952,7 +1042,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => moderator.local,
                    "tags" => [],
                    "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(moderator.name || moderator.nickname)
+                   "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -976,7 +1067,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => user1.local,
             "tags" => ["first"],
             "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user1.name || user1.nickname)
+            "display_name" => HTML.strip_tags(user1.name || user1.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => false,
@@ -986,7 +1078,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => user2.local,
             "tags" => ["second"],
             "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user2.name || user2.nickname)
+            "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -1022,13 +1115,85 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => user.local,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
+                 }
+               ]
+             }
+    end
+
+    test "it omits relay user", %{admin: admin} do
+      assert %User{} = Relay.get_actor()
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/users")
+
+      assert json_response(conn, 200) == %{
+               "count" => 1,
+               "page_size" => 50,
+               "users" => [
+                 %{
+                   "deactivated" => admin.deactivated,
+                   "id" => admin.id,
+                   "nickname" => admin.nickname,
+                   "roles" => %{"admin" => true, "moderator" => false},
+                   "local" => true,
+                   "tags" => [],
+                   "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
+                   "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
     end
   end
 
+  test "PATCH /api/pleroma/admin/users/activate" do
+    admin = insert(:user, is_admin: true)
+    user_one = insert(:user, deactivated: true)
+    user_two = insert(:user, deactivated: true)
+
+    conn =
+      build_conn()
+      |> assign(:user, admin)
+      |> 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, is_admin: true)
+    user_one = insert(:user, deactivated: false)
+    user_two = insert(:user, 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 users: @#{user_one.nickname}, @#{user_two.nickname}"
+  end
+
   test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
     admin = insert(:user, is_admin: true)
     user = insert(:user)
@@ -1047,13 +1212,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                "local" => true,
                "tags" => [],
                "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-               "display_name" => HTML.strip_tags(user.name || user.nickname)
+               "display_name" => HTML.strip_tags(user.name || user.nickname),
+               "confirmation_pending" => false
              }
 
     log_entry = Repo.one(ModerationLog)
 
     assert ModerationLog.get_log_entry_message(log_entry) ==
-             "@#{admin.nickname} deactivated user @#{user.nickname}"
+             "@#{admin.nickname} deactivated users: @#{user.nickname}"
   end
 
   describe "POST /api/pleroma/admin/users/invite_token" do
@@ -1224,7 +1390,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
   end
 
-  describe "PUT /api/pleroma/admin/reports/:id" do
+  describe "PATCH /api/pleroma/admin/reports" do
     setup %{conn: conn} do
       admin = insert(:user, is_admin: true)
       [reporter, target_user] = insert_pair(:user)
@@ -1237,16 +1403,32 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
           "status_ids" => [activity.id]
         })
 
-      %{conn: assign(conn, :user, admin), id: report_id, admin: admin}
+      {:ok, %{id: second_report_id}} =
+        CommonAPI.report(reporter, %{
+          "account_id" => target_user.id,
+          "comment" => "I feel very offended",
+          "status_ids" => [activity.id]
+        })
+
+      %{
+        conn: assign(conn, :user, admin),
+        id: report_id,
+        admin: admin,
+        second_report_id: second_report_id
+      }
     end
 
     test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
-      response =
-        conn
-        |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
-        |> json_response(:ok)
+      conn
+      |> patch("/api/pleroma/admin/reports", %{
+        "reports" => [
+          %{"state" => "resolved", "id" => id}
+        ]
+      })
+      |> json_response(:no_content)
 
-      assert response["state"] == "resolved"
+      activity = Activity.get_by_id(id)
+      assert activity.data["state"] == "resolved"
 
       log_entry = Repo.one(ModerationLog)
 
@@ -1255,12 +1437,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
 
     test "closes report", %{conn: conn, id: id, admin: admin} do
-      response =
-        conn
-        |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
-        |> json_response(:ok)
+      conn
+      |> patch("/api/pleroma/admin/reports", %{
+        "reports" => [
+          %{"state" => "closed", "id" => id}
+        ]
+      })
+      |> json_response(:no_content)
 
-      assert response["state"] == "closed"
+      activity = Activity.get_by_id(id)
+      assert activity.data["state"] == "closed"
 
       log_entry = Repo.one(ModerationLog)
 
@@ -1271,17 +1457,54 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     test "returns 400 when state is unknown", %{conn: conn, id: id} do
       conn =
         conn
-        |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"})
+        |> patch("/api/pleroma/admin/reports", %{
+          "reports" => [
+            %{"state" => "test", "id" => id}
+          ]
+        })
 
-      assert json_response(conn, :bad_request) == "Unsupported state"
+      assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state"
     end
 
     test "returns 404 when report is not exist", %{conn: conn} do
       conn =
         conn
-        |> put("/api/pleroma/admin/reports/test", %{"state" => "closed"})
+        |> patch("/api/pleroma/admin/reports", %{
+          "reports" => [
+            %{"state" => "closed", "id" => "test"}
+          ]
+        })
 
-      assert json_response(conn, :not_found) == "Not found"
+      assert hd(json_response(conn, :bad_request))["error"] == "not_found"
+    end
+
+    test "updates state of multiple reports", %{
+      conn: conn,
+      id: id,
+      admin: admin,
+      second_report_id: second_report_id
+    } do
+      conn
+      |> patch("/api/pleroma/admin/reports", %{
+        "reports" => [
+          %{"state" => "resolved", "id" => id},
+          %{"state" => "closed", "id" => second_report_id}
+        ]
+      })
+      |> json_response(:no_content)
+
+      activity = Activity.get_by_id(id)
+      second_activity = Activity.get_by_id(second_report_id)
+      assert activity.data["state"] == "resolved"
+      assert second_activity.data["state"] == "closed"
+
+      [first_log_entry, second_log_entry] = Repo.all(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(first_log_entry) ==
+               "@#{admin.nickname} updated report ##{id} with 'resolved' state"
+
+      assert ModerationLog.get_log_entry_message(second_log_entry) ==
+               "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state"
     end
   end
 
@@ -1392,7 +1615,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         |> assign(:user, user)
         |> get("/api/pleroma/admin/reports")
 
-      assert json_response(conn, :forbidden) == %{"error" => "User is not admin."}
+      assert json_response(conn, :forbidden) ==
+               %{"error" => "User is not an admin or OAuth admin scope is not granted."}
     end
 
     test "returns 403 when requested by anonymous" do
@@ -1404,59 +1628,207 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
   end
 
-  #
-  describe "POST /api/pleroma/admin/reports/:id/respond" do
+  describe "GET /api/pleroma/admin/grouped_reports" do
     setup %{conn: conn} do
       admin = insert(:user, is_admin: true)
+      [reporter, target_user] = insert_pair(:user)
 
-      %{conn: assign(conn, :user, admin), admin: admin}
-    end
+      date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
+      date2 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
+      date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
 
-    test "returns created dm", %{conn: conn, admin: admin} do
-      [reporter, target_user] = insert_pair(:user)
-      activity = insert(:note_activity, user: target_user)
+      first_status =
+        insert(:note_activity, user: target_user, data_attrs: %{"published" => date1})
 
-      {:ok, %{id: report_id}} =
+      second_status =
+        insert(:note_activity, user: target_user, data_attrs: %{"published" => date2})
+
+      third_status =
+        insert(:note_activity, user: target_user, data_attrs: %{"published" => date3})
+
+      {:ok, first_report} =
         CommonAPI.report(reporter, %{
           "account_id" => target_user.id,
-          "comment" => "I feel offended",
-          "status_ids" => [activity.id]
+          "status_ids" => [first_status.id, second_status.id, third_status.id]
+        })
+
+      {:ok, second_report} =
+        CommonAPI.report(reporter, %{
+          "account_id" => target_user.id,
+          "status_ids" => [first_status.id, second_status.id]
+        })
+
+      {:ok, third_report} =
+        CommonAPI.report(reporter, %{
+          "account_id" => target_user.id,
+          "status_ids" => [first_status.id]
         })
 
+      %{
+        conn: assign(conn, :user, admin),
+        first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]),
+        second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]),
+        third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]),
+        first_report: first_report,
+        first_status_reports: [first_report, second_report, third_report],
+        second_status_reports: [first_report, second_report],
+        third_status_reports: [first_report],
+        target_user: target_user,
+        reporter: reporter
+      }
+    end
+
+    test "returns reports grouped by status", %{
+      conn: conn,
+      first_status: first_status,
+      second_status: second_status,
+      third_status: third_status,
+      first_status_reports: first_status_reports,
+      second_status_reports: second_status_reports,
+      third_status_reports: third_status_reports,
+      target_user: target_user,
+      reporter: reporter
+    } do
       response =
         conn
-        |> post("/api/pleroma/admin/reports/#{report_id}/respond", %{
-          "status" => "I will check it out"
-        })
+        |> get("/api/pleroma/admin/grouped_reports")
         |> json_response(:ok)
 
-      recipients = Enum.map(response["mentions"], & &1["username"])
+      assert length(response["reports"]) == 3
 
-      assert reporter.nickname in recipients
-      assert response["content"] == "I will check it out"
-      assert response["visibility"] == "direct"
+      first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
 
-      log_entry = Repo.one(ModerationLog)
+      second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id))
 
-      assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} responded with 'I will check it out' to report ##{
-                 response["id"]
-               }"
+      third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id))
+
+      assert length(first_group["reports"]) == 3
+      assert length(second_group["reports"]) == 2
+      assert length(third_group["reports"]) == 1
+
+      assert first_group["date"] ==
+               Enum.max_by(first_status_reports, fn act ->
+                 NaiveDateTime.from_iso8601!(act.data["published"])
+               end).data["published"]
+
+      assert first_group["status"] ==
+               Map.put(
+                 stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+                 "deleted",
+                 false
+               )
+
+      assert(first_group["account"]["id"] == target_user.id)
+
+      assert length(first_group["actors"]) == 1
+      assert hd(first_group["actors"])["id"] == reporter.id
+
+      assert Enum.map(first_group["reports"], & &1["id"]) --
+               Enum.map(first_status_reports, & &1.id) == []
+
+      assert second_group["date"] ==
+               Enum.max_by(second_status_reports, fn act ->
+                 NaiveDateTime.from_iso8601!(act.data["published"])
+               end).data["published"]
+
+      assert second_group["status"] ==
+               Map.put(
+                 stringify_keys(StatusView.render("show.json", %{activity: second_status})),
+                 "deleted",
+                 false
+               )
+
+      assert second_group["account"]["id"] == target_user.id
+
+      assert length(second_group["actors"]) == 1
+      assert hd(second_group["actors"])["id"] == reporter.id
+
+      assert Enum.map(second_group["reports"], & &1["id"]) --
+               Enum.map(second_status_reports, & &1.id) == []
+
+      assert third_group["date"] ==
+               Enum.max_by(third_status_reports, fn act ->
+                 NaiveDateTime.from_iso8601!(act.data["published"])
+               end).data["published"]
+
+      assert third_group["status"] ==
+               Map.put(
+                 stringify_keys(StatusView.render("show.json", %{activity: third_status})),
+                 "deleted",
+                 false
+               )
+
+      assert third_group["account"]["id"] == target_user.id
+
+      assert length(third_group["actors"]) == 1
+      assert hd(third_group["actors"])["id"] == reporter.id
+
+      assert Enum.map(third_group["reports"], & &1["id"]) --
+               Enum.map(third_status_reports, & &1.id) == []
     end
 
-    test "returns 400 when status is missing", %{conn: conn} do
-      conn = post(conn, "/api/pleroma/admin/reports/test/respond")
+    test "reopened report renders status data", %{
+      conn: conn,
+      first_report: first_report,
+      first_status: first_status
+    } do
+      {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
 
-      assert json_response(conn, :bad_request) == "Invalid parameters"
+      response =
+        conn
+        |> get("/api/pleroma/admin/grouped_reports")
+        |> json_response(:ok)
+
+      first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
+
+      assert first_group["status"] ==
+               Map.put(
+                 stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+                 "deleted",
+                 false
+               )
     end
 
-    test "returns 404 when report id is invalid", %{conn: conn} do
-      conn =
-        post(conn, "/api/pleroma/admin/reports/test/respond", %{
-          "status" => "foo"
-        })
+    test "reopened report does not render status data if status has been deleted", %{
+      conn: conn,
+      first_report: first_report,
+      first_status: first_status,
+      target_user: target_user
+    } do
+      {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+      {:ok, _} = CommonAPI.delete(first_status.id, target_user)
 
-      assert json_response(conn, :not_found) == "Not found"
+      refute Activity.get_by_ap_id(first_status.id)
+
+      response =
+        conn
+        |> get("/api/pleroma/admin/grouped_reports")
+        |> json_response(:ok)
+
+      assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][
+               "deleted"
+             ] == true
+
+      assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2
+    end
+
+    test "account not empty if status was deleted", %{
+      conn: conn,
+      first_report: first_report,
+      first_status: first_status,
+      target_user: target_user
+    } do
+      {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+      {:ok, _} = CommonAPI.delete(first_status.id, target_user)
+
+      refute Activity.get_by_ap_id(first_status.id)
+
+      response =
+        conn
+        |> get("/api/pleroma/admin/grouped_reports")
+        |> json_response(:ok)
+
+      assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"]
     end
   end
 
@@ -1618,6 +1990,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       Pleroma.Config.put([:instance, :dynamic_configuration], true)
     end
 
+    @tag capture_log: true
     test "create new config setting in db", %{conn: conn} do
       conn =
         post(conn, "/api/pleroma/admin/config", %{
@@ -2181,6 +2554,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       Pleroma.Config.put([:instance, :dynamic_configuration], true)
     end
 
+    clear_config([:feed, :post_title]) do
+      Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: "…"})
+    end
+
     test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
       assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
       conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
@@ -2477,7 +2854,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       conn =
         build_conn()
         |> assign(:user, admin)
-        |> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset")
+        |> patch("/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]})
 
       assert json_response(conn, 204) == ""
 
@@ -2486,6 +2863,242 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert User.get_by_id(user.id).password_reset_pending == true
     end
   end
+
+  describe "relays" do
+    setup %{conn: conn} do
+      admin = insert(:user, is_admin: true)
+
+      %{conn: assign(conn, :user, admin), admin: admin}
+    end
+
+    test "POST /relay", %{admin: admin} do
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> post("/api/pleroma/admin/relay", %{
+          relay_url: "http://mastodon.example.org/users/admin"
+        })
+
+      assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
+    end
+
+    test "GET /relay", %{admin: admin} do
+      relay_user = Pleroma.Web.ActivityPub.Relay.get_actor()
+
+      ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"]
+      |> Enum.each(fn ap_id ->
+        {:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
+        User.follow(relay_user, user)
+      end)
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/relay")
+
+      assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == []
+    end
+
+    test "DELETE /relay", %{admin: admin} do
+      build_conn()
+      |> assign(:user, admin)
+      |> post("/api/pleroma/admin/relay", %{
+        relay_url: "http://mastodon.example.org/users/admin"
+      })
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> delete("/api/pleroma/admin/relay", %{
+          relay_url: "http://mastodon.example.org/users/admin"
+        })
+
+      assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
+
+      [log_entry_one, log_entry_two] = Repo.all(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry_one) ==
+               "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
+
+      assert ModerationLog.get_log_entry_message(log_entry_two) ==
+               "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
+    end
+  end
+
+  describe "instances" do
+    test "GET /instances/:instance/statuses" do
+      admin = insert(:user, is_admin: true)
+      user = insert(:user, local: false, nickname: "archaeme@archae.me")
+      user2 = insert(:user, local: false, nickname: "test@test.com")
+      insert_pair(:note_activity, user: user)
+      insert(:note_activity, user: user2)
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/archae.me/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 2
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/test.com/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 1
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/nonexistent.com/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 0
+    end
+  end
+
+  describe "PATCH /confirm_email" do
+    setup %{conn: conn} do
+      admin = insert(:user, is_admin: true)
+
+      %{conn: assign(conn, :user, admin), admin: admin}
+    end
+
+    test "it confirms emails of two users", %{admin: admin} do
+      [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+      assert first_user.confirmation_pending == true
+      assert second_user.confirmation_pending == true
+
+      build_conn()
+      |> assign(:user, admin)
+      |> patch("/api/pleroma/admin/users/confirm_email", %{
+        nicknames: [
+          first_user.nickname,
+          second_user.nickname
+        ]
+      })
+
+      assert first_user.confirmation_pending == true
+      assert second_user.confirmation_pending == true
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{
+                 second_user.nickname
+               }"
+    end
+  end
+
+  describe "PATCH /resend_confirmation_email" do
+    setup %{conn: conn} do
+      admin = insert(:user, is_admin: true)
+
+      %{conn: assign(conn, :user, admin), admin: admin}
+    end
+
+    test "it resend emails for two users", %{admin: admin} do
+      [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+      build_conn()
+      |> assign(:user, admin)
+      |> patch("/api/pleroma/admin/users/resend_confirmation_email", %{
+        nicknames: [
+          first_user.nickname,
+          second_user.nickname
+        ]
+      })
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
+                 second_user.nickname
+               }"
+    end
+  end
+
+  describe "POST /reports/:id/notes" do
+    setup do
+      admin = insert(:user, is_admin: true)
+      [reporter, target_user] = insert_pair(:user)
+      activity = insert(:note_activity, user: target_user)
+
+      {:ok, %{id: report_id}} =
+        CommonAPI.report(reporter, %{
+          "account_id" => target_user.id,
+          "comment" => "I feel offended",
+          "status_ids" => [activity.id]
+        })
+
+      build_conn()
+      |> assign(:user, admin)
+      |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
+        content: "this is disgusting!"
+      })
+
+      build_conn()
+      |> assign(:user, admin)
+      |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
+        content: "this is disgusting2!"
+      })
+
+      %{
+        admin_id: admin.id,
+        report_id: report_id,
+        admin: admin
+      }
+    end
+
+    test "it creates report note", %{admin_id: admin_id, report_id: report_id} do
+      [note, _] = Repo.all(ReportNote)
+
+      assert %{
+               activity_id: ^report_id,
+               content: "this is disgusting!",
+               user_id: ^admin_id
+             } = note
+    end
+
+    test "it returns reports with notes", %{admin: admin} do
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/reports")
+
+      response = json_response(conn, 200)
+      notes = hd(response["reports"])["notes"]
+      [note, _] = notes
+
+      assert note["user"]["nickname"] == admin.nickname
+      assert note["content"] == "this is disgusting!"
+      assert note["created_at"]
+      assert response["total"] == 1
+    end
+
+    test "it deletes the note", %{admin: admin, report_id: report_id} do
+      assert ReportNote |> Repo.all() |> length() == 2
+
+      [note, _] = Repo.all(ReportNote)
+
+      build_conn()
+      |> assign(:user, admin)
+      |> delete("/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
+
+      assert ReportNote |> Repo.all() |> length() == 1
+    end
+  end
 end
 
 # Needed for testing