Skip to content

Commit

Permalink
Merge 08d1acf into 68c7643
Browse files Browse the repository at this point in the history
  • Loading branch information
hughns committed May 26, 2023
2 parents 68c7643 + 08d1acf commit 4900f35
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 27 deletions.
47 changes: 47 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,53 @@ func (c *CSAPI) LoginUser(t *testing.T, localpart, password string, opts ...Logi
return userID, accessToken, deviceID
}

// LoginUserWithRefreshToken will log in to a homeserver, with refresh token enabled,
// and create a new device on an existing user.
func (c *CSAPI) LoginUserWithRefreshToken(t *testing.T, localpart, password string) (userID, accessToken, refreshToken, deviceID string, expiresInMs int64) {
t.Helper()
reqBody := map[string]interface{}{
"identifier": map[string]interface{}{
"type": "m.id.user",
"user": localpart,
},
"password": password,
"type": "m.login.password",
"refresh_token": true,
}
res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody))

body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("unable to read response body: %v", err)
}

userID = gjson.GetBytes(body, "user_id").Str
accessToken = gjson.GetBytes(body, "access_token").Str
deviceID = gjson.GetBytes(body, "device_id").Str
refreshToken = gjson.GetBytes(body, "refresh_token").Str
expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int()
return userID, accessToken, refreshToken, deviceID, expiresInMs
}

// RefreshToken will consume a refresh token and return a new access token and refresh token.
func (c *CSAPI) ConsumeRefreshToken(t *testing.T, refreshToken string) (newAccessToken, newRefreshToken string, expiresInMs int64) {
t.Helper()
reqBody := map[string]interface{}{
"refresh_token": refreshToken,
}
res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "refresh"}, WithJSONBody(t, reqBody))

body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("unable to read response body: %v", err)
}

newAccessToken = gjson.GetBytes(body, "access_token").Str
newRefreshToken = gjson.GetBytes(body, "refresh_token").Str
expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int()
return newAccessToken, newRefreshToken, expiresInMs
}

// RegisterUser will register the user with given parameters and
// return user ID, access token and device ID. It fails the test on network error.
func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID, accessToken, deviceID string) {
Expand Down
89 changes: 62 additions & 27 deletions tests/csapi/txnid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,10 @@ func mustHaveTransactionIDForEvent(t *testing.T, roomID, eventID, expectedTxnId
})
}

func mustNotHaveTransactionIDForEvent(t *testing.T, roomID, eventID string) client.SyncCheckOpt {
return client.SyncTimelineHas(roomID, func(r gjson.Result) bool {
if r.Get("event_id").Str == eventID {
unsignedTxnId := r.Get("unsigned.transaction_id")
if unsignedTxnId.Exists() {
t.Fatalf("Event %s in room %s should NOT have a 'unsigned.transaction_id', but it did (%s)", eventID, roomID, unsignedTxnId.Str)
}

return true
}

return false
})
}

