11package  csapi_tests
22
33import  (
4+ 	"fmt" 
5+ 	"testing" 
6+ 
47	"github.com/matrix-org/complement/internal/b" 
58	"github.com/matrix-org/complement/internal/client" 
9+ 	"github.com/matrix-org/complement/internal/must" 
610	"github.com/matrix-org/complement/runtime" 
711	"github.com/tidwall/gjson" 
8- 	"testing" 
912)
1013
1114// 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) {
2225	// Create a room where we can send events. 
2326	roomID  :=  c .CreateRoom (t , map [string ]interface {}{})
2427
28+ 	txnId  :=  "abcdefg" 
2529	// 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 {
2731		Type : "m.room.message" ,
2832		Content : map [string ]interface {}{
2933			"msgtype" : "m.text" ,
3034			"body" :    "first" ,
3135		},
32- 	})
36+ 	},  txnId )
3337
3438	// The transaction ID should be present on the GET /rooms/{roomID}/event/{eventID} response. 
3539	res  :=  c .MustDoFunc (t , "GET" , []string {"_matrix" , "client" , "v3" , "rooms" , roomID , "event" , eventID })
3640	body  :=  client .ParseJSON (t , res )
3741	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+ 		},
40147	}
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" )
41215}
0 commit comments