mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-11 07:58:11 +00:00
As per https://github.com/element-hq/synapse/issues/20419 this change broke Element Web clearing user status, which basically means the client is stuck with any status, and users cannot set a new one. EW should move to use DELETE to clear user status, but until then we should ensure the feature is not broken on existing clients in the wild. Should we set a clear deprecation time for rejecting nulls? There might be other clients using profile fields and doing PUT to clear the field. Discussed in [`#element-backend-internal:matrix.org`](https://matrix.to/#/!SGNQGPGUwtcPBUotTL:matrix.org/$b7zid3lLGdFDC8pwy1IvSPv3zl9y1fXEpnnBte9vcQU?via=jki.re&via=element.io&via=matrix.org) [[1]](https://matrix.to/#/!SGNQGPGUwtcPBUotTL:matrix.org/$jTEcHlDBIvKRAKtV-ESXabkPkLia9B7C4JQVkoOxzj0?via=jki.re&via=element.io&via=matrix.org) Reverts a part of https://github.com/element-hq/synapse/pull/20153 by only rejecting null fields when the profile update stream is configured (ie user status requirement). Future work to unrevert tracked by https://github.com/element-hq/synapse/issues/20424
1151 lines
44 KiB
Python
1151 lines
44 KiB
Python
#
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright 2014-2016 OpenMarket Ltd
|
|
# Copyright (C) 2023 New Vector, 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>.
|
|
#
|
|
# Originally licensed under the Apache License, Version 2.0:
|
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# [This file includes modifications made by New Vector Limited]
|
|
#
|
|
#
|
|
|
|
"""Tests REST events for /profile paths."""
|
|
|
|
import logging
|
|
import urllib.parse
|
|
from http import HTTPStatus
|
|
from typing import Any
|
|
|
|
from canonicaljson import encode_canonical_json
|
|
from parameterized import parameterized
|
|
|
|
from twisted.internet.testing import MemoryReactor
|
|
|
|
from synapse.api.errors import Codes
|
|
from synapse.rest import admin
|
|
from synapse.rest.client import login, profile, room
|
|
from synapse.server import HomeServer
|
|
from synapse.storage.databases.main.profile import MAX_PROFILE_SIZE
|
|
from synapse.types import UserID
|
|
from synapse.util.clock import Clock
|
|
|
|
from tests import unittest
|
|
from tests.unittest import override_config
|
|
from tests.utils import USE_POSTGRES_FOR_TESTS
|
|
|
|
|
|
class ProfileTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
admin.register_servlets_for_client_rest_resource,
|
|
login.register_servlets,
|
|
profile.register_servlets,
|
|
room.register_servlets,
|
|
]
|
|
|
|
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
|
self.hs = self.setup_test_homeserver()
|
|
return self.hs
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.owner = self.register_user("owner", "pass")
|
|
self.owner_tok = self.login("owner", "pass")
|
|
self.other = self.register_user("other", "pass", displayname="Bob")
|
|
|
|
def test_get_displayname(self) -> None:
|
|
res = self._get_displayname()
|
|
self.assertEqual(res, "owner")
|
|
|
|
def test_get_displayname_rejects_bad_username(self) -> None:
|
|
channel = self.make_request(
|
|
"GET", f"/profile/{urllib.parse.quote('@alice:')}/displayname"
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.result)
|
|
|
|
def test_set_displayname(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/displayname" % (self.owner,),
|
|
content={"displayname": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
res = self._get_displayname()
|
|
self.assertEqual(res, "test")
|
|
|
|
def test_set_displayname_with_extra_spaces(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/displayname" % (self.owner,),
|
|
content={"displayname": " test "},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
res = self._get_displayname()
|
|
self.assertEqual(res, "test")
|
|
|
|
def test_set_displayname_noauth(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/displayname" % (self.owner,),
|
|
content={"displayname": "test"},
|
|
)
|
|
self.assertEqual(channel.code, 401, channel.result)
|
|
|
|
def test_set_displayname_too_long(self) -> None:
|
|
"""Attempts to set a stupid displayname should get a 400"""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/displayname" % (self.owner,),
|
|
content={"displayname": "test" * 100},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
|
|
res = self._get_displayname()
|
|
self.assertEqual(res, "owner")
|
|
|
|
def test_get_displayname_other(self) -> None:
|
|
res = self._get_displayname(self.other)
|
|
self.assertEqual(res, "Bob")
|
|
|
|
def test_set_displayname_other(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/displayname" % (self.other,),
|
|
content={"displayname": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
|
|
@unittest.override_config({"enable_set_displayname": False})
|
|
def test_set_displayname_disabled(self) -> None:
|
|
"""Changing an existing displayname while `enable_set_displayname` is off
|
|
should get a 403 with M_FORBIDDEN."""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/displayname" % (self.owner,),
|
|
content={"displayname": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.FORBIDDEN)
|
|
|
|
res = self._get_displayname()
|
|
self.assertEqual(res, "owner")
|
|
|
|
@unittest.override_config({"enable_set_displayname": False})
|
|
def test_delete_displayname_disabled(self) -> None:
|
|
"""Deleting an existing displayname while `enable_set_displayname` is off
|
|
should get a 403 with M_FORBIDDEN."""
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
"/profile/%s/displayname" % (self.owner,),
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.FORBIDDEN)
|
|
|
|
res = self._get_displayname()
|
|
self.assertEqual(res, "owner")
|
|
|
|
def test_get_avatar_url(self) -> None:
|
|
res = self._get_avatar_url()
|
|
self.assertIsNone(res)
|
|
|
|
def test_set_avatar_url(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/avatar_url" % (self.owner,),
|
|
content={"avatar_url": "http://my.server/pic.gif"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
res = self._get_avatar_url()
|
|
self.assertEqual(res, "http://my.server/pic.gif")
|
|
|
|
def test_set_avatar_url_noauth(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/avatar_url" % (self.owner,),
|
|
content={"avatar_url": "http://my.server/pic.gif"},
|
|
)
|
|
self.assertEqual(channel.code, 401, channel.result)
|
|
|
|
def test_set_avatar_url_too_long(self) -> None:
|
|
"""Attempts to set a stupid avatar_url should get a 400"""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/avatar_url" % (self.owner,),
|
|
content={"avatar_url": "http://my.server/pic.gif" * 100},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
|
|
res = self._get_avatar_url()
|
|
self.assertIsNone(res)
|
|
|
|
def test_get_avatar_url_other(self) -> None:
|
|
res = self._get_avatar_url(self.other)
|
|
self.assertIsNone(res)
|
|
|
|
def test_set_avatar_url_other(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/avatar_url" % (self.other,),
|
|
content={"avatar_url": "http://my.server/pic.gif"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
|
|
@unittest.override_config({"enable_set_avatar_url": False})
|
|
def test_set_avatar_url_disabled(self) -> None:
|
|
"""Changing an existing avatar while `enable_set_avatar_url` is off should
|
|
get a 403 with M_FORBIDDEN. Setting it for the first time is allowed."""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/avatar_url" % (self.owner,),
|
|
content={"avatar_url": "http://my.server/pic.gif"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/avatar_url" % (self.owner,),
|
|
content={"avatar_url": "http://my.server/me.png"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.FORBIDDEN)
|
|
|
|
res = self._get_avatar_url()
|
|
self.assertEqual(res, "http://my.server/pic.gif")
|
|
|
|
@unittest.override_config({"enable_set_avatar_url": False})
|
|
def test_delete_avatar_url_disabled(self) -> None:
|
|
"""Deleting an existing avatar while `enable_set_avatar_url` is off should
|
|
get a 403 with M_FORBIDDEN."""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/profile/%s/avatar_url" % (self.owner,),
|
|
content={"avatar_url": "http://my.server/pic.gif"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
"/profile/%s/avatar_url" % (self.owner,),
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.FORBIDDEN)
|
|
|
|
res = self._get_avatar_url()
|
|
self.assertEqual(res, "http://my.server/pic.gif")
|
|
|
|
def _get_displayname(self, name: str | None = None) -> str | None:
|
|
channel = self.make_request(
|
|
"GET", "/profile/%s/displayname" % (name or self.owner,)
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
# FIXME: If a user has no displayname set, Synapse returns 200 and omits a
|
|
# displayname from the response. This contradicts the spec, see
|
|
# https://github.com/matrix-org/synapse/issues/13137.
|
|
return channel.json_body.get("displayname")
|
|
|
|
def _get_avatar_url(self, name: str | None = None) -> str | None:
|
|
channel = self.make_request(
|
|
"GET", "/profile/%s/avatar_url" % (name or self.owner,)
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
# FIXME: If a user has no avatar set, Synapse returns 200 and omits an
|
|
# avatar_url from the response. This contradicts the spec, see
|
|
# https://github.com/matrix-org/synapse/issues/13137.
|
|
return channel.json_body.get("avatar_url")
|
|
|
|
@unittest.override_config({"max_avatar_size": 50})
|
|
def test_avatar_size_limit_global(self) -> None:
|
|
"""Tests that the maximum size limit for avatars is enforced when updating a
|
|
global profile.
|
|
"""
|
|
self._setup_local_files(
|
|
{
|
|
"small": {"size": 40},
|
|
"big": {"size": 60},
|
|
}
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/profile/{self.owner}/avatar_url",
|
|
content={"avatar_url": "mxc://test/big"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(
|
|
channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/profile/{self.owner}/avatar_url",
|
|
content={"avatar_url": "mxc://test/small"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
@unittest.override_config({"max_avatar_size": 50})
|
|
def test_avatar_size_limit_per_room(self) -> None:
|
|
"""Tests that the maximum size limit for avatars is enforced when updating a
|
|
per-room profile.
|
|
"""
|
|
self._setup_local_files(
|
|
{
|
|
"small": {"size": 40},
|
|
"big": {"size": 60},
|
|
}
|
|
)
|
|
|
|
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", "avatar_url": "mxc://test/big"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(
|
|
channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/rooms/{room_id}/state/m.room.member/{self.owner}",
|
|
content={"membership": "join", "avatar_url": "mxc://test/small"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
@unittest.override_config({"allowed_avatar_mimetypes": ["image/png"]})
|
|
def test_avatar_allowed_mime_type_global(self) -> None:
|
|
"""Tests that the MIME type whitelist for avatars is enforced when updating a
|
|
global profile.
|
|
"""
|
|
self._setup_local_files(
|
|
{
|
|
"good": {"mimetype": "image/png"},
|
|
"bad": {"mimetype": "application/octet-stream"},
|
|
}
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/profile/{self.owner}/avatar_url",
|
|
content={"avatar_url": "mxc://test/bad"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(
|
|
channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/profile/{self.owner}/avatar_url",
|
|
content={"avatar_url": "mxc://test/good"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
@unittest.override_config({"allowed_avatar_mimetypes": ["image/png"]})
|
|
def test_avatar_allowed_mime_type_per_room(self) -> None:
|
|
"""Tests that the MIME type whitelist for avatars is enforced when updating a
|
|
per-room profile.
|
|
"""
|
|
self._setup_local_files(
|
|
{
|
|
"good": {"mimetype": "image/png"},
|
|
"bad": {"mimetype": "application/octet-stream"},
|
|
}
|
|
)
|
|
|
|
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", "avatar_url": "mxc://test/bad"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(
|
|
channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/rooms/{room_id}/state/m.room.member/{self.owner}",
|
|
content={"membership": "join", "avatar_url": "mxc://test/good"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
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 test_get_unset_field_omits_key(self) -> None:
|
|
"""
|
|
Fetching a standard profile field which the user has not set should
|
|
return a 200 with an empty body, rather than the key with a null value.
|
|
"""
|
|
for field in ("displayname", "avatar_url"):
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{field}",
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
|
|
|
for path in (
|
|
f"/profile/{self.owner}/{field}",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{field}",
|
|
):
|
|
channel = self.make_request("GET", path)
|
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
|
self.assertEqual(channel.json_body, {}, path)
|
|
|
|
def test_get_missing_custom_field(self) -> None:
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND)
|
|
|
|
def test_get_missing_custom_field_invalid_field_name(self) -> None:
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}/[custom_field]",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.INVALID_PARAM)
|
|
|
|
def test_get_custom_field_rejects_bad_username(self) -> None:
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{urllib.parse.quote('@alice:')}/custom_field",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.INVALID_PARAM)
|
|
|
|
def test_set_custom_field(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
content={"custom_field": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
|
self.assertEqual(channel.json_body, {"custom_field": "test"})
|
|
|
|
# Overwriting the field should work.
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
content={"custom_field": "new_Value"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
|
self.assertEqual(channel.json_body, {"custom_field": "new_Value"})
|
|
|
|
# Deleting the field should work.
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
content={},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND)
|
|
|
|
def test_non_string(self) -> None:
|
|
"""Non-string fields are supported for custom fields."""
|
|
fields = {
|
|
"bool_field": True,
|
|
"array_field": ["test"],
|
|
"object_field": {"test": "test"},
|
|
"numeric_field": 1,
|
|
}
|
|
|
|
for key, value in fields.items():
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
content={key: value},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
|
self.assertEqual(channel.json_body, {"displayname": "owner", **fields})
|
|
|
|
# Check getting individual fields works.
|
|
for key, value in fields.items():
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
|
self.assertEqual(channel.json_body, {key: value})
|
|
|
|
@parameterized.expand(
|
|
[
|
|
# These two fields are special as they are the original 2 fields
|
|
# and have their own storage implementation.
|
|
# They already rejected `null` before the introduction of this test.
|
|
("displayname",),
|
|
("avatar_url",),
|
|
# This is a 'custom field'.
|
|
("org.example.custom_field",),
|
|
]
|
|
)
|
|
def test_null_field_rejected(self, field_name: str) -> None:
|
|
"""
|
|
Tests that setting a field to `null` is rejected.
|
|
|
|
This is optional as per spec, but we prefer to apply this as it is best for interoperability
|
|
(prevents clients from relying on `null` values being accepted, which is essentially
|
|
Synapse-specific behaviour).
|
|
|
|
> Servers MAY reject null values.
|
|
— https://spec.matrix.org/v1.19/client-server-api/#put_matrixclientv3profileuseridkeyname
|
|
"""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{field_name}",
|
|
content={field_name: None},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.INVALID_PARAM)
|
|
|
|
@override_config({"include_profile_updates_in_sync": True})
|
|
def test_null_field_is_not_rejected_if_profile_update_stream_enabled(self) -> None:
|
|
"""
|
|
Tests that setting a field to `null` is not rejected if profile update
|
|
stream is configured active.
|
|
|
|
See https://github.com/element-hq/synapse/issues/20424 for removing
|
|
this special case.
|
|
"""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/org.example.custom_field",
|
|
content={"org.example.custom_field": None},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
|
self.assertFalse("errcode" in channel.json_body.keys())
|
|
|
|
def test_set_custom_field_noauth(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
content={"custom_field": "test"},
|
|
)
|
|
self.assertEqual(channel.code, 401, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.MISSING_TOKEN)
|
|
|
|
def test_set_custom_field_size(self) -> None:
|
|
"""
|
|
Attempts to set a custom field name that is too long should get a 400 error.
|
|
"""
|
|
# Key is missing.
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/",
|
|
content={"": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.INVALID_PARAM)
|
|
|
|
# Single key is too large.
|
|
key = "c" * 500
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
content={key: "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.KEY_TOO_LARGE)
|
|
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
content={key: "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.KEY_TOO_LARGE)
|
|
|
|
# Key doesn't match body.
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/custom_field",
|
|
content={"diff_key": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.MISSING_PARAM)
|
|
|
|
def test_set_custom_field_profile_too_long(self) -> None:
|
|
"""
|
|
Attempts to set a custom field that would push the overall profile too large.
|
|
"""
|
|
# FIXME: Because we emit huge SQL log lines and trial can't handle these,
|
|
# sometimes (flakily) failing the test run,
|
|
# disable SQL logging for this test.
|
|
# ref: https://github.com/twisted/twisted/issues/12482
|
|
# To remove this, we would need to fix the above issue and
|
|
# update, including in olddeps (so several years' wait).
|
|
sql_logger = logging.getLogger("synapse.storage.SQL")
|
|
sql_logger_was_disabled = sql_logger.disabled
|
|
sql_logger.disabled = True
|
|
try:
|
|
# Get right to the boundary:
|
|
# len("displayname") + len("owner") + 5 = 21 for the displayname
|
|
# 1 + 65498 + 5 for key "a" = 65504
|
|
# 2 braces, 1 comma
|
|
# 3 + 21 + 65498 = 65522 < 65536.
|
|
key = "a"
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
content={key: "a" * 65498},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
# Get the entire profile.
|
|
channel = self.make_request(
|
|
"GET",
|
|
f"/_matrix/client/v3/profile/{self.owner}",
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
canonical_json = encode_canonical_json(channel.json_body)
|
|
# 6 is the minimum bytes to store a value: 4 quotes, 1 colon, 1 comma, an empty key.
|
|
# Be one below that so we can prove we're at the boundary.
|
|
self.assertEqual(len(canonical_json), MAX_PROFILE_SIZE - 8)
|
|
|
|
# Postgres stores JSONB with whitespace, while SQLite doesn't.
|
|
if USE_POSTGRES_FOR_TESTS:
|
|
ADDITIONAL_CHARS = 0
|
|
else:
|
|
ADDITIONAL_CHARS = 1
|
|
|
|
# The next one should fail, note the value has a (JSON) length of 2.
|
|
key = "b"
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
content={key: "1" + "a" * ADDITIONAL_CHARS},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.PROFILE_TOO_LARGE)
|
|
|
|
# Setting an avatar or (longer) display name should not work.
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/profile/{self.owner}/displayname",
|
|
content={"displayname": "owner12345678" + "a" * ADDITIONAL_CHARS},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.PROFILE_TOO_LARGE)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/profile/{self.owner}/avatar_url",
|
|
content={"avatar_url": "mxc://foo/bar"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 400, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.PROFILE_TOO_LARGE)
|
|
|
|
# Removing a single byte should work.
|
|
key = "b"
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
content={key: "" + "a" * ADDITIONAL_CHARS},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
# Finally, setting a field that already exists to a value that is <= in length should work.
|
|
key = "a"
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/{key}",
|
|
content={key: ""},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
finally:
|
|
sql_logger.disabled = sql_logger_was_disabled
|
|
|
|
def test_set_custom_field_displayname(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/displayname",
|
|
content={"displayname": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
displayname = self._get_displayname()
|
|
self.assertEqual(displayname, "test")
|
|
|
|
def test_set_custom_field_avatar_url(self) -> None:
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.owner}/avatar_url",
|
|
content={"avatar_url": "mxc://test/good"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
avatar_url = self._get_avatar_url()
|
|
self.assertEqual(avatar_url, "mxc://test/good")
|
|
|
|
def test_set_custom_field_never_existed_user(self) -> None:
|
|
"""Setting a profile field for a user that does not exist should not
|
|
conjure up a profile row for them, even for a server admin."""
|
|
self.register_user("admin", "pass", admin=True)
|
|
admin_tok = self.login("admin", "pass")
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
"/_matrix/client/v3/profile/@never-existed:test/custom_field",
|
|
content={"custom_field": "test"},
|
|
access_token=admin_tok,
|
|
)
|
|
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND)
|
|
|
|
def test_set_custom_field_other(self) -> None:
|
|
"""Setting someone else's profile field should fail"""
|
|
channel = self.make_request(
|
|
"PUT",
|
|
f"/_matrix/client/v3/profile/{self.other}/custom_field",
|
|
content={"custom_field": "test"},
|
|
access_token=self.owner_tok,
|
|
)
|
|
self.assertEqual(channel.code, 403, channel.result)
|
|
self.assertEqual(channel.json_body["errcode"], Codes.FORBIDDEN)
|
|
|
|
def _setup_local_files(self, names_and_props: dict[str, dict[str, Any]]) -> None:
|
|
"""Stores metadata about files in the database.
|
|
|
|
Args:
|
|
names_and_props: A dictionary with one entry per file, with the key being the
|
|
file's name, and the value being a dictionary of properties. Supported
|
|
properties are "mimetype" (for the file's type) and "size" (for the
|
|
file's size).
|
|
"""
|
|
store = self.hs.get_datastores().main
|
|
|
|
for name, props in names_and_props.items():
|
|
self.get_success(
|
|
store.store_local_media(
|
|
media_id=name,
|
|
media_type=props.get("mimetype", "image/png"),
|
|
time_now_ms=self.clock.time_msec(),
|
|
upload_name=None,
|
|
media_length=props.get("size", 50),
|
|
user_id=UserID.from_string("@rin:test"),
|
|
)
|
|
)
|
|
|
|
|
|
class ProfilesRestrictedTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
admin.register_servlets_for_client_rest_resource,
|
|
login.register_servlets,
|
|
profile.register_servlets,
|
|
room.register_servlets,
|
|
]
|
|
|
|
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
|
config = self.default_config()
|
|
config["require_auth_for_profile_requests"] = True
|
|
config["limit_profile_requests_to_users_who_share_rooms"] = True
|
|
self.hs = self.setup_test_homeserver(config=config)
|
|
|
|
return self.hs
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
# User owning the requested profile.
|
|
self.owner = self.register_user("owner", "pass")
|
|
self.owner_tok = self.login("owner", "pass")
|
|
self.profile_url = "/profile/%s" % (self.owner)
|
|
|
|
# User requesting the profile.
|
|
self.requester = self.register_user("requester", "pass")
|
|
self.requester_tok = self.login("requester", "pass")
|
|
|
|
self.room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok)
|
|
|
|
def test_no_auth(self) -> None:
|
|
self.try_fetch_profile(401)
|
|
|
|
def test_not_in_shared_room(self) -> None:
|
|
self.ensure_requester_left_room()
|
|
|
|
self.try_fetch_profile(403, access_token=self.requester_tok)
|
|
|
|
def test_in_shared_room(self) -> None:
|
|
self.ensure_requester_left_room()
|
|
|
|
self.helper.join(room=self.room_id, user=self.requester, tok=self.requester_tok)
|
|
|
|
self.try_fetch_profile(200, self.requester_tok)
|
|
|
|
def try_fetch_profile(
|
|
self, expected_code: int, access_token: str | None = None
|
|
) -> None:
|
|
self.request_profile(expected_code, access_token=access_token)
|
|
|
|
self.request_profile(
|
|
expected_code, url_suffix="/displayname", access_token=access_token
|
|
)
|
|
|
|
self.request_profile(
|
|
expected_code, url_suffix="/avatar_url", access_token=access_token
|
|
)
|
|
|
|
def request_profile(
|
|
self,
|
|
expected_code: int,
|
|
url_suffix: str = "",
|
|
access_token: str | None = None,
|
|
) -> None:
|
|
channel = self.make_request(
|
|
"GET", self.profile_url + url_suffix, access_token=access_token
|
|
)
|
|
self.assertEqual(channel.code, expected_code, channel.result)
|
|
|
|
def ensure_requester_left_room(self) -> None:
|
|
try:
|
|
self.helper.leave(
|
|
room=self.room_id, user=self.requester, tok=self.requester_tok
|
|
)
|
|
except AssertionError:
|
|
# We don't care whether the leave request didn't return a 200 (e.g.
|
|
# if the user isn't already in the room), because we only want to
|
|
# make sure the user isn't in the room.
|
|
pass
|
|
|
|
|
|
class OwnProfileUnrestrictedTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
admin.register_servlets_for_client_rest_resource,
|
|
login.register_servlets,
|
|
profile.register_servlets,
|
|
]
|
|
|
|
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
|
config = self.default_config()
|
|
config["require_auth_for_profile_requests"] = True
|
|
config["limit_profile_requests_to_users_who_share_rooms"] = True
|
|
self.hs = self.setup_test_homeserver(config=config)
|
|
|
|
return self.hs
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
# User requesting the profile.
|
|
self.requester = self.register_user("requester", "pass")
|
|
self.requester_tok = self.login("requester", "pass")
|
|
|
|
def test_can_lookup_own_profile(self) -> None:
|
|
"""Tests that a user can lookup their own profile without having to be in a room
|
|
if 'require_auth_for_profile_requests' is set to true in the server's config.
|
|
"""
|
|
channel = self.make_request(
|
|
"GET", "/profile/" + self.requester, access_token=self.requester_tok
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/profile/" + self.requester + "/displayname",
|
|
access_token=self.requester_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
"/profile/" + self.requester + "/avatar_url",
|
|
access_token=self.requester_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
|
|
class ProfileRatelimitTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
admin.register_servlets_for_client_rest_resource,
|
|
login.register_servlets,
|
|
profile.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.owner = self.register_user("owner", "pass")
|
|
self.owner_tok = self.login("owner", "pass")
|
|
self.other = self.register_user("other", "pass", displayname="Bob")
|
|
self.other_tok = self.login("other", "pass")
|
|
|
|
@unittest.override_config({"rc_profile": {"per_second": 0.1, "burst_count": 3}})
|
|
def test_ratelimit_authenticated(self) -> None:
|
|
"""Profile lookups from an authenticated user are rate limited per user,
|
|
with the limit shared across the profile endpoints.
|
|
"""
|
|
channel = self.make_request(
|
|
"GET", f"/profile/{self.other}", access_token=self.owner_tok
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET", f"/profile/{self.other}/displayname", access_token=self.owner_tok
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET", f"/profile/{self.other}/avatar_url", access_token=self.owner_tok
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request(
|
|
"GET", f"/profile/{self.other}", access_token=self.owner_tok
|
|
)
|
|
self.assertEqual(channel.code, 429, channel.result)
|
|
|
|
# Another user is not affected by the first user's limit.
|
|
channel = self.make_request(
|
|
"GET", f"/profile/{self.owner}", access_token=self.other_tok
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
@unittest.override_config({"rc_profile": {"per_second": 0.1, "burst_count": 3}})
|
|
def test_ratelimit_unauthenticated(self) -> None:
|
|
"""Unauthenticated profile lookups are rate limited per IP address."""
|
|
for _ in range(3):
|
|
channel = self.make_request("GET", f"/profile/{self.other}")
|
|
self.assertEqual(channel.code, 200, channel.result)
|
|
|
|
channel = self.make_request("GET", f"/profile/{self.other}")
|
|
self.assertEqual(channel.code, 429, channel.result)
|
|
|
|
# An authenticated user is not affected by the per-IP limit.
|
|
channel = self.make_request(
|
|
"GET", f"/profile/{self.other}", access_token=self.owner_tok
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.result)
|