Skip to content

Commit 16fd3bf

Browse files
committed
MSC4306: Thread Subscriptions Tests
1 parent dd9b896 commit 16fd3bf

2 files changed

Lines changed: 341 additions & 0 deletions

File tree

tests/msc4306/main_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/matrix-org/complement"
7+
)
8+
9+
func TestMain(m *testing.M) {
10+
complement.TestMain(m, "msc4306")
11+
}
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/matrix-org/complement"
7+
"github.com/matrix-org/complement/b"
8+
"github.com/matrix-org/complement/client"
9+
"github.com/matrix-org/complement/helpers"
10+
"github.com/matrix-org/complement/match"
11+
"github.com/matrix-org/complement/must"
12+
)
13+
14+
func TestThreadSubscriptions(t *testing.T) {
15+
deployment := complement.Deploy(t, 1)
16+
defer deployment.Destroy(t)
17+
18+
alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{})
19+
20+
t.Run("Can subscribe to and unsubscribe from a thread", func(t *testing.T) {
21+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
22+
threadRootID := alice.SendEventSynced(t, roomID, b.Event{
23+
Type: "m.room.message",
24+
Content: map[string]interface{}{
25+
"msgtype": "m.text",
26+
"body": "what do you think? reply in a thread!",
27+
},
28+
})
29+
30+
// Subscribe to the thread manually
31+
alice.MustDo(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{}))
32+
33+
must.MatchResponse(t, alice.MustDo(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}), match.HTTPResponse{
34+
JSON: []match.JSON{
35+
match.JSONKeyEqual("automatic", false),
36+
},
37+
})
38+
39+
// Unsubscribe from the thread
40+
alice.MustDo(t, "DELETE", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"})
41+
42+
must.MatchResponse(t, alice.Do(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}), match.HTTPResponse{
43+
StatusCode: 404,
44+
JSON: []match.JSON{
45+
match.JSONKeyEqual("errcode", "M_NOT_FOUND"),
46+
},
47+
})
48+
})
49+
50+
t.Run("Cannot use thread root as automatic subscription cause event", func(t *testing.T) {
51+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
52+
threadRootID := alice.SendEventSynced(t, roomID, b.Event{
53+
Type: "m.room.message",
54+
Content: map[string]interface{}{
55+
"msgtype": "m.text",
56+
"body": "what do you think? reply in a thread!",
57+
},
58+
})
59+
60+
response := alice.Do(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
61+
"automatic": threadRootID,
62+
}))
63+
64+
must.MatchResponse(t, response, match.HTTPResponse{
65+
StatusCode: 400,
66+
JSON: []match.JSON{
67+
match.JSONKeyEqual("errcode", "IO.ELEMENT.MSC4306.M_NOT_IN_THREAD"),
68+
},
69+
})
70+
})
71+
72+
t.Run("Can create automatic subscription to a thread", func(t *testing.T) {
73+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
74+
threadRootID := alice.SendEventSynced(t, roomID, b.Event{
75+
Type: "m.room.message",
76+
Content: map[string]interface{}{
77+
"msgtype": "m.text",
78+
"body": "what do you think? reply in a thread!",
79+
},
80+
})
81+
82+
// Create a message in the thread
83+
threadReplyID := alice.SendEventSynced(t, roomID, b.Event{
84+
Type: "m.room.message",
85+
Content: map[string]interface{}{
86+
"msgtype": "m.text",
87+
"body": "this is a reply",
88+
"m.relates_to": map[string]interface{}{
89+
"rel_type": "m.thread",
90+
"event_id": threadRootID,
91+
},
92+
},
93+
})
94+
95+
alice.MustDo(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
96+
"automatic": threadReplyID,
97+
}))
98+
99+
must.MatchResponse(t, alice.MustDo(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}), match.HTTPResponse{
100+
JSON: []match.JSON{
101+
match.JSONKeyEqual("automatic", true),
102+
},
103+
})
104+
})
105+
106+
t.Run("Manual subscriptions overwrite automatic subscriptions", func(t *testing.T) {
107+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
108+
threadRootID := alice.SendEventSynced(t, roomID, b.Event{
109+
Type: "m.room.message",
110+
Content: map[string]interface{}{"body": "Thread Root", "msgtype": "m.text"},
111+
})
112+
113+
// Create a message in the thread
114+
threadReplyID := alice.SendEventSynced(t, roomID, b.Event{
115+
Type: "m.room.message",
116+
Content: map[string]interface{}{
117+
"body": "Thread Reply",
118+
"msgtype": "m.text",
119+
"m.relates_to": map[string]interface{}{
120+
"rel_type": "m.thread",
121+
"event_id": threadRootID,
122+
},
123+
},
124+
})
125+
126+
// Create automatic subscription first
127+
alice.MustDo(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
128+
"automatic": threadReplyID,
129+
}))
130+
131+
// Then create manual subscription
132+
alice.MustDo(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{}))
133+
134+
must.MatchResponse(t, alice.MustDo(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}), match.HTTPResponse{
135+
JSON: []match.JSON{
136+
match.JSONKeyEqual("automatic", false),
137+
},
138+
})
139+
})
140+
141+
t.Run("Error when using invalid automatic event ID", func(t *testing.T) {
142+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
143+
threadRootID := alice.SendEventSynced(t, roomID, b.Event{
144+
Type: "m.room.message",
145+
Content: map[string]interface{}{"body": "Thread Root", "msgtype": "m.text"},
146+
})
147+
otherThreadRootID := alice.SendEventSynced(t, roomID, b.Event{
148+
Type: "m.room.message",
149+
Content: map[string]interface{}{"body": "another thread root", "msgtype": "m.text"},
150+
})
151+
152+
// Send message, but *not* in the right thread
153+
otherThreadReplyID := alice.SendEventSynced(t, roomID, b.Event{
154+
Type: "m.room.message",
155+
Content: map[string]interface{}{
156+
"body": "Not in the same thread",
157+
"msgtype": "m.text",
158+
"m.relates_to": map[string]interface{}{
159+
"rel_type": "m.thread",
160+
"event_id": otherThreadRootID,
161+
},
162+
},
163+
})
164+
165+
// We can't create an automatic subscription to the first thread with
166+
// the message in the wrong thread
167+
response := alice.Do(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
168+
"automatic": otherThreadReplyID,
169+
}))
170+
171+
must.MatchResponse(t, response, match.HTTPResponse{
172+
StatusCode: 400,
173+
JSON: []match.JSON{
174+
match.JSONKeyEqual("errcode", "IO.ELEMENT.MSC4306.M_NOT_IN_THREAD"),
175+
},
176+
})
177+
})
178+
179+
// Tests idempotence
180+
t.Run("Unsubscribe succeeds even with no subscription", func(t *testing.T) {
181+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
182+
threadRootID := alice.SendEventSynced(t, roomID, b.Event{
183+
Type: "m.room.message",
184+
Content: map[string]interface{}{"body": "Thread Root", "msgtype": "m.text"},
185+
})
186+
187+
// Unsubscribe, but without being subscribed first
188+
response := alice.Do(t, "DELETE", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"})
189+
190+
must.MatchResponse(t, response, match.HTTPResponse{
191+
StatusCode: 200,
192+
})
193+
})
194+
195+
t.Run("Nonexistent threads return 404", func(t *testing.T) {
196+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
197+
nonExistentID := "$notathread:example.org"
198+
199+
// try PUT
200+
response := alice.Do(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", nonExistentID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{}))
201+
must.MatchResponse(t, response, match.HTTPResponse{
202+
StatusCode: 404,
203+
})
204+
205+
// try GET
206+
response = alice.Do(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", nonExistentID, "subscription"})
207+
must.MatchResponse(t, response, match.HTTPResponse{
208+
StatusCode: 404,
209+
})
210+
})
211+
212+
t.Run("Server-side automatic subscription ordering conflict", func(t *testing.T) {
213+
/*
214+
The desired timeline of events and subscriptions is as such:
215+
216+
1. threadRoot
217+
2. threadReply1
218+
3. auto-subscribe
219+
4. threadReply2
220+
5. unsubscribe
221+
4. threadReply3
222+
6. try to auto-subscribe using threadReply1: denied
223+
7. try to auto-subscribe using threadReply2: denied
224+
8. try to auto-subscribe using threadReply3: OK
225+
*/
226+
227+
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
228+
229+
// 1. Create a thread root message
230+
threadRootID := alice.SendEventSynced(t, roomID, b.Event{
231+
Type: "m.room.message",
232+
Content: map[string]interface{}{"body": "Thread Root", "msgtype": "m.text"},
233+
})
234+
235+
// 2.
236+
threadReply1ID := alice.SendEventSynced(t, roomID, b.Event{
237+
Type: "m.room.message",
238+
Content: map[string]interface{}{
239+
"body": "Thread Reply 1",
240+
"msgtype": "m.text",
241+
"m.relates_to": map[string]interface{}{
242+
"rel_type": "m.thread",
243+
"event_id": threadRootID,
244+
},
245+
},
246+
})
247+
248+
// 3.
249+
alice.MustDo(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
250+
"automatic": threadReply1ID,
251+
}))
252+
253+
// 4.
254+
threadReply2ID := alice.SendEventSynced(t, roomID, b.Event{
255+
Type: "m.room.message",
256+
Content: map[string]interface{}{
257+
"body": "Thread Reply 2",
258+
"msgtype": "m.text",
259+
"m.relates_to": map[string]interface{}{
260+
"rel_type": "m.thread",
261+
"event_id": threadRootID,
262+
},
263+
},
264+
})
265+
266+
// 5.
267+
alice.MustDo(t, "DELETE", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"})
268+
269+
// 6.
270+
threadReply3ID := alice.SendEventSynced(t, roomID, b.Event{
271+
Type: "m.room.message",
272+
Content: map[string]interface{}{
273+
"body": "Thread Reply 3",
274+
"msgtype": "m.text",
275+
"m.relates_to": map[string]interface{}{
276+
"rel_type": "m.thread",
277+
"event_id": threadRootID,
278+
},
279+
},
280+
})
281+
282+
// 7.
283+
response := alice.Do(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
284+
"automatic": threadReply1ID,
285+
}))
286+
287+
must.MatchResponse(t, response, match.HTTPResponse{
288+
StatusCode: 409,
289+
JSON: []match.JSON{
290+
match.JSONKeyEqual("errcode", "IO.ELEMENT.MSC4306.M_CONFLICTING_UNSUBSCRIPTION"),
291+
},
292+
})
293+
294+
response = alice.Do(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"})
295+
must.MatchResponse(t, response, match.HTTPResponse{
296+
StatusCode: 404,
297+
})
298+
299+
// 8.
300+
response = alice.Do(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
301+
"automatic": threadReply2ID,
302+
}))
303+
304+
must.MatchResponse(t, response, match.HTTPResponse{
305+
StatusCode: 409,
306+
JSON: []match.JSON{
307+
match.JSONKeyEqual("errcode", "IO.ELEMENT.MSC4306.M_CONFLICTING_UNSUBSCRIPTION"),
308+
},
309+
})
310+
311+
response = alice.Do(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"})
312+
must.MatchResponse(t, response, match.HTTPResponse{
313+
StatusCode: 404,
314+
})
315+
316+
// 9.
317+
response = alice.Do(t, "PUT", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"}, client.WithJSONBody(t, map[string]interface{}{
318+
"automatic": threadReply3ID,
319+
}))
320+
321+
must.MatchResponse(t, response, match.HTTPResponse{
322+
StatusCode: 200,
323+
})
324+
325+
response = alice.Do(t, "GET", []string{"_matrix", "client", "unstable", "io.element.msc4306", "rooms", roomID, "thread", threadRootID, "subscription"})
326+
must.MatchResponse(t, response, match.HTTPResponse{
327+
StatusCode: 200,
328+
})
329+
})
330+
}

0 commit comments

Comments
 (0)