Skip to content

Commit 7249a8c

Browse files
committed
Add tests for thread subscription endpoints
1 parent 01b4002 commit 7249a8c

File tree

1 file changed

+258
-0
lines changed

1 file changed

+258
-0
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#
2+
# This file is licensed under the Affero General Public License (AGPL) version 3.
3+
#
4+
# Copyright (C) 2025 New Vector, Ltd
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Affero General Public License as
8+
# published by the Free Software Foundation, either version 3 of the
9+
# License, or (at your option) any later version.
10+
#
11+
# See the GNU Affero General Public License for more details:
12+
# <https://www.gnu.org/licenses/agpl-3.0.html>.
13+
14+
from http import HTTPStatus
15+
from typing import override
16+
17+
from twisted.test.proto_helpers import MemoryReactor
18+
19+
from synapse.rest import admin
20+
from synapse.rest.client import login, profile, room, thread_subscriptions
21+
from synapse.server import HomeServer
22+
from synapse.types import JsonDict
23+
from synapse.util import Clock
24+
25+
from tests import unittest
26+
27+
PREFIX = "/_matrix/client/unstable/io.element.msc4306.thread_subscriptions"
28+
29+
30+
class ThreadSubscriptionsTestCase(unittest.HomeserverTestCase):
31+
servlets = [
32+
admin.register_servlets_for_client_rest_resource,
33+
login.register_servlets,
34+
profile.register_servlets,
35+
room.register_servlets,
36+
thread_subscriptions.register_servlets,
37+
]
38+
39+
@override
40+
def default_config(self) -> JsonDict:
41+
config = super().default_config()
42+
config["experimental_features"] = {"msc4306_enabled": True}
43+
return config
44+
45+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
46+
self.user_id = self.register_user("user", "password")
47+
self.token = self.login("user", "password")
48+
self.other_user_id = self.register_user("other_user", "password")
49+
self.other_token = self.login("other_user", "password")
50+
51+
# Create a room and send a message to use as a thread root
52+
self.room_id = self.helper.create_room_as(self.user_id, tok=self.token)
53+
self.helper.join(self.room_id, self.other_user_id, tok=self.other_token)
54+
response = self.helper.send(self.room_id, body="Root message", tok=self.token)
55+
self.root_event_id = response["event_id"]
56+
57+
# Send a message in the thread
58+
self.helper.send_event(
59+
room_id=self.room_id,
60+
type="m.room.message",
61+
content={
62+
"body": "Thread message",
63+
"msgtype": "m.text",
64+
"m.relates_to": {
65+
"rel_type": "m.thread",
66+
"event_id": self.root_event_id,
67+
},
68+
},
69+
tok=self.token,
70+
)
71+
72+
def test_get_thread_subscription_unsubscribed(self) -> None:
73+
"""Test retrieving thread subscription when not subscribed."""
74+
channel = self.make_request(
75+
"GET",
76+
f"{PREFIX}/{self.root_event_id}",
77+
access_token=self.token,
78+
)
79+
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
80+
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
81+
82+
def test_get_thread_subscription_nonexistent_thread(self) -> None:
83+
"""Test retrieving subscription settings for a nonexistent thread."""
84+
channel = self.make_request(
85+
"GET",
86+
f"{PREFIX}/$nonexistent:example.org",
87+
access_token=self.token,
88+
)
89+
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
90+
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
91+
92+
def test_get_thread_subscription_no_access(self) -> None:
93+
"""Test that a user can't get thread subscription for a thread they can't access."""
94+
self.register_user("no_access", "password")
95+
no_access_token = self.login("no_access", "password")
96+
97+
channel = self.make_request(
98+
"GET",
99+
f"{PREFIX}/{self.root_event_id}",
100+
access_token=no_access_token,
101+
)
102+
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
103+
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
104+
105+
def test_subscribe_manual_then_automatic(self) -> None:
106+
"""Test subscribing to a thread, first a manual subscription then an automatic subscription.
107+
The manual subscription wins over the automatic one."""
108+
channel = self.make_request(
109+
"PUT",
110+
f"{PREFIX}/{self.root_event_id}",
111+
{
112+
"automatic": False,
113+
},
114+
access_token=self.token,
115+
)
116+
self.assertEqual(channel.code, HTTPStatus.OK)
117+
118+
# Assert the subscription was saved
119+
channel = self.make_request(
120+
"GET",
121+
f"{PREFIX}/{self.root_event_id}",
122+
access_token=self.token,
123+
)
124+
self.assertEqual(channel.code, HTTPStatus.OK)
125+
self.assertEqual(channel.json_body, {"automatic": False})
126+
127+
# Now also register an automatic subscription; it should not
128+
# override the manual subscription
129+
channel = self.make_request(
130+
"PUT",
131+
f"{PREFIX}/{self.root_event_id}",
132+
{"automatic": True},
133+
access_token=self.token,
134+
)
135+
self.assertEqual(channel.code, HTTPStatus.OK)
136+
137+
# Assert the manual subscription was not overridden
138+
channel = self.make_request(
139+
"GET",
140+
f"{PREFIX}/{self.root_event_id}",
141+
access_token=self.token,
142+
)
143+
self.assertEqual(channel.code, HTTPStatus.OK)
144+
self.assertEqual(channel.json_body, {"automatic": False})
145+
146+
def test_subscribe_automatic_then_manual(self) -> None:
147+
"""Test subscribing to a thread, first an automatic subscription then a manual subscription.
148+
The manual subscription wins over the automatic one."""
149+
channel = self.make_request(
150+
"PUT",
151+
f"{PREFIX}/{self.root_event_id}",
152+
{
153+
"automatic": True,
154+
},
155+
access_token=self.token,
156+
)
157+
self.assertEqual(channel.code, HTTPStatus.OK)
158+
159+
# Assert the subscription was saved
160+
channel = self.make_request(
161+
"GET",
162+
f"{PREFIX}/{self.root_event_id}",
163+
access_token=self.token,
164+
)
165+
self.assertEqual(channel.code, HTTPStatus.OK)
166+
self.assertEqual(channel.json_body, {"automatic": True})
167+
168+
# Now also register a manual subscription
169+
channel = self.make_request(
170+
"PUT",
171+
f"{PREFIX}/{self.root_event_id}",
172+
{"automatic": False},
173+
access_token=self.token,
174+
)
175+
self.assertEqual(channel.code, HTTPStatus.OK)
176+
177+
# Assert the manual subscription was not overridden
178+
channel = self.make_request(
179+
"GET",
180+
f"{PREFIX}/{self.root_event_id}",
181+
access_token=self.token,
182+
)
183+
self.assertEqual(channel.code, HTTPStatus.OK)
184+
self.assertEqual(channel.json_body, {"automatic": False})
185+
186+
def test_unsubscribe(self) -> None:
187+
"""Test subscribing to a thread, then unsubscribing."""
188+
channel = self.make_request(
189+
"PUT",
190+
f"{PREFIX}/{self.root_event_id}",
191+
{
192+
"automatic": True,
193+
},
194+
access_token=self.token,
195+
)
196+
self.assertEqual(channel.code, HTTPStatus.OK)
197+
198+
# Assert the subscription was saved
199+
channel = self.make_request(
200+
"GET",
201+
f"{PREFIX}/{self.root_event_id}",
202+
access_token=self.token,
203+
)
204+
self.assertEqual(channel.code, HTTPStatus.OK)
205+
self.assertEqual(channel.json_body, {"automatic": True})
206+
207+
# Now also register a manual subscription
208+
channel = self.make_request(
209+
"DELETE",
210+
f"{PREFIX}/{self.root_event_id}",
211+
access_token=self.token,
212+
)
213+
self.assertEqual(channel.code, HTTPStatus.OK)
214+
215+
# Assert the manual subscription was not overridden
216+
channel = self.make_request(
217+
"GET",
218+
f"{PREFIX}/{self.root_event_id}",
219+
access_token=self.token,
220+
)
221+
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
222+
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
223+
224+
def test_set_thread_subscription_nonexistent_thread(self) -> None:
225+
"""Test setting subscription settings for a nonexistent thread."""
226+
channel = self.make_request(
227+
"PUT",
228+
f"{PREFIX}/$nonexistent:example.org",
229+
{"automatic": True},
230+
access_token=self.token,
231+
)
232+
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
233+
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
234+
235+
def test_set_thread_subscription_no_access(self) -> None:
236+
"""Test that a user can't set thread subscription for a thread they can't access."""
237+
self.register_user("no_access2", "password")
238+
no_access_token = self.login("no_access2", "password")
239+
240+
channel = self.make_request(
241+
"PUT",
242+
f"{PREFIX}/$nonexistent:example.org",
243+
{"automatic": True},
244+
access_token=no_access_token,
245+
)
246+
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
247+
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
248+
249+
def test_invalid_body(self) -> None:
250+
"""Test that sending invalid subscription settings is rejected."""
251+
channel = self.make_request(
252+
"PUT",
253+
f"{PREFIX}/{self.root_event_id}",
254+
# non-boolean `automatic`
255+
{"automatic": "true"},
256+
access_token=self.token,
257+
)
258+
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST)

0 commit comments

Comments
 (0)