9
9
"encoding/json"
10
10
"fmt"
11
11
"net/http"
12
+ "net/url"
12
13
"strings"
13
14
"testing"
14
15
"time"
@@ -19,117 +20,272 @@ import (
19
20
20
21
"github.com/matrix-org/complement/internal/b"
21
22
"github.com/matrix-org/complement/internal/client"
23
+ "github.com/matrix-org/complement/internal/docker"
22
24
"github.com/matrix-org/complement/internal/federation"
23
25
"github.com/matrix-org/complement/internal/match"
26
+ "github.com/matrix-org/complement/internal/must"
24
27
)
25
28
26
- // TestSyncBlocksDuringPartialStateJoin tests that a regular /sync request
27
- // made during a partial-state /send_join request blocks until the state is
28
- // correctly synced.
29
- func TestSyncBlocksDuringPartialStateJoin (t * testing.T ) {
30
- // We make a room on the Complement server, then have @alice:hs1 join it,
31
- // and make a sync request while the resync is in flight
29
+ func TestPartialStateJoin (t * testing.T ) {
30
+ // test that a regular /sync request made during a partial-state /send_join
31
+ // request blocks until the state is correctly synced.
32
+ t .Run ("SyncBlocksDuringPartialStateJoin" , func (t * testing.T ) {
33
+ deployment := Deploy (t , b .BlueprintAlice )
34
+ defer deployment .Destroy (t )
35
+ alice := deployment .Client (t , "hs1" , "@alice:hs1" )
32
36
33
- deployment := Deploy (t , b . BlueprintAlice )
34
- defer deployment .Destroy (t )
37
+ psjResult := beginPartialStateJoin (t , deployment , alice )
38
+ defer psjResult .Destroy ()
35
39
36
- alice := deployment . Client ( t , "hs1" , "@alice:hs1" )
40
+ // Alice has now joined the room, and the server is syncing the state in the background.
37
41
38
- srv := federation .NewServer (t , deployment ,
42
+ // attempts to sync should now block. Fire off a goroutine to try it.
43
+ syncResponseChan := make (chan gjson.Result )
44
+ defer close (syncResponseChan )
45
+ go func () {
46
+ response , _ := alice .MustSync (t , client.SyncReq {})
47
+ syncResponseChan <- response
48
+ }()
49
+
50
+ // wait for the state_ids request to arrive
51
+ psjResult .AwaitStateIdsRequest (t )
52
+
53
+ // the client-side requests should still be waiting
54
+ select {
55
+ case <- syncResponseChan :
56
+ t .Fatalf ("Sync completed before state resync complete" )
57
+ default :
58
+ }
59
+
60
+ // release the federation /state response
61
+ psjResult .FinishStateRequest ()
62
+
63
+ // the /sync request should now complete, with the new room
64
+ var syncRes gjson.Result
65
+ select {
66
+ case <- time .After (1 * time .Second ):
67
+ t .Fatalf ("/sync request request did not complete" )
68
+ case syncRes = <- syncResponseChan :
69
+ }
70
+
71
+ roomRes := syncRes .Get ("rooms.join." + client .GjsonEscape (psjResult .ServerRoom .RoomID ))
72
+ if ! roomRes .Exists () {
73
+ t .Fatalf ("/sync completed without join to new room\n " )
74
+ }
75
+
76
+ // check that the state includes both charlie and derek.
77
+ matcher := match .JSONCheckOffAllowUnwanted ("state.events" ,
78
+ []interface {}{
79
+ "m.room.member|" + psjResult .Server .UserID ("charlie" ),
80
+ "m.room.member|" + psjResult .Server .UserID ("derek" ),
81
+ }, func (result gjson.Result ) interface {} {
82
+ return strings .Join ([]string {result .Map ()["type" ].Str , result .Map ()["state_key" ].Str }, "|" )
83
+ }, nil ,
84
+ )
85
+ if err := matcher ([]byte (roomRes .Raw )); err != nil {
86
+ t .Errorf ("Did not find expected state events in /sync response: %s" , err )
87
+
88
+ }
89
+ })
90
+
91
+ // when Alice does a lazy-loading sync, she should see the room immediately
92
+ t .Run ("CanLazyLoadingSyncDuringPartialStateJoin" , func (t * testing.T ) {
93
+ deployment := Deploy (t , b .BlueprintAlice )
94
+ defer deployment .Destroy (t )
95
+ alice := deployment .Client (t , "hs1" , "@alice:hs1" )
96
+
97
+ psjResult := beginPartialStateJoin (t , deployment , alice )
98
+ defer psjResult .Destroy ()
99
+
100
+ alice .MustSyncUntil (t ,
101
+ client.SyncReq {
102
+ Filter : buildLazyLoadingSyncFilter (),
103
+ },
104
+ client .SyncJoinedTo (alice .UserID , psjResult .ServerRoom .RoomID ),
105
+ )
106
+ t .Logf ("Alice successfully synced" )
107
+ })
108
+
109
+ // we should be able to send events in the room, during the resync
110
+ t .Run ("CanSendEventsDuringPartialStateJoin" , func (t * testing.T ) {
111
+ t .Skip ("Cannot yet send events during resync" )
112
+ deployment := Deploy (t , b .BlueprintAlice )
113
+ defer deployment .Destroy (t )
114
+ alice := deployment .Client (t , "hs1" , "@alice:hs1" )
115
+
116
+ psjResult := beginPartialStateJoin (t , deployment , alice )
117
+ defer psjResult .Destroy ()
118
+
119
+ alice .Client .Timeout = 2 * time .Second
120
+ paths := []string {"_matrix" , "client" , "r0" , "rooms" , psjResult .ServerRoom .RoomID , "send" , "m.room.message" , "0" }
121
+ res := alice .MustDoFunc (t , "PUT" , paths , client .WithJSONBody (t , map [string ]interface {}{
122
+ "msgtype" : "m.text" ,
123
+ "body" : "Hello world!" ,
124
+ }))
125
+ body := gjson .ParseBytes (client .ParseJSON (t , res ))
126
+ eventID := body .Get ("event_id" ).Str
127
+ t .Logf ("Alice sent event event ID %s" , eventID )
128
+ })
129
+
130
+ // a request to (client-side) /members?at= should block until the (federation) /state request completes
131
+ // TODO(faster_joins): also need to test /state, and /members without an `at`, which follow a different path
132
+ t .Run ("MembersRequestBlocksDuringPartialStateJoin" , func (t * testing.T ) {
133
+ deployment := Deploy (t , b .BlueprintAlice )
134
+ defer deployment .Destroy (t )
135
+ alice := deployment .Client (t , "hs1" , "@alice:hs1" )
136
+
137
+ psjResult := beginPartialStateJoin (t , deployment , alice )
138
+ defer psjResult .Destroy ()
139
+
140
+ // we need a sync token to pass to the `at` param.
141
+ syncToken := alice .MustSyncUntil (t ,
142
+ client.SyncReq {
143
+ Filter : buildLazyLoadingSyncFilter (),
144
+ },
145
+ client .SyncJoinedTo (alice .UserID , psjResult .ServerRoom .RoomID ),
146
+ )
147
+ t .Logf ("Alice successfully synced" )
148
+
149
+ // Fire off a goroutine to send the request, and write the response back to a channel.
150
+ clientMembersRequestResponseChan := make (chan * http.Response )
151
+ defer close (clientMembersRequestResponseChan )
152
+ go func () {
153
+ queryParams := url.Values {}
154
+ queryParams .Set ("at" , syncToken )
155
+ clientMembersRequestResponseChan <- alice .MustDoFunc (
156
+ t ,
157
+ "GET" ,
158
+ []string {"_matrix" , "client" , "r0" , "rooms" , psjResult .ServerRoom .RoomID , "members" },
159
+ client .WithQueries (queryParams ),
160
+ )
161
+ }()
162
+
163
+ // release the federation /state response
164
+ psjResult .FinishStateRequest ()
165
+
166
+ // the client-side /members request should now complete, with a response that includes charlie and derek.
167
+ select {
168
+ case <- time .After (1 * time .Second ):
169
+ t .Fatalf ("client-side /members request did not complete" )
170
+ case res := <- clientMembersRequestResponseChan :
171
+ must .MatchResponse (t , res , match.HTTPResponse {
172
+ JSON : []match.JSON {
173
+ match .JSONCheckOff ("chunk" ,
174
+ []interface {}{
175
+ "m.room.member|" + alice .UserID ,
176
+ "m.room.member|" + psjResult .Server .UserID ("charlie" ),
177
+ "m.room.member|" + psjResult .Server .UserID ("derek" ),
178
+ }, func (result gjson.Result ) interface {} {
179
+ return strings .Join ([]string {result .Map ()["type" ].Str , result .Map ()["state_key" ].Str }, "|" )
180
+ }, nil ),
181
+ },
182
+ })
183
+ }
184
+ })
185
+ }
186
+
187
+ // buildLazyLoadingSyncFilter constructs a json-marshalled filter suitable the 'Filter' field of a client.SyncReq
188
+ func buildLazyLoadingSyncFilter () string {
189
+ j , _ := json .Marshal (map [string ]interface {}{
190
+ "room" : map [string ]interface {}{
191
+ "timeline" : map [string ]interface {}{
192
+ "lazy_load_members" : true ,
193
+ },
194
+ "state" : map [string ]interface {}{
195
+ "lazy_load_members" : true ,
196
+ },
197
+ },
198
+ })
199
+ return string (j )
200
+ }
201
+
202
+ // partialStateJoinResult is the result of beginPartialStateJoin
203
+ type partialStateJoinResult struct {
204
+ cancelListener func ()
205
+ Server * federation.Server
206
+ ServerRoom * federation.ServerRoom
207
+ fedStateIdsRequestReceivedWaiter * Waiter
208
+ fedStateIdsSendResponseWaiter * Waiter
209
+ }
210
+
211
+ // beginPartialStateJoin spins up a room on a complement server,
212
+ // then has a test user join it. It returns a partialStateJoinResult,
213
+ // which must be Destroy'd on completion.
214
+ //
215
+ // When this method completes, the /join request will have completed, but the
216
+ // state has not yet been re-synced. To allow the re-sync to proceed, call
217
+ // partialStateJoinResult.FinishStateRequest.
218
+ func beginPartialStateJoin (t * testing.T , deployment * docker.Deployment , joiningUser * client.CSAPI ) partialStateJoinResult {
219
+ result := partialStateJoinResult {}
220
+ success := false
221
+ defer func () {
222
+ if ! success {
223
+ result .Destroy ()
224
+ }
225
+ }()
226
+
227
+ result .Server = federation .NewServer (t , deployment ,
39
228
federation .HandleKeyRequests (),
40
229
federation .HandlePartialStateMakeSendJoinRequests (),
41
230
federation .HandleEventRequests (),
42
231
)
43
- cancel := srv .Listen ()
44
- defer cancel ()
232
+ result .cancelListener = result .Server .Listen ()
45
233
46
234
// some things for orchestration
47
- fedStateIdsRequestReceivedWaiter := NewWaiter ()
48
- defer fedStateIdsRequestReceivedWaiter .Finish ()
49
- fedStateIdsSendResponseWaiter := NewWaiter ()
50
- defer fedStateIdsSendResponseWaiter .Finish ()
235
+ result .fedStateIdsRequestReceivedWaiter = NewWaiter ()
236
+ result .fedStateIdsSendResponseWaiter = NewWaiter ()
51
237
52
238
// create the room on the complement server, with charlie and derek as members
53
- charlie := srv .UserID ("charlie" )
54
- derek := srv .UserID ("derek" )
55
- serverRoom := makeTestRoom (t , srv , alice .GetDefaultRoomVersion (t ), charlie , derek )
239
+ roomVer := joiningUser .GetDefaultRoomVersion (t )
240
+ result .ServerRoom = result .Server .MustMakeRoom (t , roomVer , federation .InitialRoomEvents (roomVer , result .Server .UserID ("charlie" )))
241
+ result .ServerRoom .AddEvent (result .Server .MustCreateEvent (t , result .ServerRoom , b.Event {
242
+ Type : "m.room.member" ,
243
+ StateKey : b .Ptr (result .Server .UserID ("derek" )),
244
+ Sender : result .Server .UserID ("derek" ),
245
+ Content : map [string ]interface {}{
246
+ "membership" : "join" ,
247
+ },
248
+ }))
56
249
57
250
// register a handler for /state_ids requests, which finishes fedStateIdsRequestReceivedWaiter, then
58
251
// waits for fedStateIdsSendResponseWaiter and sends a reply
59
- handleStateIdsRequests (t , srv , serverRoom , fedStateIdsRequestReceivedWaiter , fedStateIdsSendResponseWaiter )
252
+ handleStateIdsRequests (t , result . Server , result . ServerRoom , result . fedStateIdsRequestReceivedWaiter , result . fedStateIdsSendResponseWaiter )
60
253
61
254
// a handler for /state requests, which sends a sensible response
62
- handleStateRequests (t , srv , serverRoom , nil , nil )
63
-
64
- // have alice join the room by room ID.
65
- alice .JoinRoom (t , serverRoom .RoomID , []string {srv .ServerName ()})
66
- t .Logf ("Join completed" )
255
+ handleStateRequests (t , result .Server , result .ServerRoom , nil , nil )
67
256
68
- // Alice has now joined the room, and the server is syncing the state in the background.
257
+ // have joiningUser join the room by room ID.
258
+ joiningUser .JoinRoom (t , result .ServerRoom .RoomID , []string {result .Server .ServerName ()})
259
+ t .Logf ("/join request completed" )
69
260
70
- // attempts to sync should now block. Fire off a goroutine to try it.
71
- syncResponseChan := make (chan gjson.Result )
72
- defer close (syncResponseChan )
73
- go func () {
74
- response , _ := alice .MustSync (t , client.SyncReq {})
75
- syncResponseChan <- response
76
- }()
77
-
78
- // wait for the state_ids request to arrive
79
- fedStateIdsRequestReceivedWaiter .Waitf (t , 5 * time .Second , "Waiting for /state_ids request" )
261
+ success = true
262
+ return result
263
+ }
80
264
81
- // the client-side requests should still be waiting
82
- select {
83
- case <- syncResponseChan :
84
- t . Fatalf ( "Sync completed before state resync complete" )
85
- default :
265
+ // Destroy cleans up the resources associated with the join attempt. It must
266
+ // be called once the test is finished
267
+ func ( psj * partialStateJoinResult ) Destroy () {
268
+ if psj . fedStateIdsSendResponseWaiter != nil {
269
+ psj . fedStateIdsSendResponseWaiter . Finish ()
86
270
}
87
271
88
- // release the federation /state response
89
- fedStateIdsSendResponseWaiter .Finish ()
90
-
91
- // the /sync request should now complete, with the new room
92
- var syncRes gjson.Result
93
- select {
94
- case <- time .After (1 * time .Second ):
95
- t .Fatalf ("/sync request request did not complete" )
96
- case syncRes = <- syncResponseChan :
272
+ if psj .fedStateIdsRequestReceivedWaiter != nil {
273
+ psj .fedStateIdsRequestReceivedWaiter .Finish ()
97
274
}
98
275
99
- roomRes := syncRes .Get ("rooms.join." + client .GjsonEscape (serverRoom .RoomID ))
100
- if ! roomRes .Exists () {
101
- t .Fatalf ("/sync completed without join to new room\n " )
276
+ if psj .cancelListener != nil {
277
+ psj .cancelListener ()
102
278
}
279
+ }
103
280
104
- // check that the state includes both charlie and derek.
105
- matcher := match .JSONCheckOffAllowUnwanted ("state.events" ,
106
- []interface {}{
107
- "m.room.member|" + charlie ,
108
- "m.room.member|" + derek ,
109
- }, func (result gjson.Result ) interface {} {
110
- return strings .Join ([]string {result .Map ()["type" ].Str , result .Map ()["state_key" ].Str }, "|" )
111
- }, nil ,
112
- )
113
- if err := matcher ([]byte (roomRes .Raw )); err != nil {
114
- t .Errorf ("Did not find expected state events in /sync response: %s" , err )
115
- }
281
+ // wait for a /state_ids request for the test room to arrive
282
+ func (psj * partialStateJoinResult ) AwaitStateIdsRequest (t * testing.T ) {
283
+ psj .fedStateIdsRequestReceivedWaiter .Waitf (t , 5 * time .Second , "Waiting for /state_ids request" )
116
284
}
117
285
118
- // makeTestRoom constructs a test room on the Complement server, and adds the given extra members
119
- func makeTestRoom (t * testing.T , srv * federation.Server , roomVer gomatrixserverlib.RoomVersion , creator string , members ... string ) * federation.ServerRoom {
120
- serverRoom := srv .MustMakeRoom (t , roomVer , federation .InitialRoomEvents (roomVer , creator ))
121
- for _ , m := range members {
122
- serverRoom .AddEvent (srv .MustCreateEvent (t , serverRoom , b.Event {
123
- Type : "m.room.member" ,
124
- StateKey : b .Ptr (m ),
125
- Sender : m ,
126
- Content : map [string ]interface {}{
127
- "membership" : "join" ,
128
- },
129
- }),
130
- )
131
- }
132
- return serverRoom
286
+ // allow the /state_ids request to complete, thus allowing the state re-sync to complete
287
+ func (psj * partialStateJoinResult ) FinishStateRequest () {
288
+ psj .fedStateIdsSendResponseWaiter .Finish ()
133
289
}
134
290
135
291
// handleStateIdsRequests registers a handler for /state_ids requests for serverRoom.
0 commit comments