Skip to content

Commit 468c0a4

Browse files
committed
Add tests for the rendezvous implementation
1 parent 4db974d commit 468c0a4

File tree

1 file changed

+336
-1
lines changed

1 file changed

+336
-1
lines changed

tests/rest/client/test_rendezvous.py

Lines changed: 336 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is licensed under the Affero General Public License (AGPL) version 3.
33
#
44
# Copyright 2022 The Matrix.org Foundation C.I.C.
5-
# Copyright (C) 2023 New Vector, Ltd
5+
# Copyright (C) 2023-2024 New Vector, Ltd
66
#
77
# This program is free software: you can redistribute it and/or modify
88
# it under the terms of the GNU Affero General Public License as
@@ -19,9 +19,14 @@
1919
#
2020
#
2121

22+
from typing import Dict
23+
from urllib.parse import urlparse
24+
2225
from twisted.test.proto_helpers import MemoryReactor
26+
from twisted.web.resource import Resource
2327

2428
from synapse.rest.client import rendezvous
29+
from synapse.rest.synapse.client.rendezvous import MSC4108RendezvousSessionResource
2530
from synapse.server import HomeServer
2631
from synapse.util import Clock
2732

@@ -41,6 +46,12 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
4146
self.hs = self.setup_test_homeserver()
4247
return self.hs
4348

