mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-14 18:18:02 +00:00
Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
386 lines
14 KiB
Python
386 lines
14 KiB
Python
#
|
|
# 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 parameterized import parameterized
|
|
|
|
from twisted.internet.testing import MemoryReactor
|
|
|
|
import synapse.rest.admin
|
|
from synapse.api.errors import Codes
|
|
from synapse.config.server_announcements import ServerAnnouncementRoom
|
|
from synapse.rest.client import login, room, sync
|
|
from synapse.server import HomeServer
|
|
from synapse.types import JsonDict
|
|
from synapse.util.clock import Clock
|
|
|
|
from tests import unittest
|
|
|
|
|
|
class ServerAnnouncementsTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
synapse.rest.admin.register_servlets,
|
|
login.register_servlets,
|
|
room.register_servlets,
|
|
sync.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.store = hs.get_datastores().main
|
|
self.handler = hs.get_server_announcements_handler()
|
|
self.config = hs.config.serverannouncements
|
|
self.creator = self.config.room_management_mxid
|
|
|
|
self.admin_user = self.register_user("admin", "pass", admin=True)
|
|
self.admin_user_tok = self.login("admin", "pass")
|
|
|
|
self.other_user = self.register_user("user", "pass")
|
|
self.other_user_tok = self.login("user", "pass")
|
|
|
|
self.sender = self.register_user("sender", "pass")
|
|
|
|
self.general = ServerAnnouncementRoom(
|
|
config_id="general",
|
|
room_name="Server announcements",
|
|
room_topic="Planned maintenance and incidents",
|
|
room_avatar_url="mxc://test/avatar",
|
|
room_avatar_path=None,
|
|
allowed_user_ids=[self.sender],
|
|
)
|
|
self.maintenance = ServerAnnouncementRoom(
|
|
config_id="maintenance",
|
|
room_name="Maintenance",
|
|
room_topic=None,
|
|
room_avatar_url=None,
|
|
room_avatar_path=None,
|
|
allowed_user_ids=[],
|
|
excluded_user_ids=[self.other_user],
|
|
)
|
|
self.config.rooms = [self.general, self.maintenance]
|
|
|
|
def _reconcile(self) -> dict[str, str]:
|
|
self.get_success(self.handler.reconcile_rooms())
|
|
room_ids = {}
|
|
for config_id in ("general", "maintenance"):
|
|
room_id = self.get_success(
|
|
self.store.get_server_announcement_room(config_id)
|
|
)
|
|
assert room_id is not None
|
|
room_ids[config_id] = room_id
|
|
return room_ids
|
|
|
|
def _expected(self, room: ServerAnnouncementRoom, room_id: str | None) -> JsonDict:
|
|
return {
|
|
"id": room.config_id,
|
|
"room_id": room_id,
|
|
"room_name": room.room_name,
|
|
"room_topic": room.room_topic,
|
|
"room_avatar_url": room.room_avatar_url,
|
|
"allowed_user_ids": room.allowed_user_ids,
|
|
"excluded_user_ids": room.excluded_user_ids,
|
|
}
|
|
|
|
def test_get_no_auth(self) -> None:
|
|
for url in (
|
|
"/_synapse/admin/v1/server_announcements",
|
|
"/_synapse/admin/v1/server_announcements/general",
|
|
):
|
|
channel = self.make_request("GET", url)
|
|
self.assertEqual(401, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
|
|
|
def test_get_requester_is_not_admin(self) -> None:
|
|
for url in (
|
|
"/_synapse/admin/v1/server_announcements",
|
|
"/_synapse/admin/v1/server_announcements/general",
|
|
):
|
|
channel = self.make_request("GET", 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_list_not_configured(self) -> None:
|
|
"""An unconfigured feature gives an empty list, not an error."""
|
|
self.config.rooms = []
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/_synapse/admin/v1/server_announcements",
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(
|
|
channel.json_body, {"management_user_id": self.creator, "rooms": []}
|
|
)
|
|
|
|
def test_list(self) -> None:
|
|
room_ids = self._reconcile()
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/_synapse/admin/v1/server_announcements",
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(
|
|
channel.json_body,
|
|
{
|
|
"management_user_id": self.creator,
|
|
"rooms": [
|
|
self._expected(self.general, room_ids["general"]),
|
|
self._expected(self.maintenance, room_ids["maintenance"]),
|
|
],
|
|
},
|
|
)
|
|
|
|
def test_list_before_reconcile(self) -> None:
|
|
"""A configured room that has not been created yet has no room ID."""
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/_synapse/admin/v1/server_announcements",
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(
|
|
[room["room_id"] for room in channel.json_body["rooms"]], [None, None]
|
|
)
|
|
|
|
def test_get_single(self) -> None:
|
|
room_ids = self._reconcile()
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/_synapse/admin/v1/server_announcements/maintenance",
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(
|
|
channel.json_body,
|
|
self._expected(self.maintenance, room_ids["maintenance"]),
|
|
)
|
|
|
|
def test_get_single_unknown(self) -> None:
|
|
self._reconcile()
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/_synapse/admin/v1/server_announcements/unknown",
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(404, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
|
|
|
def test_get_removed_from_config(self) -> None:
|
|
"""A room whose id left the config is neither listed nor found."""
|
|
room_ids = self._reconcile()
|
|
self.config.rooms = [self.general]
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/_synapse/admin/v1/server_announcements",
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(
|
|
channel.json_body["rooms"],
|
|
[self._expected(self.general, room_ids["general"])],
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/_synapse/admin/v1/server_announcements/maintenance",
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(404, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
|
|
|
def _send(
|
|
self,
|
|
content: JsonDict,
|
|
config_id: str = "general",
|
|
access_token: str | None = None,
|
|
expected_code: int = 200,
|
|
) -> JsonDict:
|
|
channel = self.make_request(
|
|
"POST",
|
|
f"/_synapse/admin/v1/server_announcements/{config_id}/send",
|
|
content=content,
|
|
access_token=access_token or self.admin_user_tok,
|
|
)
|
|
self.assertEqual(expected_code, channel.code, msg=channel.json_body)
|
|
return channel.json_body
|
|
|
|
def test_send_no_auth(self) -> None:
|
|
channel = self.make_request(
|
|
"POST", "/_synapse/admin/v1/server_announcements/general/send"
|
|
)
|
|
self.assertEqual(401, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
|
|
|
def test_send_requester_is_not_admin(self) -> None:
|
|
body = self._send(
|
|
{"content": {"msgtype": "m.text", "body": "x"}},
|
|
access_token=self.other_user_tok,
|
|
expected_code=403,
|
|
)
|
|
self.assertEqual(Codes.FORBIDDEN, body["errcode"])
|
|
|
|
def test_send_not_enabled(self) -> None:
|
|
self.config.rooms = []
|
|
body = self._send(
|
|
{"content": {"msgtype": "m.text", "body": "x"}}, expected_code=400
|
|
)
|
|
self.assertEqual(
|
|
"Server announcements are not enabled on this server", body["error"]
|
|
)
|
|
|
|
def test_send_unknown_id(self) -> None:
|
|
self._reconcile()
|
|
body = self._send(
|
|
{"content": {"msgtype": "m.text", "body": "x"}},
|
|
config_id="unknown",
|
|
expected_code=404,
|
|
)
|
|
self.assertEqual(Codes.NOT_FOUND, body["errcode"])
|
|
|
|
def test_send_removed_id(self) -> None:
|
|
"""A room whose id left the config is not sent into."""
|
|
self._reconcile()
|
|
self.config.rooms = [self.general]
|
|
body = self._send(
|
|
{"content": {"msgtype": "m.text", "body": "x"}},
|
|
config_id="maintenance",
|
|
expected_code=404,
|
|
)
|
|
self.assertEqual(Codes.NOT_FOUND, body["errcode"])
|
|
|
|
def test_send_purged_room(self) -> None:
|
|
"""A room purged at runtime gives a 500 until the next restart."""
|
|
room_ids = self._reconcile()
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
f"/_synapse/admin/v1/rooms/{room_ids['general']}",
|
|
{"purge": True},
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.reactor.advance(1)
|
|
|
|
body = self._send(
|
|
{"content": {"msgtype": "m.text", "body": "x"}}, expected_code=500
|
|
)
|
|
self.assertIn("restart", body["error"])
|
|
|
|
def test_send_missing_content(self) -> None:
|
|
self._reconcile()
|
|
body = self._send({"type": "m.room.message"}, expected_code=400)
|
|
self.assertEqual(Codes.MISSING_PARAM, body["errcode"])
|
|
|
|
@parameterized.expand(
|
|
[
|
|
("content_string", {"content": "x"}),
|
|
("content_list", {"content": [1]}),
|
|
("type_number", {"type": 42, "content": {"body": "x"}}),
|
|
]
|
|
)
|
|
def test_send_malformed_body(self, _name: str, request_body: JsonDict) -> None:
|
|
self._reconcile()
|
|
body = self._send(request_body, expected_code=400)
|
|
self.assertEqual(Codes.BAD_JSON, body["errcode"])
|
|
|
|
def test_send_state_key_rejected(self) -> None:
|
|
self._reconcile()
|
|
body = self._send(
|
|
{"type": "m.room.topic", "state_key": "", "content": {"topic": "x"}},
|
|
expected_code=400,
|
|
)
|
|
self.assertEqual(Codes.INVALID_PARAM, body["errcode"])
|
|
|
|
def test_send_member_rejected(self) -> None:
|
|
self._reconcile()
|
|
body = self._send(
|
|
{"type": "m.room.member", "content": {"membership": "join"}},
|
|
expected_code=400,
|
|
)
|
|
self.assertEqual(Codes.INVALID_PARAM, body["errcode"])
|
|
|
|
def test_send_invalid_message_content(self) -> None:
|
|
self._reconcile()
|
|
self._send({"content": {"foo": "bar"}}, expected_code=400)
|
|
|
|
def test_send(self) -> None:
|
|
"""A non-member receives the announcement in /sync, sent by the
|
|
management user.
|
|
"""
|
|
room_ids = self._reconcile()
|
|
body = self._send({"content": {"msgtype": "m.text", "body": "Hello"}})
|
|
self.assertEqual(body["room_id"], room_ids["general"])
|
|
|
|
channel = self.make_request("GET", "/sync", access_token=self.other_user_tok)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
events = channel.json_body["rooms"]["join"][room_ids["general"]]["timeline"][
|
|
"events"
|
|
]
|
|
event = next(e for e in events if e["event_id"] == body["event_id"])
|
|
self.assertEqual(event["sender"], self.creator)
|
|
self.assertEqual(event["type"], "m.room.message")
|
|
self.assertEqual(event["content"], {"msgtype": "m.text", "body": "Hello"})
|
|
|
|
def test_send_put_idempotent(self) -> None:
|
|
self._reconcile()
|
|
event_ids = []
|
|
for _ in range(2):
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/_synapse/admin/v1/server_announcements/general/send/txn1",
|
|
content={"content": {"msgtype": "m.text", "body": "Hello"}},
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
event_ids.append(channel.json_body["event_id"])
|
|
self.assertEqual(event_ids[0], event_ids[1])
|
|
|
|
def test_send_edit(self) -> None:
|
|
self._reconcile()
|
|
original = self._send({"content": {"msgtype": "m.text", "body": "Helo"}})
|
|
new_content = {"msgtype": "m.text", "body": "Hello"}
|
|
edit = self._send(
|
|
{
|
|
"content": {
|
|
"msgtype": "m.text",
|
|
"body": "* Hello",
|
|
"m.new_content": new_content,
|
|
"m.relates_to": {
|
|
"rel_type": "m.replace",
|
|
"event_id": original["event_id"],
|
|
},
|
|
}
|
|
}
|
|
)
|
|
|
|
event = self.get_success(self.store.get_event(edit["event_id"]))
|
|
self.assertEqual(event.content["m.new_content"], new_content)
|
|
self.assertEqual(
|
|
event.content["m.relates_to"]["event_id"], original["event_id"]
|
|
)
|
|
|
|
def test_send_redaction(self) -> None:
|
|
self._reconcile()
|
|
original = self._send({"content": {"msgtype": "m.text", "body": "Oops"}})
|
|
redaction = self._send(
|
|
{
|
|
"type": "m.room.redaction",
|
|
"content": {"redacts": original["event_id"]},
|
|
}
|
|
)
|
|
|
|
event = self.get_success(self.store.get_event(redaction["event_id"]))
|
|
self.assertEqual(event.redacts, original["event_id"])
|
|
redacted = self.get_success(self.store.get_event(original["event_id"]))
|
|
self.assertEqual(dict(redacted.content), {})
|