diff --git a/tests/config/test_oauth_delegation.py b/tests/config/test_oauth_delegation.py
index f7aff15a4e..79c10b10a6 100644
--- a/tests/config/test_oauth_delegation.py
+++ b/tests/config/test_oauth_delegation.py
@@ -28,15 +28,7 @@ from synapse.types import JsonDict
from tests.server import get_clock, setup_test_homeserver
from tests.unittest import TestCase, skip_unless
-from tests.utils import default_config
-
-try:
- import authlib # noqa: F401
-
- HAS_AUTHLIB = True
-except ImportError:
- HAS_AUTHLIB = False
-
+from tests.utils import HAS_AUTHLIB, default_config
# These are a few constants that are used as config parameters in the tests.
SERVER_NAME = "test"
diff --git a/tests/handlers/test_oauth_delegation.py b/tests/handlers/test_oauth_delegation.py
index 849f956705..b9761d806d 100644
--- a/tests/handlers/test_oauth_delegation.py
+++ b/tests/handlers/test_oauth_delegation.py
@@ -19,7 +19,8 @@
#
from http import HTTPStatus
-from typing import Any, Dict, Union
+from io import BytesIO
+from typing import Any, Dict, Optional, Union
from unittest.mock import ANY, AsyncMock, Mock
from urllib.parse import parse_qs
@@ -31,6 +32,8 @@ from signedjson.key import (
from signedjson.sign import sign_json
from twisted.test.proto_helpers import MemoryReactor
+from twisted.web.http_headers import Headers
+from twisted.web.iweb import IResponse
from synapse.api.errors import (
AuthError,
@@ -39,23 +42,17 @@ from synapse.api.errors import (
OAuthInsufficientScopeError,
SynapseError,
)
+from synapse.http.site import SynapseRequest
from synapse.rest import admin
from synapse.rest.client import account, devices, keys, login, logout, register
from synapse.server import HomeServer
-from synapse.types import JsonDict
+from synapse.types import JsonDict, UserID
from synapse.util import Clock
+from tests.server import FakeChannel
from tests.test_utils import FakeResponse, get_awaitable_result
-from tests.unittest import HomeserverTestCase, skip_unless
-from tests.utils import mock_getRawHeaders
-
-try:
- import authlib # noqa: F401
-
- HAS_AUTHLIB = True
-except ImportError:
- HAS_AUTHLIB = False
-
+from tests.unittest import HomeserverTestCase, override_config, skip_unless
+from tests.utils import HAS_AUTHLIB, checked_cast, mock_getRawHeaders
# These are a few constants that are used as config parameters in the tests.
SERVER_NAME = "test"
@@ -81,6 +78,7 @@ MATRIX_DEVICE_SCOPE = MATRIX_DEVICE_SCOPE_PREFIX + DEVICE
SUBJECT = "abc-def-ghi"
USERNAME = "test-user"
USER_ID = "@" + USERNAME + ":" + SERVER_NAME
+OIDC_ADMIN_USERID = f"@__oidc_admin:{SERVER_NAME}"
async def get_json(url: str) -> JsonDict:
@@ -140,7 +138,10 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
hs = self.setup_test_homeserver(proxied_http_client=self.http_client)
- self.auth = hs.get_auth()
+ # Import this here so that we've checked that authlib is available.
+ from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth
+
+ self.auth = checked_cast(MSC3861DelegatedAuth, hs.get_auth())
return hs
@@ -681,7 +682,8 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
requester = self.get_success(self.auth.get_user_by_req(request))
self.assertEqual(
- requester.user.to_string(), "@%s:%s" % ("__oidc_admin", SERVER_NAME)
+ requester.user.to_string(),
+ OIDC_ADMIN_USERID,
)
self.assertEqual(requester.is_guest, False)
self.assertEqual(requester.device_id, None)
@@ -691,3 +693,102 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
# There should be no call to the introspection endpoint
self.http_client.request.assert_not_called()
+
+ @override_config({"mau_stats_only": True})
+ def test_request_tracking(self) -> None:
+ """Using an access token should update the client_ips and MAU tables."""
+ # To start, there are no MAU users.
+ store = self.hs.get_datastores().main
+ mau = self.get_success(store.get_monthly_active_count())
+ self.assertEqual(mau, 0)
+
+ known_token = "token-token-GOOD-:)"
+
+ async def mock_http_client_request(
+ method: str,
+ uri: str,
+ data: Optional[bytes] = None,
+ headers: Optional[Headers] = None,
+ ) -> IResponse:
+ """Mocked auth provider response."""
+ assert method == "POST"
+ token = parse_qs(data)[b"token"][0].decode("utf-8")
+ if token == known_token:
+ return FakeResponse.json(
+ code=200,
+ payload={
+ "active": True,
+ "scope": MATRIX_USER_SCOPE,
+ "sub": SUBJECT,
+ "username": USERNAME,
+ },
+ )
+
+ return FakeResponse.json(code=200, payload={"active": False})
+
+ self.http_client.request = mock_http_client_request
+
+ EXAMPLE_IPV4_ADDR = "123.123.123.123"
+ EXAMPLE_USER_AGENT = "httprettygood"
+
+ # First test a known access token
+ channel = FakeChannel(self.site, self.reactor)
+ # type-ignore: FakeChannel is a mock of an HTTPChannel, not a proper HTTPChannel
+ req = SynapseRequest(channel, self.site) # type: ignore[arg-type]
+ req.client.host = EXAMPLE_IPV4_ADDR
+ req.requestHeaders.addRawHeader("Authorization", f"Bearer {known_token}")
+ req.requestHeaders.addRawHeader("User-Agent", EXAMPLE_USER_AGENT)
+ req.content = BytesIO(b"")
+ req.requestReceived(
+ b"GET",
+ b"/_matrix/client/v3/account/whoami",
+ b"1.1",
+ )
+ channel.await_result()
+ self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
+ self.assertEqual(channel.json_body["user_id"], USER_ID, channel.json_body)
+
+ # Expect to see one MAU entry, from the first request
+ mau = self.get_success(store.get_monthly_active_count())
+ self.assertEqual(mau, 1)
+
+ conn_infos = self.get_success(
+ store.get_user_ip_and_agents(UserID.from_string(USER_ID))
+ )
+ self.assertEqual(len(conn_infos), 1, conn_infos)
+ conn_info = conn_infos[0]
+ self.assertEqual(conn_info["access_token"], known_token)
+ self.assertEqual(conn_info["ip"], EXAMPLE_IPV4_ADDR)
+ self.assertEqual(conn_info["user_agent"], EXAMPLE_USER_AGENT)
+
+ # Now test MAS making a request using the special __oidc_admin token
+ MAS_IPV4_ADDR = "127.0.0.1"
+ MAS_USER_AGENT = "masmasmas"
+
+ channel = FakeChannel(self.site, self.reactor)
+ req = SynapseRequest(channel, self.site) # type: ignore[arg-type]
+ req.client.host = MAS_IPV4_ADDR
+ req.requestHeaders.addRawHeader(
+ "Authorization", f"Bearer {self.auth._admin_token}"
+ )
+ req.requestHeaders.addRawHeader("User-Agent", MAS_USER_AGENT)
+ req.content = BytesIO(b"")
+ req.requestReceived(
+ b"GET",
+ b"/_matrix/client/v3/account/whoami",
+ b"1.1",
+ )
+ channel.await_result()
+ self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
+ self.assertEqual(
+ channel.json_body["user_id"], OIDC_ADMIN_USERID, channel.json_body
+ )
+
+ # Still expect to see one MAU entry, from the first request
+ mau = self.get_success(store.get_monthly_active_count())
+ self.assertEqual(mau, 1)
+
+ conn_infos = self.get_success(
+ store.get_user_ip_and_agents(UserID.from_string(OIDC_ADMIN_USERID))
+ )
+ self.assertEqual(conn_infos, [])
diff --git a/tests/rest/media/test_media_retention.py b/tests/media/test_media_retention.py
index 7f613f351b..7f613f351b 100644
--- a/tests/rest/media/test_media_retention.py
+++ b/tests/media/test_media_retention.py
diff --git a/tests/media/test_media_storage.py b/tests/media/test_media_storage.py
index c1177291ee..a42383bcb6 100644
--- a/tests/media/test_media_storage.py
+++ b/tests/media/test_media_storage.py
@@ -33,10 +33,11 @@ from typing_extensions import Literal
from twisted.internet import defer
from twisted.internet.defer import Deferred
+from twisted.python.failure import Failure
from twisted.test.proto_helpers import MemoryReactor
from twisted.web.resource import Resource
-from synapse.api.errors import Codes
+from synapse.api.errors import Codes, HttpResponseException
from synapse.events import EventBase
from synapse.http.types import QueryParams
from synapse.logging.context import make_deferred_yieldable
@@ -253,6 +254,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
retry_on_dns_fail: bool = True,
max_size: Optional[int] = None,
ignore_backoff: bool = False,
+ follow_redirects: bool = False,
) -> "Deferred[Tuple[int, Dict[bytes, List[bytes]]]]":
"""A mock for MatrixFederationHttpClient.get_file."""
@@ -263,10 +265,15 @@ class MediaRepoTests(unittest.HomeserverTestCase):
output_stream.write(data)
return response
+ def write_err(f: Failure) -> Failure:
+ f.trap(HttpResponseException)
+ output_stream.write(f.value.response)
+ return f
+
d: Deferred[Tuple[bytes, Tuple[int, Dict[bytes, List[bytes]]]]] = Deferred()
self.fetches.append((d, destination, path, args))
# Note that this callback changes the value held by d.
- d_after_callback = d.addCallback(write_to)
+ d_after_callback = d.addCallbacks(write_to, write_err)
return make_deferred_yieldable(d_after_callback)
# Mock out the homeserver's MatrixFederationHttpClient
@@ -322,10 +329,11 @@ class MediaRepoTests(unittest.HomeserverTestCase):
self.assertEqual(len(self.fetches), 1)
self.assertEqual(self.fetches[0][1], "example.com")
self.assertEqual(
- self.fetches[0][2], "/_matrix/media/r0/download/" + self.media_id
+ self.fetches[0][2], "/_matrix/media/v3/download/" + self.media_id
)
self.assertEqual(
- self.fetches[0][3], {"allow_remote": "false", "timeout_ms": "20000"}
+ self.fetches[0][3],
+ {"allow_remote": "false", "timeout_ms": "20000", "allow_redirect": "true"},
)
headers = {
@@ -677,6 +685,52 @@ class MediaRepoTests(unittest.HomeserverTestCase):
[b"cross-origin"],
)
+ def test_unknown_v3_endpoint(self) -> None:
+ """
+ If the v3 endpoint fails, try the r0 one.
+ """
+ channel = self.make_request(
+ "GET",
+ f"/_matrix/media/v3/download/{self.media_id}",
+ shorthand=False,
+ await_result=False,
+ )
+ self.pump()
+
+ # We've made one fetch, to example.com, using the media URL, and asking
+ # the other server not to do a remote fetch
+ self.assertEqual(len(self.fetches), 1)
+ self.assertEqual(self.fetches[0][1], "example.com")
+ self.assertEqual(
+ self.fetches[0][2], "/_matrix/media/v3/download/" + self.media_id
+ )
+
+ # The result which says the endpoint is unknown.
+ unknown_endpoint = b'{"errcode":"M_UNRECOGNIZED","error":"Unknown request"}'
+ self.fetches[0][0].errback(
+ HttpResponseException(404, "NOT FOUND", unknown_endpoint)
+ )
+
+ self.pump()
+
+ # There should now be another request to the r0 URL.
+ self.assertEqual(len(self.fetches), 2)
+ self.assertEqual(self.fetches[1][1], "example.com")
+ self.assertEqual(
+ self.fetches[1][2], f"/_matrix/media/r0/download/{self.media_id}"
+ )
+
+ headers = {
+ b"Content-Length": [b"%d" % (len(self.test_image.data))],
+ }
+
+ self.fetches[1][0].callback(
+ (self.test_image.data, (len(self.test_image.data), headers))
+ )
+
+ self.pump()
+ self.assertEqual(channel.code, 200)
+
class TestSpamCheckerLegacy:
"""A spam checker module that rejects all media that includes the bytes
diff --git a/tests/replication/test_multi_media_repo.py b/tests/replication/test_multi_media_repo.py
index e01773fa63..70c10d126d 100644
--- a/tests/replication/test_multi_media_repo.py
+++ b/tests/replication/test_multi_media_repo.py
@@ -139,7 +139,7 @@ class MediaRepoShardTestCase(BaseMultiWorkerStreamTestCase):
self.assertEqual(request.method, b"GET")
self.assertEqual(
request.path,
- f"/_matrix/media/r0/download/{target}/{media_id}".encode(),
+ f"/_matrix/media/v3/download/{target}/{media_id}".encode(),
)
self.assertEqual(
request.requestHeaders.getRawHeaders(b"host"), [target.encode("utf-8")]
diff --git a/tests/rest/admin/test_jwks.py b/tests/rest/admin/test_jwks.py
index 6c2b355aa8..3636ea3415 100644
--- a/tests/rest/admin/test_jwks.py
+++ b/tests/rest/admin/test_jwks.py
@@ -25,13 +25,7 @@ from twisted.web.resource import Resource
from synapse.rest.synapse.client import build_synapse_client_resource_tree
from tests.unittest import HomeserverTestCase, override_config, skip_unless
-
-try:
- import authlib # noqa: F401
-
- HAS_AUTHLIB = True
-except ImportError:
- HAS_AUTHLIB = False
+from tests.utils import HAS_AUTHLIB
@skip_unless(HAS_AUTHLIB, "requires authlib")
diff --git a/tests/rest/admin/test_server_notice.py b/tests/rest/admin/test_server_notice.py
index ceba09ec46..ce5e3a5c1f 100644
--- a/tests/rest/admin/test_server_notice.py
+++ b/tests/rest/admin/test_server_notice.py
@@ -483,6 +483,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:
"""
@@ -575,6 +602,115 @@ class ServerNoticeTestCase(unittest.HomeserverTestCase):
)
self.assertEqual(notice_user_state["avatar_url"], new_avatar_url)
+ @override_config(
+ {
+ "server_notices": {
+ "system_mxid_localpart": "notices",
+ "room_avatar_url": "test/url",
+ "room_topic": "Test Topic",
+ }
+ }
+ )
+ def test_notice_room_avatar_and_topic(self) -> None:
+ """
+ Tests that using `room_avatar_url` and `room_topic` config properly sets
+ those properties for the created notice rooms.
+ """
+ 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,
+ )
+
+ invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
+ notice_room_id = invited_rooms[0].room_id
+ self.helper.join(
+ room=notice_room_id, user=self.other_user, tok=self.other_user_token
+ )
+
+ room_avatar_state = self.helper.get_state(
+ notice_room_id,
+ "m.room.avatar",
+ self.other_user_token,
+ state_key="",
+ )
+ self.assertEqual(room_avatar_state["url"], "test/url")
+
+ room_topic_state = self.helper.get_state(
+ notice_room_id,
+ "m.room.topic",
+ self.other_user_token,
+ state_key="",
+ )
+ self.assertEqual(room_topic_state["topic"], "Test Topic")
+
+ @override_config(
+ {
+ "server_notices": {
+ "system_mxid_localpart": "notices",
+ "room_avatar_url": "test/url",
+ }
+ }
+ )
+ def test_update_room_avatar_when_changed(self) -> None:
+ """
+ Tests that existing server notices room avatar is updated when it is
+ different from the one in homeserver config.
+ """
+ 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,
+ )
+
+ invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
+ notice_room_id = invited_rooms[0].room_id
+ self.helper.join(
+ room=notice_room_id, user=self.other_user, tok=self.other_user_token
+ )
+
+ room_avatar_state = self.helper.get_state(
+ notice_room_id,
+ "m.room.avatar",
+ self.other_user_token,
+ state_key="",
+ )
+ self.assertEqual(room_avatar_state["url"], "test/url")
+
+ # simulate a change in server config after a server restart.
+ new_avatar_url = "test/new-url"
+ self.server_notices_manager._config.servernotices.server_notices_room_avatar_url = (
+ new_avatar_url
+ )
+ self.server_notices_manager.get_or_create_notice_room_for_user.cache.invalidate_all()
+
+ self.make_request(
+ "POST",
+ self.url,
+ access_token=self.admin_user_tok,
+ content=server_notice_request_content,
+ )
+
+ room_avatar_state = self.helper.get_state(
+ notice_room_id,
+ "m.room.avatar",
+ self.other_user_token,
+ state_key="",
+ )
+ self.assertEqual(room_avatar_state["url"], new_avatar_url)
+
def _check_invite_and_join_status(
self, user_id: str, expected_invites: int, expected_memberships: int
) -> Sequence[RoomsForUser]:
diff --git a/tests/rest/client/test_auth_issuer.py b/tests/rest/client/test_auth_issuer.py
new file mode 100644
index 0000000000..964baeec32
--- /dev/null
+++ b/tests/rest/client/test_auth_issuer.py
@@ -0,0 +1,59 @@
+# Copyright 2023 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from http import HTTPStatus
+
+from synapse.rest.client import auth_issuer
+
+from tests.unittest import HomeserverTestCase, override_config, skip_unless
+from tests.utils import HAS_AUTHLIB
+
+ISSUER = "https://account.example.com/"
+
+
+class AuthIssuerTestCase(HomeserverTestCase):
+ servlets = [
+ auth_issuer.register_servlets,
+ ]
+
+ def test_returns_404_when_msc3861_disabled(self) -> None:
+ # Make an unauthenticated request for the discovery info.
+ channel = self.make_request(
+ "GET",
+ "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer",
+ )
+ self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
+
+ @skip_unless(HAS_AUTHLIB, "requires authlib")
+ @override_config(
+ {
+ "disable_registration": True,
+ "experimental_features": {
+ "msc3861": {
+ "enabled": True,
+ "issuer": ISSUER,
+ "client_id": "David Lister",
+ "client_auth_method": "client_secret_post",
+ "client_secret": "Who shot Mister Burns?",
+ }
+ },
+ }
+ )
+ def test_returns_issuer_when_oidc_enabled(self) -> None:
+ # Make an unauthenticated request for the discovery info.
+ channel = self.make_request(
+ "GET",
+ "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer",
+ )
+ self.assertEqual(channel.code, HTTPStatus.OK)
+ self.assertEqual(channel.json_body, {"issuer": ISSUER})
diff --git a/tests/rest/client/test_keys.py b/tests/rest/client/test_keys.py
index fb0f451a5c..e99160c5ac 100644
--- a/tests/rest/client/test_keys.py
+++ b/tests/rest/client/test_keys.py
@@ -36,13 +36,7 @@ from synapse.types import JsonDict, Requester, create_requester
from tests import unittest
from tests.http.server._base import make_request_with_cancellation_test
from tests.unittest import override_config
-
-try:
- import authlib # noqa: F401
-
- HAS_AUTHLIB = True
-except ImportError:
- HAS_AUTHLIB = False
+from tests.utils import HAS_AUTHLIB
class KeyQueryTestCase(unittest.HomeserverTestCase):
diff --git a/tests/rest/client/test_profile.py b/tests/rest/client/test_profile.py
index 13e36731a5..b9852928c0 100644
--- a/tests/rest/client/test_profile.py
+++ b/tests/rest/client/test_profile.py
@@ -318,6 +318,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 a497f41fb2..7038e42058 100644
--- a/tests/rest/client/test_upgrade_room.py
+++ b/tests/rest/client/test_upgrade_room.py
@@ -252,6 +252,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/rest/test_well_known.py b/tests/rest/test_well_known.py
index 682aa63e5b..e166c13bc1 100644
--- a/tests/rest/test_well_known.py
+++ b/tests/rest/test_well_known.py
@@ -22,13 +22,7 @@ from twisted.web.resource import Resource
from synapse.rest.well_known import well_known_resource
from tests import unittest
-
-try:
- import authlib # noqa: F401
-
- HAS_AUTHLIB = True
-except ImportError:
- HAS_AUTHLIB = False
+from tests.utils import HAS_AUTHLIB
class WellKnownTests(unittest.HomeserverTestCase):
diff --git a/tests/utils.py b/tests/utils.py
index ba902dc077..5798b04eef 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -35,6 +35,13 @@ from synapse.storage.database import LoggingDatabaseConnection
from synapse.storage.engines import create_engine
from synapse.storage.prepare_database import prepare_database
+try:
+ import authlib # noqa: F401
+
+ HAS_AUTHLIB = True
+except ImportError:
+ HAS_AUTHLIB = False
+
# set this to True to run the tests against postgres instead of sqlite.
#
# When running under postgres, we first create a base database with the name
|