1
1
package csapi_tests
2
2
3
3
import (
4
+ "fmt"
5
+ "testing"
6
+
4
7
"github.com/matrix-org/complement/internal/b"
5
8
"github.com/matrix-org/complement/internal/client"
9
+ "github.com/matrix-org/complement/internal/must"
6
10
"github.com/matrix-org/complement/runtime"
7
11
"github.com/tidwall/gjson"
8
- "testing"
9
12
)
10
13
11
14
// TestTxnInEvent checks that the transaction ID is present when getting the event from the /rooms/{roomID}/event/{eventID} endpoint.
@@ -22,20 +25,191 @@ func TestTxnInEvent(t *testing.T) {
22
25
// Create a room where we can send events.
23
26
roomID := c .CreateRoom (t , map [string ]interface {}{})
24
27
28
+ txnId := "abcdefg"
25
29
// Let's send an event, and wait for it to appear in the timeline.
26
- eventID := c .SendEventSynced (t , roomID , b.Event {
30
+ eventID := c .SendEventUnsyncedWithTxnID (t , roomID , b.Event {
27
31
Type : "m.room.message" ,
28
32
Content : map [string ]interface {}{
29
33
"msgtype" : "m.text" ,
30
34
"body" : "first" ,
31
35
},
32
- })
36
+ }, txnId )
33
37
34
38
// The transaction ID should be present on the GET /rooms/{roomID}/event/{eventID} response.
35
39
res := c .MustDoFunc (t , "GET" , []string {"_matrix" , "client" , "v3" , "rooms" , roomID , "event" , eventID })
36
40
body := client .ParseJSON (t , res )
37
41
result := gjson .ParseBytes (body )
38
- if ! result .Get ("unsigned.transaction_id" ).Exists () {
39
- t .Fatalf ("Event did not have a 'transaction_id' on the GET /rooms/%s/event/%s response" , roomID , eventID )
42
+ unsignedTxnId := result .Get ("unsigned.transaction_id" )
43
+ if ! unsignedTxnId .Exists () {
44
+ t .Fatalf ("Event did not have a 'unsigned.transaction_id' on the GET /rooms/%s/event/%s response" , roomID , eventID )
45
+ }
46
+
47
+ must .EqualStr (t , unsignedTxnId .Str , txnId , fmt .Sprintf ("Event had an incorrect 'unsigned.transaction_id' on GET /rooms/%s/event/%s response" , eventID , roomID ))
48
+ }
49
+
50
+
51
+ func mustHaveTransactionIDForEvent (t * testing.T , roomID , eventID , expectedTxnId string ) client.SyncCheckOpt {
52
+ return client .SyncTimelineHas (roomID , func (r gjson.Result ) bool {
53
+ if r .Get ("event_id" ).Str == eventID {
54
+ unsignedTxnId := r .Get ("unsigned.transaction_id" )
55
+ if ! unsignedTxnId .Exists () {
56
+ t .Fatalf ("Event %s in room %s should have a 'unsigned.transaction_id', but it did not" , eventID , roomID )
57
+ }
58
+
59
+ must .EqualStr (t , unsignedTxnId .Str , expectedTxnId , fmt .Sprintf ("Event %s in room %s had an incorrect 'unsigned.transaction_id'" , eventID , roomID ))
60
+
61
+ return true
62
+ }
63
+
64
+ return false
65
+ })
66
+ }
67
+
68
+ func mustNotHaveTransactionIDForEvent (t * testing.T , roomID , eventID string ) client.SyncCheckOpt {
69
+ return client .SyncTimelineHas (roomID , func (r gjson.Result ) bool {
70
+ if r .Get ("event_id" ).Str == eventID {
71
+ unsignedTxnId := r .Get ("unsigned.transaction_id" )
72
+ if unsignedTxnId .Exists () {
73
+ t .Fatalf ("Event %s in room %s should NOT have a 'unsigned.transaction_id', but it did (%s)" , eventID , roomID , unsignedTxnId .Str )
74
+ }
75
+
76
+ return true
77
+ }
78
+
79
+ return false
80
+ })
81
+ }
82
+
83
+ // TestTxnScopeOnLocalEcho tests that transaction IDs in the sync response are scoped to the "client session", not the device
84
+ func TestTxnScopeOnLocalEcho (t * testing.T ) {
85
+ // Conduit scope transaction IDs to the device ID, not the access token.
86
+ runtime .SkipIf (t , runtime .Conduit )
87
+
88
+ deployment := Deploy (t , b .BlueprintCleanHS )
89
+ defer deployment .Destroy (t )
90
+
91
+ deployment .RegisterUser (t , "hs1" , "alice" , "password" , false )
92
+
93
+ // Create a first client, which allocates a device ID.
94
+ c1 := deployment .Client (t , "hs1" , "" )
95
+ c1 .UserID , c1 .AccessToken , c1 .DeviceID = c1 .LoginUser (t , "alice" , "password" )
96
+
97
+ // Create a room where we can send events.
98
+ roomID := c1 .CreateRoom (t , map [string ]interface {}{})
99
+
100
+ txnId := "abdefgh"
101
+ // Let's send an event, and wait for it to appear in the timeline.
102
+ eventID := c1 .SendEventUnsyncedWithTxnID (t , roomID , b.Event {
103
+ Type : "m.room.message" ,
104
+ Content : map [string ]interface {}{
105
+ "msgtype" : "m.text" ,
106
+ "body" : "first" ,
107
+ },
108
+ }, txnId )
109
+
110
+ // When syncing, we should find the event and it should have a transaction ID on the first client.
111
+ c1 .MustSyncUntil (t , client.SyncReq {}, mustHaveTransactionIDForEvent (t , roomID , eventID , txnId ))
112
+
113
+ // Create a second client, inheriting the first device ID.
114
+ c2 := deployment .Client (t , "hs1" , "" )
115
+ c2 .UserID , c2 .AccessToken , c2 .DeviceID = c2 .LoginUser (t , "alice" , "password" , client .WithDeviceID (c1 .DeviceID ))
116
+ must .EqualStr (t , c1 .DeviceID , c2 .DeviceID , "Device ID should be the same" )
117
+
118
+ // When syncing, we should find the event and it should *not* have a transaction ID on the second client.
119
+ c2 .MustSyncUntil (t , client.SyncReq {}, mustNotHaveTransactionIDForEvent (t , roomID , eventID ))
120
+ }
121
+
122
+ // TestTxnIdempotencyScopedToClientSession tests that transaction IDs are scoped to a "client session"
123
+ // and behave as expected across multiple clients even if they use the same device ID
124
+ func TestTxnIdempotencyScopedToClientSession (t * testing.T ) {
125
+ // Conduit scope transaction IDs to the device ID, not the client session.
126
+ runtime .SkipIf (t , runtime .Conduit )
127
+
128
+ deployment := Deploy (t , b .BlueprintCleanHS )
129
+ defer deployment .Destroy (t )
130
+
131
+ deployment .RegisterUser (t , "hs1" , "alice" , "password" , false )
132
+
133
+ // Create a first client, which allocates a device ID.
134
+ c1 := deployment .Client (t , "hs1" , "" )
135
+ c1 .UserID , c1 .AccessToken , c1 .DeviceID = c1 .LoginUser (t , "alice" , "password" )
136
+
137
+ // Create a room where we can send events.
138
+ roomID := c1 .CreateRoom (t , map [string ]interface {}{})
139
+
140
+ txnId := "abcdef"
141
+ event := b.Event {
142
+ Type : "m.room.message" ,
143
+ Content : map [string ]interface {}{
144
+ "msgtype" : "m.text" ,
145
+ "body" : "foo" ,
146
+ },
40
147
}
148
+ // send an event with set txnId
149
+ eventID1 := c1 .SendEventUnsyncedWithTxnID (t , roomID , event , txnId )
150
+
151
+ // Create a second client, inheriting the first device ID.
152
+ c2 := deployment .Client (t , "hs1" , "" )
153
+ c2 .UserID , c2 .AccessToken , c2 .DeviceID = c2 .LoginUser (t , "alice" , "password" , client .WithDeviceID (c1 .DeviceID ))
154
+ must .EqualStr (t , c1 .DeviceID , c2 .DeviceID , "Device ID should be the same" )
155
+
156
+ // send another event with the same txnId via the second client
157
+ eventID2 := c2 .SendEventUnsyncedWithTxnID (t , roomID , event , txnId )
158
+
159
+ // the two events should have different event IDs as they came from different clients
160
+ must .NotEqualStr (t , eventID2 , eventID1 , "Expected eventID1 and eventID2 to be different from two clients sharing the same device ID" )
161
+ }
162
+
163
+ // TestTxnIdempotency tests that PUT requests idempotency follows required semantics
164
+ func TestTxnIdempotency (t * testing.T ) {
165
+ // Conduit appears to be tracking transaction IDs individually rather than combined with the request URI/room ID
166
+ runtime .SkipIf (t , runtime .Conduit )
167
+
168
+ deployment := Deploy (t , b .BlueprintCleanHS )
169
+ defer deployment .Destroy (t )
170
+
171
+ deployment .RegisterUser (t , "hs1" , "alice" , "password" , false )
172
+
173
+ // Create a first client, which allocates a device ID.
174
+ c1 := deployment .Client (t , "hs1" , "" )
175
+ c1 .UserID , c1 .AccessToken , c1 .DeviceID = c1 .LoginUser (t , "alice" , "password" )
176
+
177
+ // Create a room where we can send events.
178
+ roomID1 := c1 .CreateRoom (t , map [string ]interface {}{})
179
+ roomID2 := c1 .CreateRoom (t , map [string ]interface {}{})
180
+
181
+ // choose a transaction ID
182
+ txnId := "abc"
183
+ event1 := b.Event {
184
+ Type : "m.room.message" ,
185
+ Content : map [string ]interface {}{
186
+ "msgtype" : "m.text" ,
187
+ "body" : "first" ,
188
+ },
189
+ }
190
+ event2 := b.Event {
191
+ Type : "m.room.message" ,
192
+ Content : map [string ]interface {}{
193
+ "msgtype" : "m.text" ,
194
+ "body" : "second" ,
195
+ },
196
+ }
197
+
198
+ // we send the event and get an event ID back
199
+ eventID1 := c1 .SendEventUnsyncedWithTxnID (t , roomID1 , event1 , txnId )
200
+
201
+ // we send the identical event again and should get back the same event ID
202
+ eventID2 := c1 .SendEventUnsyncedWithTxnID (t , roomID1 , event1 , txnId )
203
+
204
+ must .EqualStr (t , eventID2 , eventID1 , "Expected eventID1 and eventID2 to be the same, but they were not" )
205
+
206
+ // even if we change the content we should still get back the same event ID as transaction ID is the same
207
+ eventID3 := c1 .SendEventUnsyncedWithTxnID (t , roomID1 , event2 , txnId )
208
+
209
+ must .EqualStr (t , eventID3 , eventID1 , "Expected eventID3 and eventID2 to be the same even with different content, but they were not" )
210
+
211
+ // if we change the room ID we should be able to use the same transaction ID
212
+ eventID4 := c1 .SendEventUnsyncedWithTxnID (t , roomID2 , event1 , txnId )
213
+
214
+ must .NotEqualStr (t , eventID4 , eventID3 , "Expected eventID4 and eventID3 to be different, but they were not" )
41
215
}
0 commit comments