Bumps the minor-and-patches group with 3 updates: [mypy](https://github.com/python/mypy), [mypy-zope](https://github.com/Shoobx/mypy-zope) and [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers). Updates `mypy` from 1.17.1 to 1.18.2 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/python/mypy/blob/master/CHANGELOG.md">mypy's changelog</a>.</em></p> <blockquote> <h3>Mypy 1.18.2</h3> <ul> <li>Fix crash on recursive alias (Ivan Levkivskyi, PR <a href="https://redirect.github.com/python/mypy/pull/19845">19845</a>)</li> <li>Add additional guidance for stubtest errors when runtime is <code>object.__init__</code> (Stephen Morton, PR <a href="https://redirect.github.com/python/mypy/pull/19733">19733</a>)</li> <li>Fix handling of None values in f-string expressions in mypyc (BobTheBuidler, PR <a href="https://redirect.github.com/python/mypy/pull/19846">19846</a>)</li> </ul> <h3>Acknowledgements</h3> <p>Thanks to all mypy contributors who contributed to this release:</p> <ul> <li>Ali Hamdan</li> <li>Anthony Sottile</li> <li>BobTheBuidler</li> <li>Brian Schubert</li> <li>Chainfire</li> <li>Charlie Denton</li> <li>Christoph Tyralla</li> <li>CoolCat467</li> <li>Daniel Hnyk</li> <li>Emily</li> <li>Emma Smith</li> <li>Ethan Sarp</li> <li>Ivan Levkivskyi</li> <li>Jahongir Qurbonov</li> <li>Jelle Zijlstra</li> <li>Joren Hammudoglu</li> <li>Jukka Lehtosalo</li> <li>Marc Mueller</li> <li>Omer Hadari</li> <li>Piotr Sawicki</li> <li>PrinceNaroliya</li> <li>Randolf Scholz</li> <li>Robsdedude</li> <li>Saul Shanabrook</li> <li>Shantanu</li> <li>Stanislav Terliakov</li> <li>Stephen Morton</li> <li>wyattscarpenter</li> </ul> <p>I’d also like to thank my employer, Dropbox, for supporting mypy development.</p> <h2>Mypy 1.17</h2> <p>We’ve just uploaded mypy 1.17 to the Python Package Index (<a href="https://pypi.org/project/mypy/">PyPI</a>). Mypy is a static type checker for Python. This release includes new features and bug fixes. You can install it as follows:</p> <pre><code>python3 -m pip install -U mypy </code></pre> <p>You can read the full documentation for this release on <a href="http://mypy.readthedocs.io">Read the Docs</a>.</p> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="df05f05555"><code>df05f05</code></a> remove +dev from version</li> <li><a href="01a7a1285d"><code>01a7a12</code></a> Update changelog for 1.18.2 (<a href="https://redirect.github.com/python/mypy/issues/19873">#19873</a>)</li> <li><a href="ca5abf09f3"><code>ca5abf0</code></a> Typeshed cherry-pick: Make type of <code>unitest.mock.Any</code> a subclass of <code>Any</code> (<a href="https://redirect.github.com/python/mypy/issues/1">#1</a>...</li> <li><a href="9d794b57d9"><code>9d794b5</code></a> [mypyc] fix: inappropriate <code>None</code>s in f-strings (<a href="https://redirect.github.com/python/mypy/issues/19846">#19846</a>)</li> <li><a href="2c0510c848"><code>2c0510c</code></a> stubtest: additional guidance on errors when runtime is object.<strong>init</strong> (<a href="https://redirect.github.com/python/mypy/issues/19733">#19733</a>)</li> <li><a href="2f3f03c3e3"><code>2f3f03c</code></a> Bump version to 1.18.2+dev for point release</li> <li><a href="76698412bc"><code>7669841</code></a> Fix crash on recursive alias in indirection.py (<a href="https://redirect.github.com/python/mypy/issues/19845">#19845</a>)</li> <li><a href="03fbaa941b"><code>03fbaa9</code></a> bump version to 1.18.1 due to wheels failure</li> <li><a href="b44a1fbf0c"><code>b44a1fb</code></a> removed +dev from version</li> <li><a href="7197a99d1a"><code>7197a99</code></a> Removed Unreleased in the Changelog for Release 1.18 (<a href="https://redirect.github.com/python/mypy/issues/19827">#19827</a>)</li> <li>Additional commits viewable in <a href="https://github.com/python/mypy/compare/v1.17.1...v1.18.2">compare view</a></li> </ul> </details> <br /> Updates `mypy-zope` from 1.0.13 to 1.0.14 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/Shoobx/mypy-zope/blob/master/CHANGELOG.md">mypy-zope's changelog</a>.</em></p> <blockquote> <h2>1.0.14 (2025-12-01)</h2> <hr /> <ul> <li>Support mypy-1.19</li> <li>Support mypy-1.18</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="38d22f3f4f"><code>38d22f3</code></a> Preparing release 1.0.14</li> <li><a href="76762ec861"><code>76762ec</code></a> Maintain changelog</li> <li><a href="4971d98ab8"><code>4971d98</code></a> Merge pull request <a href="https://redirect.github.com/Shoobx/mypy-zope/issues/134">#134</a> from Shoobx/dependabot/pip/mypy-gte-1.0.0-and-lt-1.20.0</li> <li><a href="47af89d2c7"><code>47af89d</code></a> Update mypy requirement from <1.19.0,>=1.0.0 to >=1.0.0,<1.20.0</li> <li><a href="0c596ff804"><code>0c596ff</code></a> Maintain changelog</li> <li><a href="dcaa27841d"><code>dcaa278</code></a> Merge pull request <a href="https://redirect.github.com/Shoobx/mypy-zope/issues/132">#132</a> from Shoobx/dependabot/pip/mypy-gte-1.0.0-and-lt-1.19.0</li> <li><a href="8f7b6778df"><code>8f7b677</code></a> Update mypy requirement from <1.18.0,>=1.0.0 to >=1.0.0,<1.19.0</li> <li><a href="91b275b364"><code>91b275b</code></a> Back to development: 1.0.14</li> <li>See full diff in <a href="https://github.com/Shoobx/mypy-zope/compare/1.0.13...1.0.14">compare view</a></li> </ul> </details> <br /> Updates `phonenumbers` from 9.0.18 to 9.0.19 <details> <summary>Commits</summary> <ul> <li><a href="38f2ffe1e8"><code>38f2ffe</code></a> Prep for 9.0.19 release</li> <li><a href="cd7f0cc64f"><code>cd7f0cc</code></a> Generated files for metadata</li> <li><a href="40ae18f50a"><code>40ae18f</code></a> Merge metadata changes from upstream 9.0.19</li> <li>See full diff in <a href="https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.18...v9.0.19">compare view</a></li> </ul> </details> <br /> **Does not** update `pysaml2` from 7.5.0 to 7.5.4 since this would downgrade pyOpenSSL <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/IdentityPython/pysaml2/releases">pysaml2's releases</a>.</em></p> <blockquote> <h2>Version v7.5.4</h2> <h2>v7.5.4 (2025-10-07)</h2> <ul> <li>Minor refactor to handle <code>shelve.open</code> and <code>dbm</code> errors</li> <li>Remove import of deprecated <code>cgi</code> module</li> <li>Replace deprecated <code>datetime.utcnow()</code> by <code>datetime.now(timezone.utc)</code></li> <li>deps: Remove the <code>importlib_metadata</code> dependency</li> <li>deps: Remove the <code>importlib_resources</code> dependency</li> <li>deps: Update dependency versions and lockfile</li> <li>build: Update pyproject and lockfile to be compatible with PEP 621</li> <li>docs: Correct spelling mistakes</li> <li>docs: Fix interal references/links</li> <li>docs: Clarify units for accepted_time_diff config param</li> <li>docs: Correct documentation for contact_person</li> </ul> <h2>Version 7.5.3</h2> <h2>7.5.3 (2025-10-04)</h2> <ul> <li><a href="https://redirect.github.com/IdentityPython/pysaml2/issues/973">#973</a> Fix prepare_for_negotiated_authenticate to avoid double signing redirect requests</li> </ul> <h2>Version 7.5.2</h2> <h2>7.5.2 (2025-02-10)</h2> <ul> <li>Include the XSD of the XML Encryption Syntax and Processing Version 1.1 to the schema validator</li> </ul> <h2>Version 7.5.1</h2> <h2>7.5.1 (2025-02-10)</h2> <ul> <li>deps: restrict pyOpenSSL up to v24.2.1 until it is replaced</li> <li>deps: update dependncies for the lockfile and examples</li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/IdentityPython/pysaml2/blob/master/CHANGELOG.md">pysaml2's changelog</a>.</em></p> <blockquote> <h2>v7.5.4 (2025-10-07)</h2> <ul> <li>Minor refactor to handle <code>shelve.open</code> and <code>dbm</code> errors</li> <li>Remove import of deprecated <code>cgi</code> module</li> <li>Replace deprecated <code>datetime.utcnow()</code> by <code>datetime.now(timezone.utc)</code></li> <li>deps: Remove the <code>importlib_metadata</code> dependency</li> <li>deps: Remove the <code>importlib_resources</code> dependency</li> <li>deps: Update dependency versions and lockfile</li> <li>build: Update pyproject and lockfile to be compatible with PEP 621</li> <li>docs: Correct spelling mistakes</li> <li>docs: Fix interal references/links</li> <li>docs: Clarify units for accepted_time_diff config param</li> <li>docs: Correct documentation for contact_person</li> </ul> <h2>7.5.3 (2025-10-04)</h2> <ul> <li><a href="https://redirect.github.com/IdentityPython/pysaml2/issues/973">#973</a> Fix prepare_for_negotiated_authenticate to avoid double signing redirect requests</li> </ul> <h2>7.5.2 (2025-02-10)</h2> <ul> <li>Include the XSD of the XML Encryption Syntax and Processing Version 1.1 to the schema validator</li> </ul> <h2>7.5.1 (2025-02-10)</h2> <ul> <li>deps: restrict pyOpenSSL up to v24.2.1 until it is replaced</li> <li>deps: update dependencies for the lockfile and examples</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="9cf71f7f9e"><code>9cf71f7</code></a> Release version 7.5.4</li> <li><a href="c3ec7199d1"><code>c3ec719</code></a> Refactor _shelve_compat</li> <li><a href="1d6ea6024e"><code>1d6ea60</code></a> Remove import of deprecated cgi module</li> <li><a href="c45eb9df82"><code>c45eb9d</code></a> Replace deprecated datetime.utcnow() by datetime.now(timezone.utc)</li> <li><a href="178f6d12b4"><code>178f6d1</code></a> Remove unneeded dependencies</li> <li><a href="1f0a25a5cf"><code>1f0a25a</code></a> remove importlib_metadata import</li> <li><a href="099f716ae7"><code>099f716</code></a> remove importlib_resources imports</li> <li><a href="3fa11ee15d"><code>3fa11ee</code></a> spelling updates.</li> <li><a href="4b7887f59a"><code>4b7887f</code></a> update link.</li> <li><a href="bc8d3b4ecc"><code>bc8d3b4</code></a> update link.</li> <li>Additional commits viewable in <a href="https://github.com/IdentityPython/pysaml2/compare/v7.5.0...v7.5.4">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Devon Hudson <devonhudson@librem.one>
601 lines
19 KiB
Python
601 lines
19 KiB
Python
#
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright 2020 Dirk Klimpel
|
|
# 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]
|
|
#
|
|
#
|
|
import urllib.parse
|
|
|
|
from parameterized import parameterized
|
|
|
|
from twisted.internet.testing import MemoryReactor
|
|
|
|
import synapse.rest.admin
|
|
from synapse.api.errors import Codes
|
|
from synapse.handlers.device import MAX_DEVICE_DISPLAY_NAME_LEN, DeviceWriterHandler
|
|
from synapse.rest.client import devices, login
|
|
from synapse.server import HomeServer
|
|
from synapse.util.clock import Clock
|
|
|
|
from tests import unittest
|
|
|
|
|
|
class DeviceRestTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
synapse.rest.admin.register_servlets,
|
|
login.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
handler = hs.get_device_handler()
|
|
assert isinstance(handler, DeviceWriterHandler)
|
|
self.handler = handler
|
|
|
|
self.admin_user = self.register_user("admin", "pass", admin=True)
|
|
self.admin_user_tok = self.login("admin", "pass")
|
|
|
|
self.other_user = self.register_user("user", "pass")
|
|
self.other_user_token = self.login("user", "pass")
|
|
res = self.get_success(self.handler.get_devices_by_user(self.other_user))
|
|
self.other_user_device_id = res[0]["device_id"]
|
|
|
|
self.url = "/_synapse/admin/v2/users/%s/devices/%s" % (
|
|
urllib.parse.quote(self.other_user),
|
|
self.other_user_device_id,
|
|
)
|
|
|
|
@parameterized.expand(["GET", "PUT", "DELETE"])
|
|
def test_no_auth(self, method: str) -> None:
|
|
"""
|
|
Try to get a device of an user without authentication.
|
|
"""
|
|
channel = self.make_request(method, self.url, b"{}")
|
|
|
|
self.assertEqual(
|
|
401,
|
|
channel.code,
|
|
msg=channel.json_body,
|
|
)
|
|
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
|
|
|
@parameterized.expand(["GET", "PUT", "DELETE"])
|
|
def test_requester_is_no_admin(self, method: str) -> None:
|
|
"""
|
|
If the user is not a server admin, an error is returned.
|
|
"""
|
|
channel = self.make_request(
|
|
method,
|
|
self.url,
|
|
access_token=self.other_user_token,
|
|
)
|
|
|
|
self.assertEqual(
|
|
403,
|
|
channel.code,
|
|
msg=channel.json_body,
|
|
)
|
|
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
|
|
|
@parameterized.expand(["GET", "PUT", "DELETE"])
|
|
def test_user_does_not_exist(self, method: str) -> None:
|
|
"""
|
|
Tests that a lookup for a user that does not exist returns a 404
|
|
"""
|
|
url = (
|
|
"/_synapse/admin/v2/users/@unknown_person:test/devices/%s"
|
|
% self.other_user_device_id
|
|
)
|
|
|
|
channel = self.make_request(
|
|
method,
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(404, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
|
|
|
@parameterized.expand(["GET", "PUT", "DELETE"])
|
|
def test_user_is_not_local(self, method: str) -> None:
|
|
"""
|
|
Tests that a lookup for a user that is not a local returns a 400
|
|
"""
|
|
url = (
|
|
"/_synapse/admin/v2/users/@unknown_person:unknown_domain/devices/%s"
|
|
% self.other_user_device_id
|
|
)
|
|
|
|
channel = self.make_request(
|
|
method,
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
|
self.assertEqual("Can only lookup local users", channel.json_body["error"])
|
|
|
|
def test_unknown_device(self) -> None:
|
|
"""
|
|
Tests that a lookup for a device that does not exist returns either 404 or 200.
|
|
"""
|
|
url = "/_synapse/admin/v2/users/%s/devices/unknown_device" % urllib.parse.quote(
|
|
self.other_user
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(404, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
# Delete unknown device returns status 200
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
|
|
def test_update_device_too_long_display_name(self) -> None:
|
|
"""
|
|
Update a device with a display name that is invalid (too long).
|
|
"""
|
|
# Set iniital display name.
|
|
update = {"display_name": "new display"}
|
|
self.get_success(
|
|
self.handler.update_device(
|
|
self.other_user, self.other_user_device_id, update
|
|
)
|
|
)
|
|
|
|
# Request to update a device display name with a new value that is longer than allowed.
|
|
update = {"display_name": "a" * (MAX_DEVICE_DISPLAY_NAME_LEN + 1)}
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
content=update,
|
|
)
|
|
|
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.TOO_LARGE, channel.json_body["errcode"])
|
|
|
|
# Ensure the display name was not updated.
|
|
channel = self.make_request(
|
|
"GET",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual("new display", channel.json_body["display_name"])
|
|
|
|
def test_update_no_display_name(self) -> None:
|
|
"""
|
|
Tests that a update for a device without JSON returns a 200
|
|
"""
|
|
# Set iniital display name.
|
|
update = {"display_name": "new display"}
|
|
self.get_success(
|
|
self.handler.update_device(
|
|
self.other_user, self.other_user_device_id, update
|
|
)
|
|
)
|
|
|
|
channel = self.make_request(
|
|
"PUT",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
|
|
# Ensure the display name was not updated.
|
|
channel = self.make_request(
|
|
"GET",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual("new display", channel.json_body["display_name"])
|
|
|
|
def test_update_display_name(self) -> None:
|
|
"""
|
|
Tests a normal successful update of display name
|
|
"""
|
|
# Set new display_name
|
|
channel = self.make_request(
|
|
"PUT",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
content={"display_name": "new displayname"},
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
|
|
# Check new display_name
|
|
channel = self.make_request(
|
|
"GET",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual("new displayname", channel.json_body["display_name"])
|
|
|
|
def test_get_device(self) -> None:
|
|
"""
|
|
Tests that a normal lookup for a device is successfully
|
|
"""
|
|
channel = self.make_request(
|
|
"GET",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(self.other_user, channel.json_body["user_id"])
|
|
# Check that all fields are available
|
|
self.assertIn("user_id", channel.json_body)
|
|
self.assertIn("device_id", channel.json_body)
|
|
self.assertIn("display_name", channel.json_body)
|
|
self.assertIn("last_seen_ip", channel.json_body)
|
|
self.assertIn("last_seen_ts", channel.json_body)
|
|
|
|
def test_delete_device(self) -> None:
|
|
"""
|
|
Tests that a remove of a device is successfully
|
|
"""
|
|
# Count number of devies of an user.
|
|
res = self.get_success(self.handler.get_devices_by_user(self.other_user))
|
|
number_devices = len(res)
|
|
self.assertEqual(1, number_devices)
|
|
|
|
# Delete device
|
|
channel = self.make_request(
|
|
"DELETE",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
|
|
# Ensure that the number of devices is decreased
|
|
res = self.get_success(self.handler.get_devices_by_user(self.other_user))
|
|
self.assertEqual(number_devices - 1, len(res))
|
|
|
|
|
|
class DevicesRestTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
synapse.rest.admin.register_servlets,
|
|
devices.register_servlets,
|
|
login.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.admin_user = self.register_user("admin", "pass", admin=True)
|
|
self.admin_user_tok = self.login("admin", "pass")
|
|
|
|
self.other_user = self.register_user("user", "pass")
|
|
|
|
self.url = "/_synapse/admin/v2/users/%s/devices" % urllib.parse.quote(
|
|
self.other_user
|
|
)
|
|
|
|
def test_no_auth(self) -> None:
|
|
"""
|
|
Try to list devices of an user without authentication.
|
|
"""
|
|
channel = self.make_request("GET", self.url, b"{}")
|
|
|
|
self.assertEqual(
|
|
401,
|
|
channel.code,
|
|
msg=channel.json_body,
|
|
)
|
|
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
|
|
|
def test_requester_is_no_admin(self) -> None:
|
|
"""
|
|
If the user is not a server admin, an error is returned.
|
|
"""
|
|
other_user_token = self.login("user", "pass")
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
self.url,
|
|
access_token=other_user_token,
|
|
)
|
|
|
|
self.assertEqual(
|
|
403,
|
|
channel.code,
|
|
msg=channel.json_body,
|
|
)
|
|
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
|
|
|
def test_user_does_not_exist(self) -> None:
|
|
"""
|
|
Tests that a lookup for a user that does not exist returns a 404
|
|
"""
|
|
url = "/_synapse/admin/v2/users/@unknown_person:test/devices"
|
|
channel = self.make_request(
|
|
"GET",
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(404, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
|
|
|
def test_user_is_not_local(self) -> None:
|
|
"""
|
|
Tests that a lookup for a user that is not a local returns a 400
|
|
"""
|
|
url = "/_synapse/admin/v2/users/@unknown_person:unknown_domain/devices"
|
|
|
|
channel = self.make_request(
|
|
"GET",
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
|
self.assertEqual("Can only lookup local users", channel.json_body["error"])
|
|
|
|
def test_user_has_no_devices(self) -> None:
|
|
"""
|
|
Tests that a normal lookup for devices is successfully
|
|
if user has no devices
|
|
"""
|
|
|
|
# Get devices
|
|
channel = self.make_request(
|
|
"GET",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(0, channel.json_body["total"])
|
|
self.assertEqual(0, len(channel.json_body["devices"]))
|
|
|
|
@unittest.override_config(
|
|
{"experimental_features": {"msc2697_enabled": False, "msc3814_enabled": True}}
|
|
)
|
|
def test_get_devices(self) -> None:
|
|
"""
|
|
Tests that a normal lookup for devices is successfully
|
|
"""
|
|
# Create devices
|
|
number_devices = 5
|
|
# we create 2 fewer devices in the loop, because we will create another
|
|
# login after the loop, and we will create a dehydrated device
|
|
for _ in range(number_devices - 2):
|
|
self.login("user", "pass")
|
|
|
|
other_user_token = self.login("user", "pass")
|
|
dehydrated_device_url = (
|
|
"/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device"
|
|
)
|
|
content = {
|
|
"device_data": {
|
|
"algorithm": "m.dehydration.v1.olm",
|
|
},
|
|
"device_id": "dehydrated_device",
|
|
"initial_device_display_name": "foo bar",
|
|
"device_keys": {
|
|
"user_id": "@user:test",
|
|
"device_id": "dehydrated_device",
|
|
"valid_until_ts": "80",
|
|
"algorithms": [
|
|
"m.olm.curve25519-aes-sha2",
|
|
],
|
|
"keys": {
|
|
"<algorithm>:<device_id>": "<key_base64>",
|
|
},
|
|
"signatures": {
|
|
"@user:test": {"<algorithm>:<device_id>": "<signature_base64>"}
|
|
},
|
|
},
|
|
"fallback_keys": {
|
|
"alg1:device1": "f4llb4ckk3y",
|
|
"signed_<algorithm>:<device_id>": {
|
|
"fallback": "true",
|
|
"key": "f4llb4ckk3y",
|
|
"signatures": {
|
|
"@user:test": {"<algorithm>:<device_id>": "<key_base64>"}
|
|
},
|
|
},
|
|
},
|
|
"one_time_keys": {"alg1:k1": "0net1m3k3y"},
|
|
}
|
|
self.make_request(
|
|
"PUT",
|
|
dehydrated_device_url,
|
|
access_token=other_user_token,
|
|
content=content,
|
|
)
|
|
|
|
# Get devices
|
|
channel = self.make_request(
|
|
"GET",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
self.assertEqual(number_devices, channel.json_body["total"])
|
|
self.assertEqual(number_devices, len(channel.json_body["devices"]))
|
|
self.assertEqual(self.other_user, channel.json_body["devices"][0]["user_id"])
|
|
# Check that all fields are available, and that the dehydrated device is marked as dehydrated
|
|
found_dehydrated = False
|
|
for d in channel.json_body["devices"]:
|
|
self.assertIn("user_id", d)
|
|
self.assertIn("device_id", d)
|
|
self.assertIn("display_name", d)
|
|
self.assertIn("last_seen_ip", d)
|
|
self.assertIn("last_seen_ts", d)
|
|
if d["device_id"] == "dehydrated_device":
|
|
self.assertTrue(d.get("dehydrated"))
|
|
found_dehydrated = True
|
|
else:
|
|
# Either the field is not present, or set to False
|
|
self.assertFalse(d.get("dehydrated"))
|
|
|
|
self.assertTrue(found_dehydrated)
|
|
|
|
|
|
class DeleteDevicesRestTestCase(unittest.HomeserverTestCase):
|
|
servlets = [
|
|
synapse.rest.admin.register_servlets,
|
|
login.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.handler = hs.get_device_handler()
|
|
|
|
self.admin_user = self.register_user("admin", "pass", admin=True)
|
|
self.admin_user_tok = self.login("admin", "pass")
|
|
|
|
self.other_user = self.register_user("user", "pass")
|
|
|
|
self.url = "/_synapse/admin/v2/users/%s/delete_devices" % urllib.parse.quote(
|
|
self.other_user
|
|
)
|
|
|
|
def test_no_auth(self) -> None:
|
|
"""
|
|
Try to delete devices of an user without authentication.
|
|
"""
|
|
channel = self.make_request("POST", self.url, b"{}")
|
|
|
|
self.assertEqual(
|
|
401,
|
|
channel.code,
|
|
msg=channel.json_body,
|
|
)
|
|
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
|
|
|
def test_requester_is_no_admin(self) -> None:
|
|
"""
|
|
If the user is not a server admin, an error is returned.
|
|
"""
|
|
other_user_token = self.login("user", "pass")
|
|
|
|
channel = self.make_request(
|
|
"POST",
|
|
self.url,
|
|
access_token=other_user_token,
|
|
)
|
|
|
|
self.assertEqual(
|
|
403,
|
|
channel.code,
|
|
msg=channel.json_body,
|
|
)
|
|
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
|
|
|
def test_user_does_not_exist(self) -> None:
|
|
"""
|
|
Tests that a lookup for a user that does not exist returns a 404
|
|
"""
|
|
url = "/_synapse/admin/v2/users/@unknown_person:test/delete_devices"
|
|
channel = self.make_request(
|
|
"POST",
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(404, channel.code, msg=channel.json_body)
|
|
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
|
|
|
def test_user_is_not_local(self) -> None:
|
|
"""
|
|
Tests that a lookup for a user that is not a local returns a 400
|
|
"""
|
|
url = "/_synapse/admin/v2/users/@unknown_person:unknown_domain/delete_devices"
|
|
|
|
channel = self.make_request(
|
|
"POST",
|
|
url,
|
|
access_token=self.admin_user_tok,
|
|
)
|
|
|
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
|
self.assertEqual("Can only lookup local users", channel.json_body["error"])
|
|
|
|
def test_unknown_devices(self) -> None:
|
|
"""
|
|
Tests that a remove of a device that does not exist returns 200.
|
|
"""
|
|
channel = self.make_request(
|
|
"POST",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
content={"devices": ["unknown_device1", "unknown_device2"]},
|
|
)
|
|
|
|
# Delete unknown devices returns status 200
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
|
|
def test_delete_devices(self) -> None:
|
|
"""
|
|
Tests that a remove of devices is successfully
|
|
"""
|
|
|
|
# Create devices
|
|
number_devices = 5
|
|
for _ in range(number_devices):
|
|
self.login("user", "pass")
|
|
|
|
# Get devices
|
|
res = self.get_success(self.handler.get_devices_by_user(self.other_user))
|
|
self.assertEqual(number_devices, len(res))
|
|
|
|
# Create list of device IDs
|
|
device_ids = []
|
|
for d in res:
|
|
device_ids.append(str(d["device_id"]))
|
|
|
|
# Delete devices
|
|
channel = self.make_request(
|
|
"POST",
|
|
self.url,
|
|
access_token=self.admin_user_tok,
|
|
content={"devices": device_ids},
|
|
)
|
|
|
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
|
|
|
res = self.get_success(self.handler.get_devices_by_user(self.other_user))
|
|
self.assertEqual(0, len(res))
|