diff --git a/tests/rest/admin/test_server_notice.py b/tests/rest/admin/test_server_notice.py
index dfd14f5751..2398bc503a 100644
--- a/tests/rest/admin/test_server_notice.py
+++ b/tests/rest/admin/test_server_notice.py
@@ -477,6 +477,33 @@ class ServerNoticeTestCase(unittest.HomeserverTestCase):
# second room has new ID
self.assertNotEqual(first_room_id, second_room_id)
+ @override_config(
+ {"server_notices": {"system_mxid_localpart": "notices", "auto_join": True}}
+ )
+ def test_auto_join(self) -> None:
+ """
+ Tests that the user get automatically joined to the notice room
+ when `auto_join` setting is used.
+ """
+ # user has no room memberships
+ self._check_invite_and_join_status(self.other_user, 0, 0)
+
+ # send server notice
+ server_notice_request_content = {
+ "user_id": self.other_user,
+ "content": {"msgtype": "m.text", "body": "test msg one"},
+ }
+
+ self.make_request(
+ "POST",
+ self.url,
+ access_token=self.admin_user_tok,
+ content=server_notice_request_content,
+ )
+
+ # user has joined the room
+ self._check_invite_and_join_status(self.other_user, 0, 1)
+
@override_config({"server_notices": {"system_mxid_localpart": "notices"}})
def test_update_notice_user_name_when_changed(self) -> None:
"""
diff --git a/tests/rest/client/test_profile.py b/tests/rest/client/test_profile.py
index 8f923fd40f..eb0fa00bb3 100644
--- a/tests/rest/client/test_profile.py
+++ b/tests/rest/client/test_profile.py
@@ -312,6 +312,166 @@ class ProfileTestCase(unittest.HomeserverTestCase):
)
self.assertEqual(channel.code, 200, channel.result)
+ @unittest.override_config(
+ {"experimental_features": {"msc4069_profile_inhibit_propagation": True}}
+ )
+ def test_msc4069_inhibit_propagation(self) -> None:
+ """Tests to ensure profile update propagation can be inhibited."""
+ for prop in ["avatar_url", "displayname"]:
+ room_id = self.helper.create_room_as(tok=self.owner_tok)
+
+ channel = self.make_request(
+ "PUT",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ content={"membership": "join", prop: "mxc://my.server/existing"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ channel = self.make_request(
+ "PUT",
+ f"/profile/{self.owner}/{prop}?org.matrix.msc4069.propagate=false",
+ content={prop: "http://my.server/pic.gif"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ res = (
+ self._get_avatar_url()
+ if prop == "avatar_url"
+ else self._get_displayname()
+ )
+ self.assertEqual(res, "http://my.server/pic.gif")
+
+ channel = self.make_request(
+ "GET",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+ self.assertEqual(channel.json_body.get(prop), "mxc://my.server/existing")
+
+ def test_msc4069_inhibit_propagation_disabled(self) -> None:
+ """Tests to ensure profile update propagation inhibit flags are ignored when the
+ experimental flag is not enabled.
+ """
+ for prop in ["avatar_url", "displayname"]:
+ room_id = self.helper.create_room_as(tok=self.owner_tok)
+
+ channel = self.make_request(
+ "PUT",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ content={"membership": "join", prop: "mxc://my.server/existing"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ channel = self.make_request(
+ "PUT",
+ f"/profile/{self.owner}/{prop}?org.matrix.msc4069.propagate=false",
+ content={prop: "http://my.server/pic.gif"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ res = (
+ self._get_avatar_url()
+ if prop == "avatar_url"
+ else self._get_displayname()
+ )
+ self.assertEqual(res, "http://my.server/pic.gif")
+
+ channel = self.make_request(
+ "GET",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ # The ?propagate=false should be ignored by the server because the config flag
+ # isn't enabled.
+ self.assertEqual(channel.json_body.get(prop), "http://my.server/pic.gif")
+
+ def test_msc4069_inhibit_propagation_default(self) -> None:
+ """Tests to ensure profile update propagation happens by default."""
+ for prop in ["avatar_url", "displayname"]:
+ room_id = self.helper.create_room_as(tok=self.owner_tok)
+
+ channel = self.make_request(
+ "PUT",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ content={"membership": "join", prop: "mxc://my.server/existing"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ channel = self.make_request(
+ "PUT",
+ f"/profile/{self.owner}/{prop}",
+ content={prop: "http://my.server/pic.gif"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ res = (
+ self._get_avatar_url()
+ if prop == "avatar_url"
+ else self._get_displayname()
+ )
+ self.assertEqual(res, "http://my.server/pic.gif")
+
+ channel = self.make_request(
+ "GET",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ # The ?propagate=false should be ignored by the server because the config flag
+ # isn't enabled.
+ self.assertEqual(channel.json_body.get(prop), "http://my.server/pic.gif")
+
+ @unittest.override_config(
+ {"experimental_features": {"msc4069_profile_inhibit_propagation": True}}
+ )
+ def test_msc4069_inhibit_propagation_like_default(self) -> None:
+ """Tests to ensure clients can request explicit profile propagation."""
+ for prop in ["avatar_url", "displayname"]:
+ room_id = self.helper.create_room_as(tok=self.owner_tok)
+
+ channel = self.make_request(
+ "PUT",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ content={"membership": "join", prop: "mxc://my.server/existing"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ channel = self.make_request(
+ "PUT",
+ f"/profile/{self.owner}/{prop}?org.matrix.msc4069.propagate=true",
+ content={prop: "http://my.server/pic.gif"},
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ res = (
+ self._get_avatar_url()
+ if prop == "avatar_url"
+ else self._get_displayname()
+ )
+ self.assertEqual(res, "http://my.server/pic.gif")
+
+ channel = self.make_request(
+ "GET",
+ f"/rooms/{room_id}/state/m.room.member/{self.owner}",
+ access_token=self.owner_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.result)
+
+ # The client requested ?propagate=true, so it should have happened.
+ self.assertEqual(channel.json_body.get(prop), "http://my.server/pic.gif")
+
def _setup_local_files(self, names_and_props: Dict[str, Dict[str, Any]]) -> None:
"""Stores metadata about files in the database.
diff --git a/tests/rest/client/test_upgrade_room.py b/tests/rest/client/test_upgrade_room.py
index 0b4c691318..28f772261b 100644
--- a/tests/rest/client/test_upgrade_room.py
+++ b/tests/rest/client/test_upgrade_room.py
@@ -246,6 +246,34 @@ class UpgradeRoomTest(unittest.HomeserverTestCase):
# We should now have an integer power level.
self.assertEqual(new_power_levels["users"][self.creator], 100, new_power_levels)
+ def test_events_field_missing(self) -> None:
+ """Regression test for https://github.com/matrix-org/synapse/issues/16715."""
+ # Create a new room.
+ room_id = self.helper.create_room_as(
+ self.creator, tok=self.creator_token, room_version="10"
+ )
+ self.helper.join(room_id, self.other, tok=self.other_token)
+
+ # Retrieve the room's current power levels.
+ power_levels = self.helper.get_state(
+ room_id,
+ "m.room.power_levels",
+ tok=self.creator_token,
+ )
+
+ # Remove the events field and re-set the power levels.
+ del power_levels["events"]
+ self.helper.send_state(
+ room_id,
+ "m.room.power_levels",
+ body=power_levels,
+ tok=self.creator_token,
+ )
+
+ # Upgrade the room. Check the homeserver reports success.
+ channel = self._upgrade_room(room_id=room_id)
+ self.assertEqual(200, channel.code, channel.result)
+
def test_space(self) -> None:
"""Test upgrading a space."""
diff --git a/tests/storage/test_database.py b/tests/storage/test_database.py
index 4d0ebb550d..aa8c76f187 100644
--- a/tests/storage/test_database.py
+++ b/tests/storage/test_database.py
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from typing import Callable, Tuple
+from typing import Callable, List, Tuple
from unittest.mock import Mock, call
from twisted.internet import defer
@@ -29,6 +29,7 @@ from synapse.storage.database import (
from synapse.util import Clock
from tests import unittest
+from tests.utils import USE_POSTGRES_FOR_TESTS
class TupleComparisonClauseTestCase(unittest.TestCase):
@@ -279,3 +280,85 @@ class CancellationTestCase(unittest.HomeserverTestCase):
]
)
self.assertEqual(exception_callback.call_count, 6) # no additional calls
+
+
+class PostgresReplicaIdentityTestCase(unittest.HomeserverTestCase):
+ if not USE_POSTGRES_FOR_TESTS:
+ skip = "Requires Postgres"
+
+ def prepare(
+ self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
+ ) -> None:
+ self.db_pools = homeserver.get_datastores().databases
+
+ def test_all_tables_have_postgres_replica_identity(self) -> None:
+ """
+ Tests that all tables have a Postgres REPLICA IDENTITY.
+ (See https://github.com/matrix-org/synapse/issues/16224).
+
+ Tables with a PRIMARY KEY have an implied REPLICA IDENTITY and are fine.
+ Other tables need them to be set with `ALTER TABLE`.
+
+ A REPLICA IDENTITY is required for Postgres logical replication to work
+ properly without blocking updates and deletes.
+ """
+
+ sql = """
+ -- Select tables that have no primary key and use the default replica identity rule
+ -- (the default is to use the primary key)
+ WITH tables_no_pkey AS (
+ SELECT tbl.table_schema, tbl.table_name
+ FROM information_schema.tables tbl
+ WHERE table_type = 'BASE TABLE'
+ AND table_schema not in ('pg_catalog', 'information_schema')
+ AND NOT EXISTS (
+ SELECT 1
+ FROM information_schema.table_constraints tc
+ WHERE tc.constraint_type = 'PRIMARY KEY'
+ AND tc.table_schema = tbl.table_schema
+ AND tc.table_name = tbl.table_name
+ )
+ )
+ SELECT pg_class.oid::regclass FROM tables_no_pkey INNER JOIN pg_class ON pg_class.oid::regclass = table_name::regclass
+ WHERE relreplident = 'd'
+
+ UNION
+
+ -- Also select tables that use an index as a replica identity
+ -- but where the index doesn't exist
+ -- (e.g. it could have been deleted)
+ SELECT pg_class.oid::regclass
+ FROM information_schema.tables tbl
+ INNER JOIN pg_class ON pg_class.oid::regclass = table_name::regclass
+ WHERE table_type = 'BASE TABLE'
+ AND table_schema not in ('pg_catalog', 'information_schema')
+
+ -- 'i' means an index is used as the replica identity
+ AND relreplident = 'i'
+
+ -- look for indices that are marked as the replica identity
+ AND NOT EXISTS (
+ SELECT indexrelid::regclass
+ FROM pg_index
+ WHERE indrelid = pg_class.oid::regclass AND indisreplident
+ )
+ """
+
+ def _list_tables_with_missing_replica_identities_txn(
+ txn: LoggingTransaction,
+ ) -> List[str]:
+ txn.execute(sql)
+ return [table_name for table_name, in txn]
+
+ for pool in self.db_pools:
+ missing = self.get_success(
+ pool.runInteraction(
+ "test_list_missing_replica_identities",
+ _list_tables_with_missing_replica_identities_txn,
+ )
+ )
+ self.assertEqual(
+ len(missing),
+ 0,
+ f"The following tables in the {pool.name()!r} database are missing REPLICA IDENTITIES: {missing!r}.",
+ )
|