Add Admin API endpoints to manage user reports (#19657)

Adds [Admin
API](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html)
endpoints to list, fetch and delete user reports from the homeserver.
Follows on from #18120, which added the endpoints to report users.
This commit is contained in:
Shay authored and GitHub committed 2026-04-17 11:01:23 +01:00
1 parent bdb1cf7416
commit 647fb59190
5 files changed
+951

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
Adds [Admin API](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html) endpoints to
list, fetch and delete user reports.
+6
View File
@@ -96,6 +96,10 @@ from synapse.rest.admin.statistics import (
LargestRoomsStatistics,
UserMediaStatisticsRestServlet,
)
from synapse.rest.admin.user_reports import (
UserReportDetailRestServlet,
UserReportsRestServlet,
)
from synapse.rest.admin.username_available import UsernameAvailableRestServlet
from synapse.rest.admin.users import (
AccountDataRestServlet,
@@ -312,6 +316,8 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
LargestRoomsStatistics(hs).register(http_server)
EventReportDetailRestServlet(hs).register(http_server)
EventReportsRestServlet(hs).register(http_server)
UserReportsRestServlet(hs).register(http_server)
UserReportDetailRestServlet(hs).register(http_server)
AccountDataRestServlet(hs).register(http_server)
PushersRestServlet(hs).register(http_server)
MakeRoomAdminRestServlet(hs).register(http_server)
+173
View File
@@ -0,0 +1,173 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
import logging
from http import HTTPStatus
from typing import TYPE_CHECKING
from synapse.api.constants import Direction
from synapse.api.errors import Codes, NotFoundError, SynapseError
from synapse.http.servlet import RestServlet, parse_enum, parse_integer, parse_string
from synapse.http.site import SynapseRequest
from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin
from synapse.types import JsonDict
if TYPE_CHECKING:
from synapse.server import HomeServer
logger = logging.getLogger(__name__)
class UserReportsRestServlet(RestServlet):
"""
List all reported users that are known to the homeserver. Results are returned
in a dictionary containing report information. Supports pagination.
The requester must have administrator access in Synapse.
GET /_synapse/admin/v1/user_reports
returns:
200 OK with list of reports if success otherwise an error.
Args:
The parameters `from` and `limit` are required only for pagination.
By default, a `limit` of 100 is used.
The parameter `dir` can be used to define the order of results.
The `user_id` query parameter filters by the user ID of the reporter of the target user.
The `target_user_id` query parameter filters by user id of the target user.
Returns:
A list of user reprots and an integer representing the total number of user
reports that exist given this query
"""
PATTERNS = admin_patterns("/user_reports$")
def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastores().main
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
start = parse_integer(request, "from", default=0)
limit = parse_integer(request, "limit", default=100)
direction = parse_enum(request, "dir", Direction, Direction.BACKWARDS)
user_id = parse_string(request, "user_id")
target_user_id = parse_string(request, "target_user_id")
if start < 0:
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"The start parameter must be a positive integer.",
errcode=Codes.INVALID_PARAM,
)
if limit < 0:
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"The limit parameter must be a positive integer.",
errcode=Codes.INVALID_PARAM,
)
user_reports, total = await self._store.get_user_reports_paginate(
start, limit, direction, user_id, target_user_id
)
ret = {"user_reports": user_reports, "total": total}
if (start + limit) < total:
ret["next_token"] = start + len(user_reports)
return HTTPStatus.OK, ret
class UserReportDetailRestServlet(RestServlet):
"""
Get a specific user report that is known to the homeserver. Results are returned
in a dictionary containing report information.
The requester must have administrator access in Synapse.
GET /_synapse/admin/v1/user_reports/<report_id>
returns:
200 OK with details report if success otherwise an error.
Args:
The parameter `report_id` is the ID of the user report in the database.
Returns:
JSON blob of information about the user report
"""
PATTERNS = admin_patterns("/user_reports/(?P<report_id>[^/]*)$")
def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastores().main
async def on_GET(
self, request: SynapseRequest, report_id: str
) -> tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
message = (
"The report_id parameter must be a string representing a positive integer."
)
try:
resolved_report_id = int(report_id)
except ValueError:
raise SynapseError(
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
)
if resolved_report_id < 0:
raise SynapseError(
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
)
ret = await self._store.get_user_report(resolved_report_id)
if not ret:
raise NotFoundError("User report not found")
id, received_ts, target_user_id, user_id, reason = ret
response = {
"id": id,
"received_ts": received_ts,
"target_user_id": target_user_id,
"user_id": user_id,
"reason": reason,
}
return HTTPStatus.OK, response
async def on_DELETE(
self, request: SynapseRequest, report_id: str
) -> tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
message = (
"The report_id parameter must be a string representing a positive integer."
)
try:
resolved_report_id = int(report_id)
except ValueError:
raise SynapseError(
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
)
if resolved_report_id < 0:
raise SynapseError(
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
)
if await self._store.delete_user_report(resolved_report_id):
return HTTPStatus.OK, {}
raise NotFoundError("User report not found")
+126
View File
@@ -2277,6 +2277,132 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
return True
async def get_user_report(
self, report_id: int
) -> tuple[int, int, str, str, str] | None:
"""Retrieve a user report
Args:
report_id: ID of user report in database
Returns:
JSON dict of information from a user report or None if the
report does not exist.
"""
return await self.db_pool.simple_select_one(
table="user_reports",
keyvalues={"id": report_id},
retcols=("id", "received_ts", "target_user_id", "user_id", "reason"),
allow_none=True,
desc="get_user_report",
)
async def get_user_reports_paginate(
self,
start: int,
limit: int,
direction: Direction = Direction.BACKWARDS,
user_id: str | None = None,
target_user_id: str | None = None,
) -> tuple[list[JsonDict], int]:
"""Retrieve a paginated list of user reports
Args:
start: event offset to begin the query from
limit: number of rows to retrieve
direction: Whether to fetch the most recent first (backwards) or the
oldest first (forwards)
user_id: search for user_id of the reporter. Ignored if user_id is None
target_user_id: search for user_id of the target. Ignored if target_user_id is None
Returns:
Tuple of:
json list of user reports
total number of user reports matching the filter criteria
"""
def _get_user_reports_paginate_txn(
txn: LoggingTransaction,
) -> tuple[list[dict[str, Any]], int]:
filters = []
args: list[object] = []
if user_id:
filters.append("user_id LIKE ?")
args.extend(["%" + user_id + "%"])
if target_user_id:
filters.append("target_user_id LIKE ?")
args.extend(["%" + target_user_id + "%"])
if direction == Direction.BACKWARDS:
order = "DESC"
else:
order = "ASC"
where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""
sql = f"""
SELECT COUNT(*) as total_user_reports
FROM user_reports {where_clause}
"""
txn.execute(sql, args)
count = cast(tuple[int], txn.fetchone())[0]
sql = f"""
SELECT
id,
received_ts,
target_user_id,
user_id,
reason
FROM user_reports
{where_clause}
ORDER BY received_ts {order}
LIMIT ?
OFFSET ?
"""
args += [limit, start]
txn.execute(sql, args)
user_reports = []
for row in txn:
user_reports.append(
{
"id": row[0],
"received_ts": row[1],
"target_user_id": row[2],
"user_id": row[3],
"reason": row[4],
}
)
return user_reports, count
return await self.db_pool.runInteraction(
"get_user_reports_paginate", _get_user_reports_paginate_txn
)
async def delete_user_report(self, report_id: int) -> bool:
"""Remove a user report from database.
Args:
report_id: Report to delete
Returns:
Whether the report was successfully deleted or not.
"""
try:
await self.db_pool.simple_delete_one(
table="user_reports",
keyvalues={"id": report_id},
desc="delete_user_report",
)
except StoreError:
# Deletion failed because report does not exist
return False
return True
async def set_room_is_public(self, room_id: str, is_public: bool) -> None:
await self.db_pool.simple_update_one(
table="rooms",
+644
View File
@@ -0,0 +1,644 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
from twisted.internet.testing import MemoryReactor
import synapse.rest.admin
from synapse.api.errors import Codes
from synapse.rest.client import login, reporting, room
from synapse.server import HomeServer
from synapse.types import JsonDict
from synapse.util.clock import Clock
from tests import unittest
class UserReportsTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
room.register_servlets,
reporting.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")
self.users = {}
for i in range(10):
self.users[i] = self.register_user(f"user{i}", "pass")
# users 1 and 2 report all other users
reporter_1_tok = self.login(self.users[0], "pass")
reporter_2_tok = self.login(self.users[1], "pass")
for num, user in self.users.items():
if num <= 1:
continue
if num % 2 == 0:
self._report_user(user, reporter_1_tok)
else:
self._report_user(user, reporter_2_tok)
self.url = "/_synapse/admin/v1/user_reports"
def test_no_auth(self) -> None:
"""
Try to get a user report without authentication.
"""
channel = self.make_request("GET", self.url, {})
self.assertEqual(401, channel.code, msg=channel.json_body)
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
def test_requester_is_no_admin(self) -> None:
"""
If the user is not a server admin, an error 403 is returned.
"""
rando_tok = self.login(self.users[4], "pass")
channel = self.make_request(
"GET",
self.url,
access_token=rando_tok,
)
self.assertEqual(403, channel.code, msg=channel.json_body)
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
def test_default_success(self) -> None:
"""
Testing list of reported users
"""
channel = self.make_request(
"GET",
self.url,
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 8)
self.assertNotIn("next_token", channel.json_body)
self._check_fields(channel.json_body["user_reports"])
def test_limit(self) -> None:
"""
Testing list of reported users with limit
"""
channel = self.make_request(
"GET",
self.url + "?limit=5",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 5)
self.assertEqual(channel.json_body["next_token"], 5)
self._check_fields(channel.json_body["user_reports"])
def test_from(self) -> None:
"""
Testing list of reported users with a defined starting point (from)
"""
channel = self.make_request(
"GET",
self.url + "?from=5",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 3)
self.assertNotIn("next_token", channel.json_body)
self._check_fields(channel.json_body["user_reports"])
def test_limit_and_from(self) -> None:
"""
Testing list of reported users with a defined starting point and limit
"""
channel = self.make_request(
"GET",
self.url + "?from=2&limit=5",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(channel.json_body["next_token"], 7)
self.assertEqual(len(channel.json_body["user_reports"]), 5)
self._check_fields(channel.json_body["user_reports"])
def test_filter_by_target_user_id(self) -> None:
"""
Testing list of reported users with a filter of target_user_id
"""
channel = self.make_request(
"GET",
self.url + "?target_user_id=%s" % self.users[3],
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 1)
self.assertEqual(len(channel.json_body["user_reports"]), 1)
self.assertNotIn("next_token", channel.json_body)
self._check_fields(channel.json_body["user_reports"])
for report in channel.json_body["user_reports"]:
self.assertEqual(report["target_user_id"], self.users[3])
def test_filter_user(self) -> None:
"""
Testing list of reported users with a filter of reporting user
"""
channel = self.make_request(
"GET",
self.url + "?user_id=%s" % self.users[0],
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 4)
self.assertEqual(len(channel.json_body["user_reports"]), 4)
self.assertNotIn("next_token", channel.json_body)
self._check_fields(channel.json_body["user_reports"])
for report in channel.json_body["user_reports"]:
self.assertEqual(report["user_id"], self.users[0])
def test_filter_user_and_target_user(self) -> None:
"""
Testing list of reported users with a filter of reporting user and target_user
"""
channel = self.make_request(
"GET",
self.url + "?user_id=%s&target_user_id=%s" % (self.users[1], self.users[7]),
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 1)
self.assertEqual(len(channel.json_body["user_reports"]), 1)
self.assertNotIn("next_token", channel.json_body)
self._check_fields(channel.json_body["user_reports"])
for report in channel.json_body["user_reports"]:
self.assertEqual(report["user_id"], self.users[1])
self.assertEqual(report["target_user_id"], self.users[7])
def test_valid_search_order(self) -> None:
"""
Testing search order. Order by timestamps.
"""
# fetch the most recent first, largest timestamp
channel = self.make_request(
"GET",
self.url + "?dir=b",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 8)
report = 1
while report < len(channel.json_body["user_reports"]):
self.assertGreaterEqual(
channel.json_body["user_reports"][report - 1]["received_ts"],
channel.json_body["user_reports"][report]["received_ts"],
)
report += 1
# fetch the oldest first, smallest timestamp
channel = self.make_request(
"GET",
self.url + "?dir=f",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 8)
report = 1
while report < len(channel.json_body["user_reports"]):
self.assertLessEqual(
channel.json_body["user_reports"][report - 1]["received_ts"],
channel.json_body["user_reports"][report]["received_ts"],
)
report += 1
def test_invalid_search_order(self) -> None:
"""
Testing that a invalid search order returns a 400
"""
channel = self.make_request(
"GET",
self.url + "?dir=bar",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
self.assertEqual(
"Query parameter 'dir' must be one of ['b', 'f']",
channel.json_body["error"],
)
def test_limit_is_negative(self) -> None:
"""
Testing that a negative limit parameter returns a 400
"""
channel = self.make_request(
"GET",
self.url + "?limit=-5",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
def test_from_is_negative(self) -> None:
"""
Testing that a negative from parameter returns a 400
"""
channel = self.make_request(
"GET",
self.url + "?from=-5",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
def test_next_token(self) -> None:
"""
Testing that `next_token` appears at the right place
"""
# `next_token` does not appear
# Number of results is the number of entries
channel = self.make_request(
"GET",
self.url + "?limit=8",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 8)
self.assertNotIn("next_token", channel.json_body)
# `next_token` does not appear
# Number of max results is larger than the number of entries
channel = self.make_request(
"GET",
self.url + "?limit=10",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 8)
self.assertNotIn("next_token", channel.json_body)
# `next_token` does appear
# Number of max results is smaller than the number of entries
channel = self.make_request(
"GET",
self.url + "?limit=6",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 6)
self.assertEqual(channel.json_body["next_token"], 6)
# Check
# Set `from` to value of `next_token` for request remaining entries
# `next_token` does not appear
channel = self.make_request(
"GET",
self.url + "?from=6",
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(channel.json_body["total"], 8)
self.assertEqual(len(channel.json_body["user_reports"]), 2)
self.assertNotIn("next_token", channel.json_body)
def _report_user(self, target_user: str, reporter_tok: str) -> None:
"""Report a user"""
channel = self.make_request(
"POST",
"_matrix/client/v3/users/%s/report" % (target_user),
{"reason": "stone-cold bummer"},
access_token=reporter_tok,
shorthand=False,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
def _check_fields(self, content: list[JsonDict]) -> None:
"""Checks that all attributes are present in a user report"""
for c in content:
self.assertIn("id", c)
self.assertIn("received_ts", c)
self.assertIn("user_id", c)
self.assertIn("target_user_id", c)
self.assertIn("reason", c)
class UserReportDetailTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
room.register_servlets,
reporting.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")
self.bad_user = self.register_user("user", "pass")
self.bad_user_tok = self.login("user", "pass")
channel = self.make_request(
"POST",
"_matrix/client/v3/users/%s/report" % (self.bad_user),
{"reason": "stone-cold bummer"},
access_token=self.admin_user_tok,
shorthand=False,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
# first created user report gets `id`=2
self.url = "/_synapse/admin/v1/user_reports/2"
def test_no_auth(self) -> None:
"""
Try to get user report without authentication.
"""
channel = self.make_request("GET", self.url, {})
self.assertEqual(401, channel.code, msg=channel.json_body)
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
def test_requester_is_no_admin(self) -> None:
"""
If the user is not a server admin, an error 403 is returned.
"""
channel = self.make_request(
"GET",
self.url,
access_token=self.bad_user_tok,
)
self.assertEqual(403, channel.code, msg=channel.json_body)
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
def test_default_success(self) -> None:
"""
Testing get a reported user
"""
channel = self.make_request(
"GET",
self.url,
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self._check_fields(channel.json_body)
def test_invalid_report_id(self) -> None:
"""
Testing that an invalid `report_id` returns a 400.
"""
# `report_id` is negative
channel = self.make_request(
"GET",
"/_synapse/admin/v1/user_reports/-123",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
self.assertEqual(
"The report_id parameter must be a string representing a positive integer.",
channel.json_body["error"],
)
# `report_id` is a non-numerical string
channel = self.make_request(
"GET",
"/_synapse/admin/v1/user_reports/abcdef",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
self.assertEqual(
"The report_id parameter must be a string representing a positive integer.",
channel.json_body["error"],
)
# `report_id` is undefined
channel = self.make_request(
"GET",
"/_synapse/admin/v1/user_reports/",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
self.assertEqual(
"The report_id parameter must be a string representing a positive integer.",
channel.json_body["error"],
)
def test_report_id_not_found(self) -> None:
"""
Testing that a not existing `report_id` returns a 404.
"""
channel = self.make_request(
"GET",
"/_synapse/admin/v1/user_reports/123",
access_token=self.admin_user_tok,
)
self.assertEqual(404, channel.code, msg=channel.json_body)
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
self.assertEqual("User report not found", channel.json_body["error"])
def _check_fields(self, content: JsonDict) -> None:
"""Checks that all attributes are present in a user report"""
self.assertIn("id", content)
self.assertIn("received_ts", content)
self.assertIn("target_user_id", content)
self.assertIn("user_id", content)
self.assertIn("reason", content)
class DeleteUserReportTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self._store = hs.get_datastores().main
self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")
self.bad_user = self.register_user("user", "pass")
self.other_user_tok = self.login("user", "pass")
# create report
self.get_success(
self._store.add_user_report(
self.bad_user,
self.admin_user,
"super bummer",
self.clock.time_msec(),
)
)
self.url = "/_synapse/admin/v1/user_reports/2"
def test_no_auth(self) -> None:
"""
Try to delete user report without authentication.
"""
channel = self.make_request("DELETE", self.url)
self.assertEqual(401, channel.code, msg=channel.json_body)
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
def test_requester_is_no_admin(self) -> None:
"""
If the user is not a server admin, an error 403 is returned.
"""
channel = self.make_request(
"DELETE",
self.url,
access_token=self.other_user_tok,
)
self.assertEqual(403, channel.code, msg=channel.json_body)
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
def test_delete_success(self) -> None:
"""
Testing delete a report.
"""
channel = self.make_request(
"DELETE",
self.url,
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual({}, channel.json_body)
channel = self.make_request(
"GET",
self.url,
access_token=self.admin_user_tok,
)
# check that report was deleted
self.assertEqual(404, channel.code, msg=channel.json_body)
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
def test_invalid_report_id(self) -> None:
"""
Testing that an invalid `report_id` returns a 400.
"""
# `report_id` is negative
channel = self.make_request(
"DELETE",
"/_synapse/admin/v1/user_reports/-123",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
self.assertEqual(
"The report_id parameter must be a string representing a positive integer.",
channel.json_body["error"],
)
# `report_id` is a non-numerical string
channel = self.make_request(
"DELETE",
"/_synapse/admin/v1/user_reports/abcdef",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
self.assertEqual(
"The report_id parameter must be a string representing a positive integer.",
channel.json_body["error"],
)
# `report_id` is undefined
channel = self.make_request(
"DELETE",
"/_synapse/admin/v1/user_reports/",
access_token=self.admin_user_tok,
)
self.assertEqual(400, channel.code, msg=channel.json_body)
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
self.assertEqual(
"The report_id parameter must be a string representing a positive integer.",
channel.json_body["error"],
)
def test_report_id_not_found(self) -> None:
"""
Testing that a not existing `report_id` returns a 404.
"""
channel = self.make_request(
"DELETE",
"/_synapse/admin/v1/user_reports/123",
access_token=self.admin_user_tok,
)
self.assertEqual(404, channel.code, msg=channel.json_body)
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
self.assertEqual("User report not found", channel.json_body["error"])