Repository: matrix-org/synapse

Issue:
PUT /dehydrated_device is broken and doesn't accept valid requests
### Description

See https://github.com/matrix-org/matrix-js-sdk/issues/2809
See https://github.com/matrix-org/synapse/pull/14054/files#r1009320711

The PUT API is demanding a `device_id` body param, which is not part of the MSC. https://github.com/uhoreg/matrix-doc/blob/dehydration/proposals/2697-device-dehydration.md#dehydrating-a-device

### Steps to reproduce

- Send valid PUT /dehydrated_device request
- Receive 400 (Bad Request) `M_MISSING_PARAM: MatrixError: [400] 1 validation error for PutBody device_id field required (type=value_error.missing) (/_matrix/client/unstable/org.matrix.msc2697.v2/dehydrated_device)`

### Homeserver

Any

### Synapse Version

1.70.0

### Installation Method

_No response_

### Platform

Any

### Relevant log output

```shell
M_MISSING_PARAM: MatrixError: [400] 1 validation error for PutBody device_id field required (type=value_error.missing) (/_matrix/client/unstable/org.matrix.msc2697.v2/dehydrated_device)
```


### Anything else that would be useful to know?

Regressed by https://github.com/matrix-org/synapse/pull/14054


Discussion:
To make it explicit -- this regressed in v1.70.0rc1.
@clokep Are you sure this is a regression? AFAICS from https://github.com/matrix-org/synapse/pull/14054/files?diff=split&w=0#diff-7225331690e55a76b271fbcaf20bed5aaaf69fb5a21b03a9bf297929b157ba1cL300-R333 we were always looking for `device_id` in the request body.
@DMRobertson that's POST, this issue is about PUT
Ahh yes, thanks.

Resolving patch:
diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py
--- a/synapse/rest/client/devices.py
+++ b/synapse/rest/client/devices.py
@@ -231,7 +231,7 @@ class DehydratedDeviceServlet(RestServlet):
       }
     }
 
-    PUT /org.matrix.msc2697/dehydrated_device
+    PUT /org.matrix.msc2697.v2/dehydrated_device
     Content-Type: application/json
 
     {
@@ -271,7 +271,6 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
             raise errors.NotFoundError("No dehydrated device available")
 
     class PutBody(RequestBodyModel):
-        device_id: StrictStr
         device_data: DehydratedDeviceDataModel
         initial_device_display_name: Optional[StrictStr]
 
@@ -281,7 +280,7 @@ async def on_PUT(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
 
         device_id = await self.device_handler.store_dehydrated_device(
             requester.user.to_string(),
-            submission.device_data,
+            submission.device_data.dict(),
             submission.initial_device_display_name,
         )
         return 200, {"device_id": device_id}


Test patch:
diff --git a/tests/rest/client/test_devices.py b/tests/rest/client/test_devices.py
--- a/tests/rest/client/test_devices.py
+++ b/tests/rest/client/test_devices.py
@@ -200,3 +200,37 @@ def test_delete_stale_devices(self) -> None:
         self.reactor.advance(43200)
         self.get_success(self.handler.get_device(user_id, "abc"))
         self.get_failure(self.handler.get_device(user_id, "def"), NotFoundError)
+
+
+class DehydratedDeviceTestCase(unittest.HomeserverTestCase):
+    servlets = [
+        admin.register_servlets_for_client_rest_resource,
+        login.register_servlets,
+        register.register_servlets,
+        devices.register_servlets,
+    ]
+
+    def test_PUT(self) -> None:
+        """Sanity-check that we can PUT a dehydrated device.
+
+        Detects https://github.com/matrix-org/synapse/issues/14334.
+        """
+        alice = self.register_user("alice", "correcthorse")
+        token = self.login(alice, "correcthorse")
+
+        # Have alice update their device list
+        channel = self.make_request(
+            "PUT",
+            "_matrix/client/unstable/org.matrix.msc2697.v2/dehydrated_device",
+            {
+                "device_data": {
+                    "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
+                    "account": "dehydrated_device",
+                }
+            },
+            access_token=token,
+            shorthand=False,
+        )
+        self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
+        device_id = channel.json_body.get("device_id")
+        self.assertIsInstance(device_id, str)