Merge pull request 'MastoAPI: /api/{v1,v2}/admin/accounts, /api/v1/admin/reports' (#7842) from mkljczk/pleroma:mastodon-admin-api into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7842
This commit is contained in:
lain 2026-07-26 10:06:01 +00:00
commit e210e16a37
17 changed files with 1830 additions and 10 deletions

View file

@ -0,0 +1,327 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.Admin.AccountControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
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 "GET /api/v1/admin/accounts" do
test "search by display name", %{conn: conn} do
%{id: id} = insert(:user, name: "Display name")
insert(:user, name: "Other name")
assert [%{"id" => ^id}] =
conn
|> get("/api/v1/admin/accounts?display_name=Display")
|> json_response_and_validate_schema(200)
end
end
# Adapted from
# https://github.com/mastodon/mastodon/blob/main/spec/requests/api/v2/admin/accounts_spec.rb
describe "GET /api/v2/admin/accounts" do
setup do
remote_account = insert(:user, nickname: "remote@example.org", local: false)
other_remote_account = insert(:user, nickname: "other@foo.bar", local: false)
# suspended_account = insert(:user)
# suspended_remote = insert(:user)
disabled_account = insert(:user, is_active: false)
pending_account = insert(:user, is_approved: false)
admin_account = insert(:user, is_admin: true)
{:ok,
%{
remote_account: remote_account,
other_remote_account: other_remote_account,
disabled_account: disabled_account,
pending_account: pending_account,
admin_account: admin_account
}}
end
# test "returns the correct accounts when called with status active
# and origin local and permissions staff", %{
# conn: conn,
# admin_account: %{id: admin_account_id}
# } do
# assert [%{"id" => ^admin_account_id}] =
# conn
# |> get("/api/v2/admin/accounts?status=active&origin=local&permissions=staff")
# |> json_response_and_validate_schema(200)
# end
test "returns the correct accounts when called with by_domain value and origin remote", %{
conn: conn,
remote_account: %{id: remote_account_id}
} do
assert [%{"id" => ^remote_account_id}] =
conn
|> get("/api/v2/admin/accounts?by_domain=example.org&origin=remote")
|> json_response_and_validate_schema(200)
end
# test "returns the correct accounts when called with status suspended", %{
# conn: conn,
# suspended_account: %{id: suspended_account_id}
# } do
# assert [%{"id" => ^suspended_account_id}] =
# conn
# |> get("/api/v2/admin/accounts?status=suspended")
# |> json_response_and_validate_schema(200)
# end
test "returns the correct accounts when called with status disabled", %{
conn: conn,
disabled_account: %{id: disabled_account_id}
} do
assert [%{"id" => ^disabled_account_id}] =
conn
|> get("/api/v2/admin/accounts?status=disabled")
|> json_response_and_validate_schema(200)
end
test "returns the correct accounts when called with status pending", %{
conn: conn,
pending_account: %{id: pending_account_id}
} do
assert [%{"id" => ^pending_account_id}] =
conn
|> get("/api/v2/admin/accounts?status=pending")
|> json_response_and_validate_schema(200)
end
test "sets the correct pagination headers with limit param", %{
conn: conn,
admin_account: %{id: admin_account_id}
} do
response =
conn
|> get("/api/v2/admin/accounts?limit=1")
next_url =
~r{<.+?(?<link>/api[^>]+)>; rel=\"next\"}
|> Regex.named_captures(get_resp_header(response, "link") |> Enum.at(0))
|> Map.get("link")
next_url =~ "&limit=1&max_id=#{admin_account_id}"
end
end
describe "GET /api/v1/admin/accounts/:id" do
test "show admin-level information", %{conn: conn} do
%{id: id} =
insert(:user,
email: "email@example.com",
is_confirmed: false,
is_moderator: true
)
assert %{
"id" => ^id,
"email" => "email@example.com",
"confirmed" => false,
"role" => "moderator"
} =
conn
|> get("/api/v1/admin/accounts/#{id}")
|> json_response_and_validate_schema(200)
end
end
describe "DELETE /api/v1/admin/accounts/:id" do
test "delete account", %{conn: conn} do
%{id: id} = user = insert(:user)
conn
|> delete("/api/v1/admin/accounts/#{id}")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_active: false} = user
end
end
describe "POST /api/v1/admin/accounts/:id/action" do
test "disable account", %{conn: conn} do
%{id: id} = user = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{
"type" => "disable"
})
|> json_response_and_validate_schema(204)
user = Repo.reload!(user)
assert %{is_active: false} = user
end
test "does not disable a remote account", %{conn: conn} do
%{id: id} = user = insert(:user, local: false)
assert %{"error" => "Only local accounts can be disabled"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{
"type" => "disable"
})
|> json_response_and_validate_schema(400)
assert %{is_active: true} = Repo.reload!(user)
end
test "perform action with assigned report", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
{:ok, %{id: report_id} = report} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{target_user.id}/action", %{
"type" => "none",
"report_id" => report_id
})
|> json_response_and_validate_schema(204)
report = Repo.reload!(report)
assert %{data: %{"state" => "resolved"}} = report
end
test "requires the report management privilege", %{conn: conn} do
clear_config([:instance, :admin_privileges], [:users_manage_activation_state])
%{id: id} = insert(:user)
assert %{"error" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{"type" => "none"})
|> json_response(403)
end
test "does not resolve a report for another account", %{conn: conn} do
[reporter, target_user, other_user] = insert_list(3, :user)
{:ok, report} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
assert %{"error" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{other_user.id}/action", %{
"type" => "none",
"report_id" => report.id
})
|> json_response_and_validate_schema(400)
assert %{data: %{"state" => "open"}} = Repo.reload!(report)
end
test "requires an action type", %{conn: conn} do
%{id: id} = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{})
|> json_response_and_validate_schema(400)
end
test "rejects unsupported action types", %{conn: conn} do
%{id: id} = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{"type" => "sensitive"})
|> json_response_and_validate_schema(400)
end
end
describe "POST /api/v1/admin/accounts/:id/enable" do
test "enable account", %{conn: conn} do
%{id: id} = user = insert(:user)
User.set_activation(user, false)
conn
|> post("/api/v1/admin/accounts/#{id}/enable")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_active: true} = user
end
end
describe "POST /api/v1/admin/accounts/:id/approve" do
test "approve account", %{conn: conn} do
%{id: id} = user = insert(:user, is_approved: false)
conn
|> post("/api/v1/admin/accounts/#{id}/approve")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_approved: true} = user
end
end
describe "POST /api/v1/admin/accounts/:id/reject" do
test "reject account", %{conn: conn} do
%{id: id} = user = insert(:user, is_approved: false)
conn
|> post("/api/v1/admin/accounts/#{id}/reject")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_active: false} = user
end
test "do not allow rejecting already accepted accounts", %{conn: conn} do
%{id: id} = user = insert(:user, is_approved: true)
assert %{"error" => "User is approved"} ==
conn
|> post("/api/v1/admin/accounts/#{id}/reject")
|> json_response_and_validate_schema(400)
user = Repo.reload!(user)
assert %{is_approved: true} = user
end
end
end

