mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 11:17:51 +00:00
A1: Advertise MSC4428 and add unsigned.org.matrix.msc4428.stable_id
Add the `experimental_features.msc4428_enabled` flag. When set, `/versions` advertises `org.matrix.msc4428` and the Rust event serializer adds `unsigned.org.matrix.msc4428.stable_id` (the sender's user ID) to every event served to clients, including the events embedded in `unsigned` (`redacted_because`, bundled edits, thread summaries). The field is subject to the client's field allowlist like any other unsigned key. `SerializeEventConfig.__init__` gains a required `msc4428_enabled` argument (not module API). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
45e26a2afb
commit
45cfb3bc93
12 files changed
+390
-4
No files matched your search
@@ -145,6 +145,7 @@ experimental_features:
|
||||
msc4222_enabled: true
|
||||
# State DAGs
|
||||
msc4242_enabled: true
|
||||
msc4428_enabled: true
|
||||
|
||||
server_notices:
|
||||
system_mxid_localpart: _server
|
||||
|
||||
@@ -72,6 +72,7 @@ pub struct ExperimentalConfig {
|
||||
pub msc4169_enabled: bool,
|
||||
pub msc4354_enabled: bool,
|
||||
pub msc4222_enabled: bool,
|
||||
pub msc4428_enabled: bool,
|
||||
pub msc4491_enabled: bool,
|
||||
pub msc4143_enabled: bool,
|
||||
pub msc4446_enabled: bool,
|
||||
|
||||
@@ -84,6 +84,9 @@ pub mod unsigned_field {
|
||||
pub const MEMBERSHIP: &str = "membership";
|
||||
/// Unsigned field: msc4354_sticky_duration_ttl_ms (MSC4354)
|
||||
pub const STICKY_TTL: &str = "msc4354_sticky_duration_ttl_ms";
|
||||
/// Unsigned field: org.matrix.msc4428.stable_id (MSC4428), the stable
|
||||
/// identifier of the room member who sent the event.
|
||||
pub const MSC4428_STABLE_ID: &str = "org.matrix.msc4428.stable_id";
|
||||
/// Unsigned field: io.element.synapse.soft_failed (admin metadata)
|
||||
pub const SOFT_FAILED: &str = "io.element.synapse.soft_failed";
|
||||
/// Unsigned field: io.element.synapse.policy_server_spammy (admin metadata)
|
||||
|
||||
@@ -128,6 +128,10 @@ pub struct SerializeEventConfig {
|
||||
/// Whether MSC4354 (sticky events) is enabled. When `true`, the remaining
|
||||
/// stickiness TTL is computed and added to `unsigned`.
|
||||
msc4354_enabled: bool,
|
||||
/// Whether MSC4428 (stable identifiers for room members) is enabled. When
|
||||
/// `true`, the sender is repeated in `unsigned` as
|
||||
/// `org.matrix.msc4428.stable_id`.
|
||||
msc4428_enabled: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
@@ -142,6 +146,7 @@ impl SerializeEventConfig {
|
||||
include_stripped_room_state: bool,
|
||||
include_admin_metadata: bool,
|
||||
msc4354_enabled: bool,
|
||||
msc4428_enabled: bool,
|
||||
) -> PyResult<Self> {
|
||||
let requester = requester.map(|r| r.get().clone());
|
||||
|
||||
@@ -153,6 +158,7 @@ impl SerializeEventConfig {
|
||||
include_stripped_room_state,
|
||||
include_admin_metadata,
|
||||
msc4354_enabled,
|
||||
msc4428_enabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -518,6 +524,16 @@ fn serialize_event_value(
|
||||
}
|
||||
}
|
||||
|
||||
if config.msc4428_enabled {
|
||||
// MSC4428: clients must use this, not `sender`, to identify the room
|
||||
// member who sent the event: a later room version may drop `sender`.
|
||||
// Today the stable ID is the sender's user ID.
|
||||
unsigned.insert(
|
||||
unsigned_field::MSC4428_STABLE_ID.to_owned(),
|
||||
Value::String(event.sender().to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
if config.msc4354_enabled {
|
||||
if let Some(sticky_duration) = event.sticky_duration() {
|
||||
// min() ensures the origin server can't claim a time in the future
|
||||
|
||||
@@ -261,6 +261,9 @@ pub struct UnstableFeatureMap {
|
||||
/// MSC4354: Sticky events
|
||||
#[serde(rename = "org.matrix.msc4354")]
|
||||
msc4354: bool,
|
||||
/// MSC4428: Stable identifiers for room members.
|
||||
#[serde(rename = "org.matrix.msc4428")]
|
||||
msc4428: bool,
|
||||
/// MSC4380: Invite blocking
|
||||
#[serde(rename = "org.matrix.msc4380.stable")]
|
||||
msc4380: bool,
|
||||
@@ -329,6 +332,7 @@ pub fn synapse_config_to_global_unstable_feature_map(
|
||||
msc4169: config.experimental.msc4169_enabled,
|
||||
msc4262: config.server.include_profile_updates_in_sync,
|
||||
msc4354: config.experimental.msc4354_enabled,
|
||||
msc4428: config.experimental.msc4428_enabled,
|
||||
msc4380: true,
|
||||
msc4429: config.server.include_profile_updates_in_sync,
|
||||
msc4445_initial_sync_timeline_topological_ordering: true,
|
||||
|
||||
@@ -328,6 +328,9 @@ class EventUnsignedContentFields:
|
||||
|
||||
STICKY_TTL: Final = "msc4354_sticky_duration_ttl_ms"
|
||||
|
||||
# MSC4428: the stable identifier of the room member who sent the event.
|
||||
MSC4428_STABLE_ID: Final = "org.matrix.msc4428.stable_id"
|
||||
|
||||
|
||||
class MTextFields:
|
||||
"""Fields found inside m.text content blocks."""
|
||||
|
||||
@@ -171,6 +171,10 @@ class ExperimentalConfig(Config):
|
||||
# Enable the room version
|
||||
KNOWN_ROOM_VERSIONS.add_room_version(RoomVersions.MSC4242v12)
|
||||
|
||||
# MSC4428: Stable identifiers for room members. Adds
|
||||
# `unsigned.org.matrix.msc4428.stable_id` to every event served to clients.
|
||||
self.msc4428_enabled: bool = experimental.get("msc4428_enabled", False)
|
||||
|
||||
# MSC3391: Removing account data.
|
||||
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
|
||||
|
||||
|
||||
@@ -182,8 +182,8 @@ class EventClientSerializer:
|
||||
|
||||
Helper method that sets the `include_admin_metadata` field based on
|
||||
whether the requester is a server admin if it is not explicitly
|
||||
provided. Also sets the `msc4354_enabled` field based on the homeserver
|
||||
config.
|
||||
provided. Also sets the `msc4354_enabled` and `msc4428_enabled` fields
|
||||
based on the homeserver config.
|
||||
|
||||
Args:
|
||||
as_client_event: Whether to serialize the events as client events.
|
||||
@@ -218,6 +218,7 @@ class EventClientSerializer:
|
||||
include_stripped_room_state=include_stripped_room_state,
|
||||
include_admin_metadata=include_admin_metadata,
|
||||
msc4354_enabled=self._config.experimental.msc4354_enabled,
|
||||
msc4428_enabled=self._config.experimental.msc4428_enabled,
|
||||
)
|
||||
|
||||
async def serialize_event(
|
||||
|
||||
@@ -371,6 +371,7 @@ class SerializeEventConfig:
|
||||
include_stripped_room_state: bool,
|
||||
include_admin_metadata: bool,
|
||||
msc4354_enabled: bool,
|
||||
msc4428_enabled: bool,
|
||||
) -> None: ...
|
||||
@property
|
||||
def as_client_event(self) -> bool:
|
||||
@@ -411,6 +412,12 @@ class SerializeEventConfig:
|
||||
"""Whether MSC4354 (sticky events) is enabled. When ``True``, the
|
||||
remaining stickiness TTL is computed and added to ``unsigned``."""
|
||||
|
||||
@property
|
||||
def msc4428_enabled(self) -> bool:
|
||||
"""Whether MSC4428 (stable identifiers for room members) is enabled.
|
||||
When ``True``, the sender is repeated in ``unsigned`` as
|
||||
``org.matrix.msc4428.stable_id``."""
|
||||
|
||||
def serialize_events(
|
||||
events: list[tuple[Event, str | None]],
|
||||
time_now_ms: int,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import unittest as stdlib_unittest
|
||||
from typing import TYPE_CHECKING, Any, Mapping
|
||||
|
||||
from synapse.api.constants import EventContentFields
|
||||
from synapse.api.constants import EventContentFields, EventUnsignedContentFields
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.events import EventBase, StrippedStateEvent
|
||||
from synapse.events.utils import (
|
||||
@@ -41,7 +41,7 @@ from synapse.types import JsonDict
|
||||
from synapse.util.frozenutils import freeze
|
||||
|
||||
from tests.test_utils.event_builders import make_test_event
|
||||
from tests.unittest import HomeserverTestCase
|
||||
from tests.unittest import HomeserverTestCase, override_config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -927,6 +927,71 @@ class SerializeEventTestCase(HomeserverTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_msc4428_stable_id_absent_by_default(self) -> None:
|
||||
serialized = self.serialize(
|
||||
make_test_event(sender="@alice:localhost", room_id="!foo:bar"), None
|
||||
)
|
||||
self.assertNotIn(
|
||||
EventUnsignedContentFields.MSC4428_STABLE_ID, serialized["unsigned"]
|
||||
)
|
||||
|
||||
@override_config({"experimental_features": {"msc4428_enabled": True}})
|
||||
def test_msc4428_stable_id(self) -> None:
|
||||
"""With MSC4428 enabled, every serialized event carries the sender as
|
||||
`unsigned.org.matrix.msc4428.stable_id`, including the embedded
|
||||
`redacted_because` event."""
|
||||
redaction_id = "$redaction_event_id"
|
||||
|
||||
event = make_test_event(
|
||||
type="foo",
|
||||
event_id="test",
|
||||
sender="@alice:localhost",
|
||||
room_id="!foo:bar",
|
||||
content={"foo": "bar"},
|
||||
)
|
||||
event.internal_metadata.redacted_by = redaction_id
|
||||
|
||||
redaction_event = make_test_event(
|
||||
type="m.room.redaction",
|
||||
event_id=redaction_id,
|
||||
sender="@mod:localhost",
|
||||
content={"redacts": "test"},
|
||||
)
|
||||
|
||||
serialized = self.serialize(
|
||||
event, None, redaction_map={redaction_id: redaction_event}
|
||||
)
|
||||
self.assertEqual(
|
||||
serialized["unsigned"][EventUnsignedContentFields.MSC4428_STABLE_ID],
|
||||
"@alice:localhost",
|
||||
)
|
||||
self.assertEqual(
|
||||
serialized["unsigned"]["redacted_because"]["unsigned"][
|
||||
EventUnsignedContentFields.MSC4428_STABLE_ID
|
||||
],
|
||||
"@mod:localhost",
|
||||
)
|
||||
|
||||
# The field is subject to the client's field allowlist like any other
|
||||
# (the dots in its name need escaping in the allowlist).
|
||||
self.assertEqual(
|
||||
self.serialize(event, ["content.foo"]), {"content": {"foo": "bar"}}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.serialize(
|
||||
event,
|
||||
[
|
||||
"unsigned."
|
||||
+ EventUnsignedContentFields.MSC4428_STABLE_ID.replace(".", r"\.")
|
||||
],
|
||||
),
|
||||
{
|
||||
"unsigned": {
|
||||
EventUnsignedContentFields.MSC4428_STABLE_ID: "@alice:localhost"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CopyPowerLevelsContentTestCase(stdlib_unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
#
|
||||
# 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>.
|
||||
#
|
||||
"""Tests for MSC4428 (stable identifiers for room members): the
|
||||
`unsigned.org.matrix.msc4428.stable_id` field on events served to clients.
|
||||
|
||||
This stands in for the Complement test "stable_id on every event" (COMPLEMENT.md
|
||||
§2.2) until that suite exists: every event-returning client endpoint, including
|
||||
the events embedded in `unsigned` (`redacted_because`, bundled edits and thread
|
||||
summaries), carries the field.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Iterable
|
||||
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
|
||||
from synapse.api.constants import EventTypes, EventUnsignedContentFields, RelationTypes
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, relations, room, sync
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util.clock import Clock
|
||||
|
||||
from tests import unittest
|
||||
|
||||
STABLE_ID = EventUnsignedContentFields.MSC4428_STABLE_ID
|
||||
|
||||
|
||||
class StableIdTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
relations.register_servlets,
|
||||
sync.register_servlets,
|
||||
]
|
||||
|
||||
def default_config(self) -> JsonDict:
|
||||
config = super().default_config()
|
||||
config["experimental_features"] = {"msc4428_enabled": True}
|
||||
return config
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.alice = self.register_user("alice", "pass")
|
||||
self.alice_tok = self.login("alice", "pass")
|
||||
self.bob = self.register_user("bob", "pass")
|
||||
self.bob_tok = self.login("bob", "pass")
|
||||
|
||||
# A legacy room: MSC4428 applies to every room version.
|
||||
self.room_id = self.helper.create_room_as(self.alice, tok=self.alice_tok)
|
||||
self.helper.join(self.room_id, self.bob, tok=self.bob_tok)
|
||||
|
||||
# A message with an edit and a thread reply, so that bundled
|
||||
# aggregations embed events sent by someone else.
|
||||
self.parent_id = self.helper.send(self.room_id, body="Hi!", tok=self.alice_tok)[
|
||||
"event_id"
|
||||
]
|
||||
self.edit_id = self.helper.send_event(
|
||||
self.room_id,
|
||||
EventTypes.Message,
|
||||
{
|
||||
"msgtype": "m.text",
|
||||
"body": "* Hello!",
|
||||
"m.new_content": {"msgtype": "m.text", "body": "Hello!"},
|
||||
"m.relates_to": {
|
||||
"rel_type": RelationTypes.REPLACE,
|
||||
"event_id": self.parent_id,
|
||||
},
|
||||
},
|
||||
tok=self.alice_tok,
|
||||
)["event_id"]
|
||||
self.reply_id = self.helper.send_event(
|
||||
self.room_id,
|
||||
EventTypes.Message,
|
||||
{
|
||||
"msgtype": "m.text",
|
||||
"body": "reply",
|
||||
"m.relates_to": {
|
||||
"rel_type": RelationTypes.THREAD,
|
||||
"event_id": self.parent_id,
|
||||
},
|
||||
},
|
||||
tok=self.bob_tok,
|
||||
)["event_id"]
|
||||
|
||||
# A message redacted by someone other than its sender, so that the
|
||||
# embedded `redacted_because` has a different stable ID to its parent.
|
||||
self.redacted_id = self.helper.send(
|
||||
self.room_id, body="oops", tok=self.bob_tok
|
||||
)["event_id"]
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
f"/rooms/{self.room_id}/redact/{self.redacted_id}",
|
||||
{},
|
||||
access_token=self.alice_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
self.redaction_id = channel.json_body["event_id"]
|
||||
|
||||
def assert_stable_ids(self, events: Iterable[JsonDict]) -> list[str]:
|
||||
"""Assert every event, and every event embedded in its `unsigned`,
|
||||
carries `stable_id == sender`. Returns the event IDs checked."""
|
||||
checked = []
|
||||
for event in events:
|
||||
checked.extend(self._assert_stable_id(event))
|
||||
return checked
|
||||
|
||||
def _assert_stable_id(self, event: JsonDict) -> list[str]:
|
||||
self.assertEqual(
|
||||
event.get("unsigned", {}).get(STABLE_ID),
|
||||
event["sender"],
|
||||
f"stable_id missing or wrong on {event.get('event_id')}: {event}",
|
||||
)
|
||||
checked = [event["event_id"]]
|
||||
|
||||
unsigned = event.get("unsigned", {})
|
||||
if "redacted_because" in unsigned:
|
||||
checked.extend(self._assert_stable_id(unsigned["redacted_because"]))
|
||||
bundled = unsigned.get("m.relations", {})
|
||||
if RelationTypes.REPLACE in bundled:
|
||||
checked.extend(self._assert_stable_id(bundled[RelationTypes.REPLACE]))
|
||||
if RelationTypes.THREAD in bundled:
|
||||
checked.extend(
|
||||
self._assert_stable_id(bundled[RelationTypes.THREAD]["latest_event"])
|
||||
)
|
||||
return checked
|
||||
|
||||
def test_sync(self) -> None:
|
||||
# Limit the timeline so that the state block is populated too.
|
||||
sync_filter = json.dumps({"room": {"timeline": {"limit": 3}}})
|
||||
channel = self.make_request(
|
||||
"GET", f"/sync?filter={sync_filter}", access_token=self.alice_tok
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
room = channel.json_body["rooms"]["join"][self.room_id]
|
||||
|
||||
self.assertGreater(len(room["state"]["events"]), 0)
|
||||
self.assert_stable_ids(room["state"]["events"])
|
||||
checked = self.assert_stable_ids(room["timeline"]["events"])
|
||||
# The redacted message, with its embedded redaction, is in the timeline.
|
||||
self.assertIn(self.redacted_id, checked)
|
||||
self.assertIn(self.redaction_id, checked)
|
||||
|
||||
def test_messages(self) -> None:
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/rooms/{self.room_id}/messages?dir=b",
|
||||
access_token=self.alice_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
checked = self.assert_stable_ids(channel.json_body["chunk"])
|
||||
# The whole room, with the embedded redaction and edit.
|
||||
for event_id in (
|
||||
self.parent_id,
|
||||
self.edit_id,
|
||||
self.reply_id,
|
||||
self.redacted_id,
|
||||
self.redaction_id,
|
||||
):
|
||||
self.assertIn(event_id, checked)
|
||||
|
||||
def test_context(self) -> None:
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/rooms/{self.room_id}/context/{self.parent_id}",
|
||||
access_token=self.alice_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
body = channel.json_body
|
||||
self.assert_stable_ids([body["event"]])
|
||||
self.assert_stable_ids(body["events_before"])
|
||||
self.assert_stable_ids(body["events_after"])
|
||||
self.assertGreater(len(body["state"]), 0)
|
||||
self.assert_stable_ids(body["state"])
|
||||
|
||||
def test_event(self) -> None:
|
||||
for event_id in (self.parent_id, self.redacted_id):
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/rooms/{self.room_id}/event/{event_id}",
|
||||
access_token=self.alice_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
self.assert_stable_ids([channel.json_body])
|
||||
|
||||
# The bundled edit and thread summary on the parent are events sent by
|
||||
# alice and bob respectively; both carry their own sender.
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/rooms/{self.room_id}/event/{self.parent_id}",
|
||||
access_token=self.alice_tok,
|
||||
)
|
||||
bundled = channel.json_body["unsigned"]["m.relations"]
|
||||
self.assertEqual(
|
||||
bundled[RelationTypes.REPLACE]["unsigned"][STABLE_ID], self.alice
|
||||
)
|
||||
self.assertEqual(
|
||||
bundled[RelationTypes.THREAD]["latest_event"]["unsigned"][STABLE_ID],
|
||||
self.bob,
|
||||
)
|
||||
|
||||
def test_state(self) -> None:
|
||||
channel = self.make_request(
|
||||
"GET", f"/rooms/{self.room_id}/state", access_token=self.alice_tok
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.text_body)
|
||||
self.assertGreater(len(channel.json_list), 0)
|
||||
self.assert_stable_ids(channel.json_list)
|
||||
|
||||
def test_relations(self) -> None:
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/_matrix/client/v1/rooms/{self.room_id}/relations/{self.parent_id}",
|
||||
access_token=self.alice_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
checked = self.assert_stable_ids(channel.json_body["chunk"])
|
||||
self.assertIn(self.edit_id, checked)
|
||||
self.assertIn(self.reply_id, checked)
|
||||
|
||||
def test_threads(self) -> None:
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/_matrix/client/v1/rooms/{self.room_id}/threads",
|
||||
access_token=self.alice_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
checked = self.assert_stable_ids(channel.json_body["chunk"])
|
||||
self.assertIn(self.parent_id, checked)
|
||||
# The thread summary embeds bob's reply.
|
||||
self.assertIn(self.reply_id, checked)
|
||||
|
||||
|
||||
class StableIdDisabledTestCase(unittest.HomeserverTestCase):
|
||||
"""Without the flag nothing changes on the wire."""
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
sync.register_servlets,
|
||||
]
|
||||
|
||||
def test_no_stable_id(self) -> None:
|
||||
alice = self.register_user("alice", "pass")
|
||||
alice_tok = self.login("alice", "pass")
|
||||
room_id = self.helper.create_room_as(alice, tok=alice_tok)
|
||||
event_id = self.helper.send(room_id, body="Hi!", tok=alice_tok)["event_id"]
|
||||
|
||||
channel = self.make_request(
|
||||
"GET", f"/rooms/{room_id}/event/{event_id}", access_token=alice_tok
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
self.assertNotIn(STABLE_ID, channel.json_body["unsigned"])
|
||||
|
||||
channel = self.make_request("GET", "/sync", access_token=alice_tok)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
for event in channel.json_body["rooms"]["join"][room_id]["timeline"]["events"]:
|
||||
self.assertNotIn(STABLE_ID, event.get("unsigned", {}))
|
||||
@@ -145,6 +145,17 @@ class VersionsTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
self.assertTrue(channel.json_body["unstable_features"]["io.element.msc4502"])
|
||||
|
||||
def test_msc4428_false_by_default(self) -> None:
|
||||
channel = self.make_request("GET", "/_matrix/client/versions")
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
self.assertFalse(channel.json_body["unstable_features"]["org.matrix.msc4428"])
|
||||
|
||||
@unittest.override_config({"experimental_features": {"msc4428_enabled": True}})
|
||||
def test_msc4428_true_if_enabled(self) -> None:
|
||||
channel = self.make_request("GET", "/_matrix/client/versions")
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
self.assertTrue(channel.json_body["unstable_features"]["org.matrix.msc4428"])
|
||||
|
||||
def _sanity_check_versions_response(self, versions_response: JsonDict) -> None:
|
||||
"""
|
||||
Make sure this looks like a `/_matrix/client/versions` response
|
||||
|
||||
Reference in new issue
Block a user