mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-10-01 11:49:51 -04:00
Add support for MSC2697: Dehydrated devices (#8380)
This allows a user to store an offline device on the server and then restore it at a subsequent login.
This commit is contained in:
parent
43c622885c
commit
4cb44a1585
1
changelog.d/8380.feature
Normal file
1
changelog.d/8380.feature
Normal file
@ -0,0 +1 @@
|
||||
Add support for device dehydration ([MSC2697](https://github.com/matrix-org/matrix-doc/pull/2697)).
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016 OpenMarket Ltd
|
||||
# Copyright 2019 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2019,2020 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.
|
||||
@ -15,7 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from synapse.api import errors
|
||||
from synapse.api.constants import EventTypes
|
||||
@ -29,6 +29,7 @@ from synapse.api.errors import (
|
||||
from synapse.logging.opentracing import log_kv, set_tag, trace
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.types import (
|
||||
JsonDict,
|
||||
StreamToken,
|
||||
get_domain_from_id,
|
||||
get_verify_key_from_cross_signing_key,
|
||||
@ -505,6 +506,85 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||
# receive device updates. Mark this in DB.
|
||||
await self.store.mark_remote_user_device_list_as_unsubscribed(user_id)
|
||||
|
||||
async def store_dehydrated_device(
|
||||
self,
|
||||
user_id: str,
|
||||
device_data: JsonDict,
|
||||
initial_device_display_name: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Store a dehydrated device for a user. If the user had a previous
|
||||
dehydrated device, it is removed.
|
||||
|
||||
Args:
|
||||
user_id: the user that we are storing the device for
|
||||
device_data: the dehydrated device information
|
||||
initial_device_display_name: The display name to use for the device
|
||||
Returns:
|
||||
device id of the dehydrated device
|
||||
"""
|
||||
device_id = await self.check_device_registered(
|
||||
user_id, None, initial_device_display_name,
|
||||
)
|
||||
old_device_id = await self.store.store_dehydrated_device(
|
||||
user_id, device_id, device_data
|
||||
)
|
||||
if old_device_id is not None:
|
||||
await self.delete_device(user_id, old_device_id)
|
||||
return device_id
|
||||
|
||||
async def get_dehydrated_device(
|
||||
self, user_id: str
|
||||
) -> Optional[Tuple[str, JsonDict]]:
|
||||
"""Retrieve the information for a dehydrated device.
|
||||
|
||||
Args:
|
||||
user_id: the user whose dehydrated device we are looking for
|
||||
Returns:
|
||||
a tuple whose first item is the device ID, and the second item is
|
||||
the dehydrated device information
|
||||
"""
|
||||
return await self.store.get_dehydrated_device(user_id)
|
||||
|
||||
async def rehydrate_device(
|
||||
self, user_id: str, access_token: str, device_id: str
|
||||
) -> dict:
|
||||
"""Process a rehydration request from the user.
|
||||
|
||||
Args:
|
||||
user_id: the user who is rehydrating the device
|
||||
access_token: the access token used for the request
|
||||
device_id: the ID of the device that will be rehydrated
|
||||
Returns:
|
||||
a dict containing {"success": True}
|
||||
"""
|
||||
success = await self.store.remove_dehydrated_device(user_id, device_id)
|
||||
|
||||
if not success:
|
||||
raise errors.NotFoundError()
|
||||
|
||||
# If the dehydrated device was successfully deleted (the device ID
|
||||
# matched the stored dehydrated device), then modify the access
|
||||
# token to use the dehydrated device's ID and copy the old device
|
||||
# display name to the dehydrated device, and destroy the old device
|
||||
# ID
|
||||
old_device_id = await self.store.set_device_for_access_token(
|
||||
access_token, device_id
|
||||
)
|
||||
old_device = await self.store.get_device(user_id, old_device_id)
|
||||
await self.store.update_device(user_id, device_id, old_device["display_name"])
|
||||
# can't call self.delete_device because that will clobber the
|
||||
# access token so call the storage layer directly
|
||||
await self.store.delete_device(user_id, old_device_id)
|
||||
await self.store.delete_e2e_keys_by_device(
|
||||
user_id=user_id, device_id=old_device_id
|
||||
)
|
||||
|
||||
# tell everyone that the old device is gone and that the dehydrated
|
||||
# device has a new display name
|
||||
await self.notify_device_update(user_id, [old_device_id, device_id])
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
def _update_device_from_client_ips(device, client_ips):
|
||||
ip = client_ips.get((device["user_id"], device["device_id"]), {})
|
||||
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2020 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.
|
||||
@ -21,6 +22,7 @@ from synapse.http.servlet import (
|
||||
assert_params_in_dict,
|
||||
parse_json_object_from_request,
|
||||
)
|
||||
from synapse.http.site import SynapseRequest
|
||||
|
||||
from ._base import client_patterns, interactive_auth_handler
|
||||
|
||||
@ -151,7 +153,139 @@ class DeviceRestServlet(RestServlet):
|
||||
return 200, {}
|
||||
|
||||
|
||||
class DehydratedDeviceServlet(RestServlet):
|
||||
"""Retrieve or store a dehydrated device.
|
||||
|
||||
GET /org.matrix.msc2697.v2/dehydrated_device
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": "dehydrated_device_id",
|
||||
"device_data": {
|
||||
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
|
||||
"account": "dehydrated_device"
|
||||
}
|
||||
}
|
||||
|
||||
PUT /org.matrix.msc2697/dehydrated_device
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_data": {
|
||||
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
|
||||
"account": "dehydrated_device"
|
||||
}
|
||||
}
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": "dehydrated_device_id"
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
PATTERNS = client_patterns("/org.matrix.msc2697.v2/dehydrated_device", releases=())
|
||||
|
||||
def __init__(self, hs):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.device_handler = hs.get_device_handler()
|
||||
|
||||
async def on_GET(self, request: SynapseRequest):
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
dehydrated_device = await self.device_handler.get_dehydrated_device(
|
||||
requester.user.to_string()
|
||||
)
|
||||
if dehydrated_device is not None:
|
||||
(device_id, device_data) = dehydrated_device
|
||||
result = {"device_id": device_id, "device_data": device_data}
|
||||
return (200, result)
|
||||
else:
|
||||
raise errors.NotFoundError("No dehydrated device available")
|
||||
|
||||
async def on_PUT(self, request: SynapseRequest):
|
||||
submission = parse_json_object_from_request(request)
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
if "device_data" not in submission:
|
||||
raise errors.SynapseError(
|
||||
400, "device_data missing", errcode=errors.Codes.MISSING_PARAM,
|
||||
)
|
||||
elif not isinstance(submission["device_data"], dict):
|
||||
raise errors.SynapseError(
|
||||
400,
|
||||
"device_data must be an object",
|
||||
errcode=errors.Codes.INVALID_PARAM,
|
||||
)
|
||||
|
||||
device_id = await self.device_handler.store_dehydrated_device(
|
||||
requester.user.to_string(),
|
||||
submission["device_data"],
|
||||
submission.get("initial_device_display_name", None),
|
||||
)
|
||||
return 200, {"device_id": device_id}
|
||||
|
||||
|
||||
class ClaimDehydratedDeviceServlet(RestServlet):
|
||||
"""Claim a dehydrated device.
|
||||
|
||||
POST /org.matrix.msc2697.v2/dehydrated_device/claim
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": "dehydrated_device_id"
|
||||
}
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"success": true,
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
PATTERNS = client_patterns(
|
||||
"/org.matrix.msc2697.v2/dehydrated_device/claim", releases=()
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.device_handler = hs.get_device_handler()
|
||||
|
||||
async def on_POST(self, request: SynapseRequest):
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
submission = parse_json_object_from_request(request)
|
||||
|
||||
if "device_id" not in submission:
|
||||
raise errors.SynapseError(
|
||||
400, "device_id missing", errcode=errors.Codes.MISSING_PARAM,
|
||||
)
|
||||
elif not isinstance(submission["device_id"], str):
|
||||
raise errors.SynapseError(
|
||||
400, "device_id must be a string", errcode=errors.Codes.INVALID_PARAM,
|
||||
)
|
||||
|
||||
result = await self.device_handler.rehydrate_device(
|
||||
requester.user.to_string(),
|
||||
self.auth.get_access_token_from_request(request),
|
||||
submission["device_id"],
|
||||
)
|
||||
|
||||
return (200, result)
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
DeleteDevicesRestServlet(hs).register(http_server)
|
||||
DevicesRestServlet(hs).register(http_server)
|
||||
DeviceRestServlet(hs).register(http_server)
|
||||
DehydratedDeviceServlet(hs).register(http_server)
|
||||
ClaimDehydratedDeviceServlet(hs).register(http_server)
|
||||
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2019 New Vector Ltd
|
||||
# Copyright 2020 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.
|
||||
@ -67,6 +68,7 @@ class KeyUploadServlet(RestServlet):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.e2e_keys_handler = hs.get_e2e_keys_handler()
|
||||
self.device_handler = hs.get_device_handler()
|
||||
|
||||
@trace(opname="upload_keys")
|
||||
async def on_POST(self, request, device_id):
|
||||
@ -75,9 +77,14 @@ class KeyUploadServlet(RestServlet):
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
if device_id is not None:
|
||||
# passing the device_id here is deprecated; however, we allow it
|
||||
# for now for compatibility with older clients.
|
||||
# Providing the device_id should only be done for setting keys
|
||||
# for dehydrated devices; however, we allow it for any device for
|
||||
# compatibility with older clients.
|
||||
if requester.device_id is not None and device_id != requester.device_id:
|
||||
dehydrated_device = await self.device_handler.get_dehydrated_device(
|
||||
user_id
|
||||
)
|
||||
if dehydrated_device is not None and device_id != dehydrated_device[0]:
|
||||
set_tag("error", True)
|
||||
log_kv(
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016 OpenMarket Ltd
|
||||
# Copyright 2019 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2019,2020 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.
|
||||
@ -33,7 +33,7 @@ from synapse.storage.database import (
|
||||
make_tuple_comparison_clause,
|
||||
)
|
||||
from synapse.types import Collection, JsonDict, get_verify_key_from_cross_signing_key
|
||||
from synapse.util import json_encoder
|
||||
from synapse.util import json_decoder, json_encoder
|
||||
from synapse.util.caches.descriptors import Cache, cached, cachedList
|
||||
from synapse.util.iterutils import batch_iter
|
||||
from synapse.util.stringutils import shortstr
|
||||
@ -698,6 +698,80 @@ class DeviceWorkerStore(SQLBaseStore):
|
||||
_mark_remote_user_device_list_as_unsubscribed_txn,
|
||||
)
|
||||
|
||||
async def get_dehydrated_device(
|
||||
self, user_id: str
|
||||
) -> Optional[Tuple[str, JsonDict]]:
|
||||
"""Retrieve the information for a dehydrated device.
|
||||
|
||||
Args:
|
||||
user_id: the user whose dehydrated device we are looking for
|
||||
Returns:
|
||||
a tuple whose first item is the device ID, and the second item is
|
||||
the dehydrated device information
|
||||
"""
|
||||
# FIXME: make sure device ID still exists in devices table
|
||||
row = await self.db_pool.simple_select_one(
|
||||
table="dehydrated_devices",
|
||||
keyvalues={"user_id": user_id},
|
||||
retcols=["device_id", "device_data"],
|
||||
allow_none=True,
|
||||
)
|
||||
return (
|
||||
(row["device_id"], json_decoder.decode(row["device_data"])) if row else None
|
||||
)
|
||||
|
||||
def _store_dehydrated_device_txn(
|
||||
self, txn, user_id: str, device_id: str, device_data: str
|
||||
) -> Optional[str]:
|
||||
old_device_id = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="dehydrated_devices",
|
||||
keyvalues={"user_id": user_id},
|
||||
retcol="device_id",
|
||||
allow_none=True,
|
||||
)
|
||||
self.db_pool.simple_upsert_txn(
|
||||
txn,
|
||||
table="dehydrated_devices",
|
||||
keyvalues={"user_id": user_id},
|
||||
values={"device_id": device_id, "device_data": device_data},
|
||||
)
|
||||
return old_device_id
|
||||
|
||||
async def store_dehydrated_device(
|
||||
self, user_id: str, device_id: str, device_data: JsonDict
|
||||
) -> Optional[str]:
|
||||
"""Store a dehydrated device for a user.
|
||||
|
||||
Args:
|
||||
user_id: the user that we are storing the device for
|
||||
device_id: the ID of the dehydrated device
|
||||
device_data: the dehydrated device information
|
||||
Returns:
|
||||
device id of the user's previous dehydrated device, if any
|
||||
"""
|
||||
return await self.db_pool.runInteraction(
|
||||
"store_dehydrated_device_txn",
|
||||
self._store_dehydrated_device_txn,
|
||||
user_id,
|
||||
device_id,
|
||||
json_encoder.encode(device_data),
|
||||
)
|
||||
|
||||
async def remove_dehydrated_device(self, user_id: str, device_id: str) -> bool:
|
||||
"""Remove a dehydrated device.
|
||||
|
||||
Args:
|
||||
user_id: the user that the dehydrated device belongs to
|
||||
device_id: the ID of the dehydrated device
|
||||
"""
|
||||
count = await self.db_pool.simple_delete(
|
||||
"dehydrated_devices",
|
||||
{"user_id": user_id, "device_id": device_id},
|
||||
desc="remove_dehydrated_device",
|
||||
)
|
||||
return count >= 1
|
||||
|
||||
|
||||
class DeviceBackgroundUpdateStore(SQLBaseStore):
|
||||
def __init__(self, database: DatabasePool, db_conn, hs):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2019 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2019,2020 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.
|
||||
@ -844,6 +844,11 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore):
|
||||
self._invalidate_cache_and_stream(
|
||||
txn, self.count_e2e_one_time_keys, (user_id, device_id)
|
||||
)
|
||||
self.db_pool.simple_delete_txn(
|
||||
txn,
|
||||
table="dehydrated_devices",
|
||||
keyvalues={"user_id": user_id, "device_id": device_id},
|
||||
)
|
||||
self.db_pool.simple_delete_txn(
|
||||
txn,
|
||||
table="e2e_fallback_keys_json",
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2019,2020 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.
|
||||
@ -964,6 +964,36 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
||||
desc="add_access_token_to_user",
|
||||
)
|
||||
|
||||
def _set_device_for_access_token_txn(self, txn, token: str, device_id: str) -> str:
|
||||
old_device_id = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn, "access_tokens", {"token": token}, "device_id"
|
||||
)
|
||||
|
||||
self.db_pool.simple_update_txn(
|
||||
txn, "access_tokens", {"token": token}, {"device_id": device_id}
|
||||
)
|
||||
|
||||
self._invalidate_cache_and_stream(txn, self.get_user_by_access_token, (token,))
|
||||
|
||||
return old_device_id
|
||||
|
||||
async def set_device_for_access_token(self, token: str, device_id: str) -> str:
|
||||
"""Sets the device ID associated with an access token.
|
||||
|
||||
Args:
|
||||
token: The access token to modify.
|
||||
device_id: The new device ID.
|
||||
Returns:
|
||||
The old device ID associated with the access token.
|
||||
"""
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"set_device_for_access_token",
|
||||
self._set_device_for_access_token_txn,
|
||||
token,
|
||||
device_id,
|
||||
)
|
||||
|
||||
async def register_user(
|
||||
self,
|
||||
user_id: str,
|
||||
|
@ -0,0 +1,20 @@
|
||||
/* Copyright 2020 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.
|
||||
*/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dehydrated_devices(
|
||||
user_id TEXT NOT NULL PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
device_data TEXT NOT NULL -- JSON-encoded client-defined data
|
||||
);
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2020 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.
|
||||
@ -224,3 +225,84 @@ class DeviceTestCase(unittest.HomeserverTestCase):
|
||||
)
|
||||
)
|
||||
self.reactor.advance(1000)
|
||||
|
||||
|
||||
class DehydrationTestCase(unittest.HomeserverTestCase):
|
||||
def make_homeserver(self, reactor, clock):
|
||||
hs = self.setup_test_homeserver("server", http_client=None)
|
||||
self.handler = hs.get_device_handler()
|
||||
self.registration = hs.get_registration_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
return hs
|
||||
|
||||
def test_dehydrate_and_rehydrate_device(self):
|
||||
user_id = "@boris:dehydration"
|
||||
|
||||
self.get_success(self.store.register_user(user_id, "foobar"))
|
||||
|
||||
# First check if we can store and fetch a dehydrated device
|
||||
stored_dehydrated_device_id = self.get_success(
|
||||
self.handler.store_dehydrated_device(
|
||||
user_id=user_id,
|
||||
device_data={"device_data": {"foo": "bar"}},
|
||||
initial_device_display_name="dehydrated device",
|
||||
)
|
||||
)
|
||||
|
||||
retrieved_device_id, device_data = self.get_success(
|
||||
self.handler.get_dehydrated_device(user_id=user_id)
|
||||
)
|
||||
|
||||
self.assertEqual(retrieved_device_id, stored_dehydrated_device_id)
|
||||
self.assertEqual(device_data, {"device_data": {"foo": "bar"}})
|
||||
|
||||
# Create a new login for the user and dehydrated the device
|
||||
device_id, access_token = self.get_success(
|
||||
self.registration.register_device(
|
||||
user_id=user_id, device_id=None, initial_display_name="new device",
|
||||
)
|
||||
)
|
||||
|
||||
# Trying to claim a nonexistent device should throw an error
|
||||
self.get_failure(
|
||||
self.handler.rehydrate_device(
|
||||
user_id=user_id,
|
||||
access_token=access_token,
|
||||
device_id="not the right device ID",
|
||||
),
|
||||
synapse.api.errors.NotFoundError,
|
||||
)
|
||||
|
||||
# dehydrating the right devices should succeed and change our device ID
|
||||
# to the dehydrated device's ID
|
||||
res = self.get_success(
|
||||
self.handler.rehydrate_device(
|
||||
user_id=user_id,
|
||||
access_token=access_token,
|
||||
device_id=retrieved_device_id,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(res, {"success": True})
|
||||
|
||||
# make sure that our device ID has changed
|
||||
user_info = self.get_success(self.auth.get_user_by_access_token(access_token))
|
||||
|
||||
self.assertEqual(user_info["device_id"], retrieved_device_id)
|
||||
|
||||
# make sure the device has the display name that was set from the login
|
||||
res = self.get_success(self.handler.get_device(user_id, retrieved_device_id))
|
||||
|
||||
self.assertEqual(res["display_name"], "new device")
|
||||
|
||||
# make sure that the device ID that we were initially assigned no longer exists
|
||||
self.get_failure(
|
||||
self.handler.get_device(user_id, device_id),
|
||||
synapse.api.errors.NotFoundError,
|
||||
)
|
||||
|
||||
# make sure that there's no device available for dehydrating now
|
||||
ret = self.get_success(self.handler.get_dehydrated_device(user_id=user_id))
|
||||
|
||||
self.assertIsNone(ret)
|
||||
|
Loading…
Reference in New Issue
Block a user