Skip to content

Commit b986a30

Browse files
hughnsclokep
andauthored
Test for new transaction ID scope after MSC3970 was merged in Matrix 1.7 (#637)
Co-authored-by: Patrick Cloke <[email protected]>
1 parent f032b6c commit b986a30

File tree

2 files changed

+106
-27
lines changed

2 files changed

+106
-27
lines changed

internal/client/client.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,53 @@ func (c *CSAPI) LoginUser(t *testing.T, localpart, password string, opts ...Logi
459459
return userID, accessToken, deviceID
460460
}
461461

462+
// LoginUserWithRefreshToken will log in to a homeserver, with refresh token enabled,
463+
// and create a new device on an existing user.
464+
func (c *CSAPI) LoginUserWithRefreshToken(t *testing.T, localpart, password string) (userID, accessToken, refreshToken, deviceID string, expiresInMs int64) {
465+
t.Helper()
466+
reqBody := map[string]interface{}{
467+
"identifier": map[string]interface{}{
468+
"type": "m.id.user",
469+
"user": localpart,
470+
},
471+
"password": password,
472+
"type": "m.login.password",
473+
"refresh_token": true,
474+
}
475+
res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody))
476+
477+
body, err := ioutil.ReadAll(res.Body)
478+
if err != nil {
479+
t.Fatalf("unable to read response body: %v", err)
480+
}
481+
482+
userID = gjson.GetBytes(body, "user_id").Str
483+
accessToken = gjson.GetBytes(body, "access_token").Str
484+
deviceID = gjson.GetBytes(body, "device_id").Str
485+
refreshToken = gjson.GetBytes(body, "refresh_token").Str
486+
expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int()
487+
return userID, accessToken, refreshToken, deviceID, expiresInMs
488+
}
489+
490+
// RefreshToken will consume a refresh token and return a new access token and refresh token.
491+
func (c *CSAPI) ConsumeRefreshToken(t *testing.T, refreshToken string) (newAccessToken, newRefreshToken string, expiresInMs int64) {
492+
t.Helper()
493+
reqBody := map[string]interface{}{
494+
"refresh_token": refreshToken,
495+
}
496+
res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "refresh"}, WithJSONBody(t, reqBody))
497+
498+
body, err := ioutil.ReadAll(res.Body)
499+
if err != nil {
500+
t.Fatalf("unable to read response body: %v", err)
501+
}
502+
503+
newAccessToken = gjson.GetBytes(body, "access_token").Str
504+
newRefreshToken = gjson.GetBytes(body, "refresh_token").Str
505+
expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int()
506+
return newAccessToken, newRefreshToken, expiresInMs
507+
}
508+
462509
// RegisterUser will register the user with given parameters and
463510
// return user ID, access token and device ID. It fails the test on network error.
464511
func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID, accessToken, deviceID string) {

tests/csapi/txnid_test.go

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,9 @@ func mustHaveTransactionIDForEvent(t *testing.T, roomID, eventID, expectedTxnId
6565
})
6666
}
6767

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
68+
// TestTxnScopeOnLocalEcho tests that transaction IDs in the sync response are scoped to the device
8469
func TestTxnScopeOnLocalEcho(t *testing.T) {
85-
// Conduit scope transaction IDs to the device ID, not the access token.
86-
runtime.SkipIf(t, runtime.Conduit)
70+
runtime.SkipIf(t, runtime.Dendrite)
8771

8872
deployment := Deploy(t, b.BlueprintCleanHS)
8973
defer deployment.Destroy(t)
@@ -115,15 +99,14 @@ func TestTxnScopeOnLocalEcho(t *testing.T) {
11599
c2.UserID, c2.AccessToken, c2.DeviceID = c2.LoginUser(t, "alice", "password", client.WithDeviceID(c1.DeviceID))
116100
must.EqualStr(t, c1.DeviceID, c2.DeviceID, "Device ID should be the same")
117101

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))
102+
// When syncing, we should find the event and it should have the same transaction ID on the second client.
103+
c2.MustSyncUntil(t, client.SyncReq{}, mustHaveTransactionIDForEvent(t, roomID, eventID, txnId))
120104
}
121105

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)
106+
// TestTxnIdempotencyScopedToDevice tests that transaction IDs are scoped to a device
107+
// and behave as expected across multiple clients if they use the same device ID
108+
func TestTxnIdempotencyScopedToDevice(t *testing.T) {
109+
runtime.SkipIf(t, runtime.Dendrite)
127110

128111
deployment := Deploy(t, b.BlueprintCleanHS)
129112
defer deployment.Destroy(t)
@@ -156,8 +139,8 @@ func TestTxnIdempotencyScopedToClientSession(t *testing.T) {
156139
// send another event with the same txnId via the second client
157140
eventID2 := c2.SendEventUnsyncedWithTxnID(t, roomID, event, txnId)
158141

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")
142+
// the two events should have the same event IDs as they came from the same device
143+
must.EqualStr(t, eventID2, eventID1, "Expected eventID1 and eventID2 to be the same from two clients sharing the same device ID")
161144
}
162145

163146
// TestTxnIdempotency tests that PUT requests idempotency follows required semantics
@@ -213,3 +196,52 @@ func TestTxnIdempotency(t *testing.T) {
213196

214197
must.NotEqualStr(t, eventID4, eventID3, "Expected eventID4 and eventID3 to be different, but they were not")
215198
}
199+
200+
// TestTxnIdWithRefreshToken tests that when a client refreshes its access token,
201+
// it still gets back a transaction ID in the sync response and idempotency is respected.
202+
func TestTxnIdWithRefreshToken(t *testing.T) {
203+
// Dendrite and Conduit don't support refresh tokens yet.
204+
runtime.SkipIf(t, runtime.Dendrite, runtime.Conduit)
205+
206+
deployment := Deploy(t, b.BlueprintCleanHS)
207+
defer deployment.Destroy(t)
208+
209+
deployment.RegisterUser(t, "hs1", "alice", "password", false)
210+
211+
c := deployment.Client(t, "hs1", "")
212+
213+
var refreshToken string
214+
c.UserID, c.AccessToken, refreshToken, c.DeviceID, _ = c.LoginUserWithRefreshToken(t, "alice", "password")
215+
216+
// Create a room where we can send events.
217+
roomID := c.CreateRoom(t, map[string]interface{}{})
218+
219+
txnId := "abcdef"
220+
// We send an event
221+
eventID1 := c.SendEventUnsyncedWithTxnID(t, roomID, b.Event{
222+
Type: "m.room.message",
223+
Content: map[string]interface{}{
224+
"msgtype": "m.text",
225+
"body": "first",
226+
},
227+
}, txnId)
228+
229+
// Use the refresh token to get a new access token.
230+
c.AccessToken, refreshToken, _ = c.ConsumeRefreshToken(t, refreshToken)
231+
232+
// When syncing, we should find the event and it should also have the correct transaction ID even
233+
// though the access token is different.
234+
c.MustSyncUntil(t, client.SyncReq{}, mustHaveTransactionIDForEvent(t, roomID, eventID1, txnId))
235+
236+
// We try sending the event again with the same transaction ID
237+
eventID2 := c.SendEventUnsyncedWithTxnID(t, roomID, b.Event{
238+
Type: "m.room.message",
239+
Content: map[string]interface{}{
240+
"msgtype": "m.text",
241+
"body": "first",
242+
},
243+
}, txnId)
244+
245+
// The event should have been deduplicated and we should get back the same event ID
246+
must.EqualStr(t, eventID2, eventID1, "Expected eventID1 and eventID2 to be the same from a client using a refresh token")
247+
}

0 commit comments

Comments
 (0)