49+
def create_resource_dict(self) -> Dict[str, Resource]:
50+
return {
51+
**super().create_resource_dict(),
52+
"/_synapse/client/rendezvous": MSC4108RendezvousSessionResource(self.hs),
53+
}
54+
4455
def test_disabled(self) -> None:
4556
channel = self.make_request("POST", msc3886_endpoint, {}, access_token=None)
4657
self.assertEqual(channel.code, 404)
@@ -73,3 +84,327 @@ def test_msc4108_delegation(self) -> None:
7384
channel = self.make_request("POST", msc4108_endpoint, {}, access_token=None)
7485
self.assertEqual(channel.code, 307)
7586
self.assertEqual(channel.headers.getRawHeaders("Location"), ["https://asd"])
87+
88+
@override_config(
89+
{
90+
"disable_registration": True,
91+
"experimental_features": {
92+
"msc4108_enabled": True,
93+
"msc3861": {
94+
"enabled": True,
95+
"issuer": "https://issuer",
96+
"client_id": "client_id",
97+
"client_auth_method": "client_secret_post",
98+
"client_secret": "client_secret",
99+
"admin_token": "admin_token_value",
100+
},
101+
},
102+
}
103+
)
104+
def test_msc4108(self) -> None:
105+
"""
106+
Test the MSC4108 rendezvous endpoint, including:
107+
- Creating a session
108+
- Getting the data back
109+
- Updating the data
110+
- Deleting the data
111+
- ETag handling
112+
"""
113+
# We can post arbitrary data to the endpoint
114+
channel = self.make_request(
115+
"POST",
116+
msc4108_endpoint,
117+
"foo=bar",
118+
# This sets the content type to application/x-www-form-urlencoded
119+
content_is_form=True,
120+
access_token=None,
121+
)
122+
self.assertEqual(channel.code, 201)
123+
self.assertSubstring("/_synapse/client/rendezvous/", channel.json_body["url"])
124+
headers = dict(channel.headers.getAllRawHeaders())
125+
self.assertIn(b"ETag", headers)
126+
self.assertIn(b"Expires", headers)
127+
self.assertEqual(headers[b"Content-Type"], [b"application/json"])
128+
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
129+
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
130+
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
131+
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
132+
self.assertIn("url", channel.json_body)
133+
self.assertTrue(channel.json_body["url"].startswith("https://"))
134+
135+
url = urlparse(channel.json_body["url"])
136+
session_endpoint = url.path
137+
etag = headers[b"ETag"][0]
138+
139+
# We can get the data back
140+
channel = self.make_request(
141+
"GET",
142+
session_endpoint,
143+
access_token=None,
144+
)
145+
146+
self.assertEqual(channel.code, 200)
147+
headers = dict(channel.headers.getAllRawHeaders())
148+
self.assertEqual(headers[b"ETag"], [etag])
149+
self.assertIn(b"Expires", headers)
150+
self.assertEqual(
151+
headers[b"Content-Type"], [b"application/x-www-form-urlencoded"]
152+
)
153+
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
154+
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
155+
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
156+
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
157+
self.assertEqual(channel.text_body, "foo=bar")
158+
159+
# We can make sure the data hasn't changed
160+
channel = self.make_request(
161+
"GET",
162+
session_endpoint,
163+
access_token=None,
164+
custom_headers=[("If-None-Match", etag)],
165+
)
166+
167+
self.assertEqual(channel.code, 304)
168+
169+
# We can update the data
170+
channel = self.make_request(
171+
"PUT",
172+
session_endpoint,
173+
"foo=baz",
174+
content_is_form=True,
175+
access_token=None,
176+
custom_headers=[("If-Match", etag)],
177+
)
178+
179+
self.assertEqual(channel.code, 202)
180+
headers = dict(channel.headers.getAllRawHeaders())
181+
old_etag = etag
182+
new_etag = headers[b"ETag"][0]
183+
184+
# If we try to update it again with the old etag, it should fail
185+
channel = self.make_request(
186+
"PUT",
187+
session_endpoint,
188+
"bar=baz",
189+
content_is_form=True,
190+
access_token=None,
191+
custom_headers=[("If-Match", old_etag)],
192+
)
193+
194+
self.assertEqual(channel.code, 412)
195+
self.assertEqual(channel.json_body["errcode"], "M_CONCURRENT_WRITE")
196+
197+
# If we try to get with the old etag, we should get the updated data
198+
channel = self.make_request(
199+
"GET",
200+
session_endpoint,
201+
access_token=None,
202+
custom_headers=[("If-None-Match", old_etag)],
203+
)
204+
205+
self.assertEqual(channel.code, 200)
206+
headers = dict(channel.headers.getAllRawHeaders())
207+
self.assertEqual(headers[b"ETag"], [new_etag])
208+
self.assertEqual(channel.text_body, "foo=baz")
209+
210+
# We can delete the data
211+
channel = self.make_request(
212+
"DELETE",
213+
session_endpoint,
214+
access_token=None,
215+
)
216+
217+
self.assertEqual(channel.code, 204)
218+
219+
# If we try to get the data again, it should fail
220+
channel = self.make_request(
221+
"GET",
222+
session_endpoint,
223+
access_token=None,
224+
)
225+
226+
self.assertEqual(channel.code, 404)
227+
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
228+
229+
@override_config(
230+
{
231+
"disable_registration": True,
232+
"experimental_features": {
233+
"msc4108_enabled": True,
234+
"msc3861": {
235+
"enabled": True,
236+
"issuer": "https://issuer",
237+
"client_id": "client_id",
238+
"client_auth_method": "client_secret_post",
239+
"client_secret": "client_secret",
240+
"admin_token": "admin_token_value",
241+
},
242+
},
243+
}
244+
)
245+
def test_msc4108_expiration(self) -> None:
246+
"""
247+
Test that entries are evicted after a TTL.
248+
"""
249+
# Start a new session
250+
channel = self.make_request(
251+
"POST",
252+
msc4108_endpoint,
253+
"foo=bar",
254+
content_is_form=True,
255+
access_token=None,
256+
)
257+
self.assertEqual(channel.code, 201)
258+
session_endpoint = urlparse(channel.json_body["url"]).path
259+
260+
# Sanity check that we can get the data back
261+
channel = self.make_request(
262+
"GET",
263+
session_endpoint,
264+
access_token=None,
265+
)
266+
self.assertEqual(channel.code, 200)
267+
self.assertEqual(channel.text_body, "foo=bar")
268+
269+
# Advance the clock, TTL of entries is 5 minutes
270+
self.reactor.advance(300)
271+
272+
# Get the data back, it should be gone
273+
channel = self.make_request(
274+
"GET",
275+
session_endpoint,
276+
access_token=None,
277+
)
278+
self.assertEqual(channel.code, 404)
279+
280+
@override_config(
281+
{
282+
"disable_registration": True,
283+
"experimental_features": {
284+
"msc4108_enabled": True,
285+
"msc3861": {
286+
"enabled": True,
287+
"issuer": "https://issuer",
288+
"client_id": "client_id",
289+
"client_auth_method": "client_secret_post",
290+
"client_secret": "client_secret",
291+
"admin_token": "admin_token_value",
292+
},
293+
},
294+
}
295+
)
296+
def test_msc4108_capacity(self) -> None:
297+
"""
298+
Test that a capacity limit is enforced on the rendezvous sessions, as old
299+
entries are evicted at an interval when the limit is reached.
300+
"""
301+
# Start a new session
302+
channel = self.make_request(
303+
"POST",
304+
msc4108_endpoint,
305+
"foo=bar",
306+
content_is_form=True,
307+
access_token=None,
308+
)
309+
self.assertEqual(channel.code, 201)
310+
session_endpoint = urlparse(channel.json_body["url"]).path
311+
312+
# Sanity check that we can get the data back
313+
channel = self.make_request(
314+
"GET",
315+
session_endpoint,
316+
access_token=None,
317+
)
318+
self.assertEqual(channel.code, 200)
319+
self.assertEqual(channel.text_body, "foo=bar")
320+
321+
# Start a lot of new sessions
322+
for _ in range(100):
323+
channel = self.make_request(
324+
"POST",
325+
msc4108_endpoint,
326+
"foo=bar",
327+
content_is_form=True,
328+
access_token=None,
329+
)
330+
self.assertEqual(channel.code, 201)
331+
332+
# Get the data back, it should still be there, as the eviction hasn't run yet
333+
channel = self.make_request(
334+
"GET",
335+
session_endpoint,
336+
access_token=None,
337+
)
338+
339+
self.assertEqual(channel.code, 200)
340+
341+
# Advance the clock, as it will trigger the eviction
342+
self.reactor.advance(1)
343+
344+
# Get the data back, it should be gone
345+
channel = self.make_request(
346+
"GET",
347+
session_endpoint,
348+
access_token=None,
349+
)
350+
351+
@override_config(
352+
{
353+
"disable_registration": True,
354+
"experimental_features": {
355+
"msc4108_enabled": True,
356+
"msc3861": {
357+
"enabled": True,
358+
"issuer": "https://issuer",
359+
"client_id": "client_id",
360+
"client_auth_method": "client_secret_post",
361+
"client_secret": "client_secret",
362+
"admin_token": "admin_token_value",
363+
},
364+
},
365+
}
366+
)
367+
def test_msc4108_hard_capacity(self) -> None:
368+
"""
369+
Test that a hard capacity limit is enforced on the rendezvous sessions, as old
370+
entries are evicted immediately when the limit is reached.
371+
"""
372+
# Start a new session
373+
channel = self.make_request(
374+
"POST",
375+
msc4108_endpoint,
376+
"foo=bar",
377+
content_is_form=True,
378+
access_token=None,
379+
)
380+
self.assertEqual(channel.code, 201)
381+
session_endpoint = urlparse(channel.json_body["url"]).path
382+
383+
# Sanity check that we can get the data back
384+
channel = self.make_request(
385+
"GET",
386+
session_endpoint,
387+
access_token=None,
388+
)
389+
self.assertEqual(channel.code, 200)
390+
self.assertEqual(channel.text_body, "foo=bar")
391+
392+
# Start a lot of new sessions
393+
for _ in range(200):
394+
channel = self.make_request(
395+
"POST",
396+
msc4108_endpoint,
397+
"foo=bar",
398+
content_is_form=True,
399+
access_token=None,
400+
)
401+
self.assertEqual(channel.code, 201)
402+
403+
# Get the data back, it should already be gone as we hit the hard limit
404+
channel = self.make_request(
405+
"GET",
406+
session_endpoint,
407+
access_token=None,
408+
)
409+
410+
self.assertEqual(channel.code, 404)

0 commit comments

Comments
 (0)