View file

@ -0,0 +1,239 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.Admin.ReportControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Rule
alias Pleroma.Web.CommonAPI
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
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 "GET /api/v1/admin/reports" do
test "get reports by state", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id1}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "this user uses an app called Hitler Tusky",
status_ids: [activity.id]
})
{:ok, %{id: report_id2}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
status_ids: [activity.id]
})
CommonAPI.update_report_state(report_id2, "resolved")
assert [%{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports?unresolved=true")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports?resolved=false")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id2}, %{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports?resolved=true&unresolved=true")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id2}] =
conn
|> get("/api/v1/admin/reports?resolved=true")
|> json_response_and_validate_schema(200)
end
test "get reports by target account", %{conn: conn} do
[reporter, target_user, other_target] = insert_list(3, :user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
{:ok, _report} =
CommonAPI.report(reporter, %{
account_id: other_target.id
})
assert [%{"id" => ^report_id}] =
conn
|> get("/api/v1/admin/reports?target_account_id=#{target_user.id}")
|> json_response_and_validate_schema(200)
end
end
describe "GET /api/v1/admin/reports/:id" do
test "get report by id", %{conn: conn} do
[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,
status_ids: [activity.id]
})
assert %{"id" => ^report_id} =
conn
|> get("/api/v1/admin/reports/#{report_id}")
|> json_response_and_validate_schema(200)
end
test "returns report rules", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
rule = Rule.create(%{text: "Be excellent to each other"})
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
rule_ids: [rule.id]
})
assert %{
"rules" => [
%{"id" => rule_id, "text" => "Be excellent to each other"}
]
} =
conn
|> get("/api/v1/admin/reports/#{report_id}")
|> json_response_and_validate_schema(200)
assert rule_id == to_string(rule.id)
end
test "returns 404 for a missing report", %{conn: conn} do
conn
|> get("/api/v1/admin/reports/0")
|> json_response_and_validate_schema(404)
end
end
describe "POST /api/v1/admin/reports/:id/resolve" do
test "resolve a report", %{conn: conn} do
[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,
status_ids: [activity.id]
})
assert %{"id" => ^report_id, "action_taken" => true} =
conn
|> post("/api/v1/admin/reports/#{report_id}/resolve")
|> json_response_and_validate_schema(200)
end
test "does not mutate a non-report activity", %{conn: conn} do
activity = insert(:note_activity)
conn
|> post("/api/v1/admin/reports/#{activity.id}/resolve")
|> json_response_and_validate_schema(404)
refute Repo.reload!(activity).data["state"]
end
end
describe "POST /api/v1/admin/reports/:id/reopen" do
test "reopen a report", %{conn: conn} do
[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,
status_ids: [activity.id]
})
CommonAPI.update_report_state(report_id, "resolved")
assert %{"id" => ^report_id, "action_taken" => false} =
conn
|> post("/api/v1/admin/reports/#{report_id}/reopen")
|> json_response_and_validate_schema(200)
end
test "does not mutate a non-report activity", %{conn: conn} do
activity = insert(:note_activity)
conn
|> post("/api/v1/admin/reports/#{activity.id}/reopen")
|> json_response_and_validate_schema(404)
refute Repo.reload!(activity).data["state"]
end
end
describe "POST /api/v1/admin/reports/:id/assign_to_self" do
test "assign and unassign a report", %{admin: admin, conn: conn} do
[reporter, target_user] = insert_pair(:user)
{:ok, report} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
assert %{"assigned_account" => %{"id" => admin_id}} =
conn
|> post("/api/v1/admin/reports/#{report.id}/assign_to_self")
|> json_response_and_validate_schema(200)
assert admin_id == admin.id
assert Repo.reload!(report).data["assigned_account"] == admin.id
assert %{"assigned_account" => nil} =
conn
|> post("/api/v1/admin/reports/#{report.id}/unassign")
|> json_response_and_validate_schema(200)
refute Repo.reload!(report).data["assigned_account"]
end
test "does not assign a non-report activity", %{conn: conn} do
%Activity{id: id} = activity = insert(:note_activity)
conn
|> post("/api/v1/admin/reports/#{id}/assign_to_self")
|> json_response_and_validate_schema(404)
refute Repo.reload!(activity).data["assigned_account"]
end
end
end

View file

@ -545,11 +545,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test "represent a relationship for the blocking and blocked user with expiry" do
user = insert(:user)
other_user = insert(:user)
date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second)
{:ok, user, other_user} = User.follow(user, other_user)
{:ok, _subscription} = User.subscribe(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60})
{:ok, user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60})
{:ok, _user_relationship} = User.block(other_user, user)
expected =
@ -558,7 +556,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
%{
following: false,
blocking: true,
block_expires_at: date,
block_expires_at: user_relationship.expires_at,
blocked_by: true,
id: to_string(other_user.id)
}
@ -570,17 +568,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test "represent a relationship for the muting user with expiry" do
user = insert(:user)
other_user = insert(:user)
date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second)
{:ok, _user_relationship} =
{:ok, user_relationships} =
User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60})
user_relationship =
Enum.find(user_relationships, &(&1.relationship_type == :mute))
expected =
Map.merge(
@blank_response,
%{
muting: true,
mute_expires_at: date,
mute_expires_at: user_relationship.expires_at,
muting_notifications: true,
id: to_string(other_user.id)
}