// TestTxnScopeOnLocalEcho tests that transaction IDs in the sync response are scoped to the "client session", not the device
// TestTxnScopeOnLocalEcho tests that transaction IDs in the sync response are scoped to the device
func TestTxnScopeOnLocalEcho(t *testing.T) {
// Conduit scope transaction IDs to the device ID, not the access token.
runtime.SkipIf(t, runtime.Conduit)
// Synapse will support this once https://github.com/matrix-org/synapse/pull/15629 is merged
runtime.SkipIf(t, runtime.Dendrite, runtime.Synapse)

deployment := Deploy(t, b.BlueprintCleanHS)
defer deployment.Destroy(t)
Expand Down Expand Up @@ -115,15 +100,15 @@ func TestTxnScopeOnLocalEcho(t *testing.T) {
c2.UserID, c2.AccessToken, c2.DeviceID = c2.LoginUser(t, "alice", "password", client.WithDeviceID(c1.DeviceID))
must.EqualStr(t, c1.DeviceID, c2.DeviceID, "Device ID should be the same")

// When syncing, we should find the event and it should *not* have a transaction ID on the second client.
c2.MustSyncUntil(t, client.SyncReq{}, mustNotHaveTransactionIDForEvent(t, roomID, eventID))
// When syncing, we should find the event and it should have the same transaction ID on the second client.
c2.MustSyncUntil(t, client.SyncReq{}, mustHaveTransactionIDForEvent(t, roomID, eventID, txnId))
}

// TestTxnIdempotencyScopedToClientSession tests that transaction IDs are scoped to a "client session"
// and behave as expected across multiple clients even if they use the same device ID
func TestTxnIdempotencyScopedToClientSession(t *testing.T) {
// Conduit scope transaction IDs to the device ID, not the client session.
runtime.SkipIf(t, runtime.Conduit)
// TestTxnIdempotencyScopedToDevice tests that transaction IDs are scoped to a device
// and behave as expected across multiple clients if they use the same device ID
func TestTxnIdempotencyScopedToDevice(t *testing.T) {
// Synapse will support this once https://github.com/matrix-org/synapse/pull/15629 is merged
runtime.SkipIf(t, runtime.Dendrite, runtime.Synapse)

deployment := Deploy(t, b.BlueprintCleanHS)
defer deployment.Destroy(t)
Expand Down Expand Up @@ -156,8 +141,8 @@ func TestTxnIdempotencyScopedToClientSession(t *testing.T) {
// send another event with the same txnId via the second client
eventID2 := c2.SendEventUnsyncedWithTxnID(t, roomID, event, txnId)

// the two events should have different event IDs as they came from different clients
must.NotEqualStr(t, eventID2, eventID1, "Expected eventID1 and eventID2 to be different from two clients sharing the same device ID")
// the two events should have the same event IDs as they came from the same device
must.EqualStr(t, eventID2, eventID1, "Expected eventID1 and eventID2 to be the same from two clients sharing the same device ID")
}

// TestTxnIdempotency tests that PUT requests idempotency follows required semantics
Expand Down Expand Up @@ -213,3 +198,53 @@ func TestTxnIdempotency(t *testing.T) {

must.NotEqualStr(t, eventID4, eventID3, "Expected eventID4 and eventID3 to be different, but they were not")
}

// TestTxnIdWithRefreshToken tests that when a client refreshes its access token,
// it still gets back a transaction ID in the sync response and idempotency is respected.
func TestTxnIdWithRefreshToken(t *testing.T) {
// Dendrite and Conduit don't support refresh tokens yet.
// Synapse will pass once https://github.com/matrix-org/synapse/pull/15629 is merged
runtime.SkipIf(t, runtime.Dendrite, runtime.Conduit, runtime.Synapse)

deployment := Deploy(t, b.BlueprintCleanHS)
defer deployment.Destroy(t)

deployment.RegisterUser(t, "hs1", "alice", "password", false)

c := deployment.Client(t, "hs1", "")

var refreshToken string
c.UserID, c.AccessToken, refreshToken, c.DeviceID, _ = c.LoginUserWithRefreshToken(t, "alice", "password")

// Create a room where we can send events.
roomID := c.CreateRoom(t, map[string]interface{}{})

txnId := "abcdef"
// We send an event
eventID1 := c.SendEventUnsyncedWithTxnID(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "first",
},
}, txnId)

// Use the refresh token to get a new access token.
c.AccessToken, refreshToken, _ = c.ConsumeRefreshToken(t, refreshToken)

// When syncing, we should find the event and it should also have the correct transaction ID even
// though the access token is different.
c.MustSyncUntil(t, client.SyncReq{}, mustHaveTransactionIDForEvent(t, roomID, eventID1, txnId))

// We try sending the event again with the same transaction ID
eventID2 := c.SendEventUnsyncedWithTxnID(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "first",
},
}, txnId)

// The event should have been deduplicated and we should get back the same event ID
must.EqualStr(t, eventID2, eventID1, "Expected eventID1 and eventID2 to be the same from a client using a refresh token")
}

0 comments on commit 4900f35

Please sign in to comment.