Skip to content

Commit 49d0556

Browse files
committed
Test for transaction ID semantics
1 parent 6e900e0 commit 49d0556

File tree

2 files changed

+213
-2
lines changed

2 files changed

+213
-2
lines changed

internal/client/client.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,14 @@ func (c *CSAPI) SetPushRule(t *testing.T, scope string, kind string, ruleID stri
264264
// SendEventUnsynced sends `e` into the room.
265265
// Returns the event ID of the sent event.
266266
func (c *CSAPI) SendEventUnsynced(t *testing.T, roomID string, e b.Event) string {
267-
t.Helper()
268267
txnID := int(atomic.AddInt64(&c.txnID, 1))
268+
return c.SendEventUnsyncedWithTxnID(t, roomID, e, txnID)
269+
}
270+
271+
// SendEventUnsynced sends `e` into the room.
272+
// Returns the event ID of the sent event.
273+
func (c *CSAPI) SendEventUnsyncedWithTxnID(t *testing.T, roomID string, e b.Event, txnID int) string {
274+
t.Helper()
269275
paths := []string{"_matrix", "client", "v3", "rooms", roomID, "send", e.Type, strconv.Itoa(txnID)}
270276
if e.StateKey != nil {
271277
paths = []string{"_matrix", "client", "v3", "rooms", roomID, "state", e.Type, *e.StateKey}
@@ -438,7 +444,34 @@ func (c *CSAPI) LoginUser(t *testing.T, localpart, password string) (userID, acc
438444
return userID, accessToken, deviceID
439445
}
440446

441-
//RegisterUser will register the user with given parameters and
447+
// LoginUserWithDeviceID will log in to a homeserver on an existing device
448+
func (c *CSAPI) LoginUserWithDeviceID(t *testing.T, localpart, password, deviceID string) (userID, accessToken string) {
449+
t.Helper()
450+
reqBody := map[string]interface{}{
451+
"identifier": map[string]interface{}{
452+
"type": "m.id.user",
453+
"user": localpart,
454+
},
455+
"device_id": deviceID,
456+
"password": password,
457+
"type": "m.login.password",
458+
}
459+
res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody))
460+
461+
body, err := ioutil.ReadAll(res.Body)
462+
if err != nil {
463+
t.Fatalf("unable to read response body: %v", err)
464+
}
465+
466+
userID = gjson.GetBytes(body, "user_id").Str
467+
accessToken = gjson.GetBytes(body, "access_token").Str
468+
if gjson.GetBytes(body, "device_id").Str != deviceID {
469+
t.Fatalf("device_id returned by login does not match the one requested")
470+
}
471+
return userID, accessToken
472+
}
473+
474+
// RegisterUser will register the user with given parameters and
442475
// return user ID & access token, and fail the test on network error
443476
func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID, accessToken, deviceID string) {
444477
t.Helper()

tests/csapi/txnid_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package csapi_tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/matrix-org/complement/internal/b"
7+
"github.com/matrix-org/complement/internal/client"
8+
"github.com/matrix-org/complement/runtime"
9+
"github.com/tidwall/gjson"
10+
)
11+
12+
func mustHaveTransactionID(t *testing.T, roomID, eventID string) client.SyncCheckOpt {
13+
return client.SyncTimelineHas(roomID, func(r gjson.Result) bool {
14+
if r.Get("event_id").Str == eventID {
15+
if !r.Get("unsigned.transaction_id").Exists() {
16+
t.Fatalf("Event %s in room %s should have a 'transaction_id', but it did not", eventID, roomID)
17+
}
18+
19+
return true
20+
}
21+
22+
return false
23+
})
24+
}
25+
26+
func mustNotHaveTransactionID(t *testing.T, roomID, eventID string) client.SyncCheckOpt {
27+
return client.SyncTimelineHas(roomID, func(r gjson.Result) bool {
28+
if r.Get("event_id").Str == eventID {
29+
res := r.Get("unsigned.transaction_id")
30+
if res.Exists() {
31+
t.Fatalf("Event %s in room %s should NOT have a 'transaction_id', but it did (%s)", eventID, roomID, res.Str)
32+
}
33+
34+
return true
35+
}
36+
37+
return false
38+
})
39+
}
40+
41+
// TestTxnScopeOnLocalEcho tests that transaction IDs are scoped to the access token, not the device
42+
// on the sync response
43+
func TestTxnScopeOnLocalEcho(t *testing.T) {
44+
// Conduit scope transaction IDs to the device ID, not the access token.
45+
runtime.SkipIf(t, runtime.Conduit)
46+
47+
deployment := Deploy(t, b.BlueprintCleanHS)
48+
defer deployment.Destroy(t)
49+
50+
deployment.RegisterUser(t, "hs1", "alice", "password", false)
51+
52+
// Create a first client, which allocates a device ID.
53+
c1 := deployment.Client(t, "hs1", "")
54+
c1.UserID, c1.AccessToken, c1.DeviceID = c1.LoginUser(t, "alice", "password")
55+
56+
// Create a room where we can send events.
57+
roomID := c1.CreateRoom(t, map[string]interface{}{})
58+
59+
// Let's send an event, and wait for it to appear in the timeline.
60+
eventID := c1.SendEventUnsynced(t, roomID, b.Event{
61+
Type: "m.room.message",
62+
Content: map[string]interface{}{
63+
"msgtype": "m.text",
64+
"body": "first",
65+
},
66+
})
67+
68+
// When syncing, we should find the event and it should have a transaction ID on the first client.
69+
c1.MustSyncUntil(t, client.SyncReq{}, mustHaveTransactionID(t, roomID, eventID))
70+
71+
// Create a second client, inheriting the first device ID.
72+
c2 := deployment.Client(t, "hs1", "")
73+
c2.UserID, c2.AccessToken = c2.LoginUserWithDeviceID(t, "alice", "password", c1.DeviceID)
74+
c2.DeviceID = c1.DeviceID
75+
76+
// When syncing, we should find the event and it should *not* have a transaction ID on the second client.
77+
c2.MustSyncUntil(t, client.SyncReq{}, mustNotHaveTransactionID(t, roomID, eventID))
78+
}
79+
80+
// TestTxnIdempotencyScopedToClientSession tests that transaction IDs are scoped to a "client session"
81+
// and behave as expected across multiple clients even if they use the same device ID
82+
func TestTxnIdempotencyScopedToClientSession(t *testing.T) {
83+
// Conduit scope transaction IDs to the device ID, not the client session.
84+
runtime.SkipIf(t, runtime.Conduit)
85+
86+
deployment := Deploy(t, b.BlueprintCleanHS)
87+
defer deployment.Destroy(t)
88+
89+
deployment.RegisterUser(t, "hs1", "alice", "password", false)
90+
91+
// Create a first client, which allocates a device ID.
92+
c1 := deployment.Client(t, "hs1", "")
93+
c1.UserID, c1.AccessToken, c1.DeviceID = c1.LoginUser(t, "alice", "password")
94+
95+
// Create a room where we can send events.
96+
roomID := c1.CreateRoom(t, map[string]interface{}{})
97+
98+
txnId := 1
99+
event := b.Event{
100+
Type: "m.room.message",
101+
Content: map[string]interface{}{
102+
"msgtype": "m.text",
103+
"body": "foo",
104+
},
105+
}
106+
// send an event with set txnId
107+
eventID1 := c1.SendEventUnsyncedWithTxnID(t, roomID, event, txnId)
108+
109+
// Create a second client, inheriting the first device ID.
110+
c2 := deployment.Client(t, "hs1", "")
111+
c2.UserID, c2.AccessToken = c2.LoginUserWithDeviceID(t, "alice", "password", c1.DeviceID)
112+
c2.DeviceID = c1.DeviceID
113+
114+
// send another event with the same txnId
115+
eventID2 := c2.SendEventUnsyncedWithTxnID(t, roomID, event, txnId)
116+
117+
// the two events should have different event IDs as they came from different clients
118+
if eventID1 == eventID2 {
119+
t.Fatalf("Expected event IDs to be different from two clients sharing the same device ID")
120+
}
121+
}
122+
123+
// TestTxnIdempotency tests that PUT requests idempotency follows required semantics
124+
func TestTxnIdempotency(t *testing.T) {
125+
deployment := Deploy(t, b.BlueprintCleanHS)
126+
defer deployment.Destroy(t)
127+
128+
deployment.RegisterUser(t, "hs1", "alice", "password", false)
129+
130+
// Create a first client, which allocates a device ID.
131+
c1 := deployment.Client(t, "hs1", "")
132+
c1.UserID, c1.AccessToken, c1.DeviceID = c1.LoginUser(t, "alice", "password")
133+
134+
// Create a room where we can send events.
135+
roomID1 := c1.CreateRoom(t, map[string]interface{}{})
136+
roomID2 := c1.CreateRoom(t, map[string]interface{}{})
137+
138+
// choose a transaction ID
139+
txnId := 1
140+
event1 := b.Event{
141+
Type: "m.room.message",
142+
Content: map[string]interface{}{
143+
"msgtype": "m.text",
144+
"body": "first",
145+
},
146+
}
147+
event2 := b.Event{
148+
Type: "m.room.message",
149+
Content: map[string]interface{}{
150+
"msgtype": "m.text",
151+
"body": "second",
152+
},
153+
}
154+
155+
// we send the event and get an event ID back
156+
eventID1 := c1.SendEventUnsyncedWithTxnID(t, roomID1, event1, txnId)
157+
158+
// we send the identical event again and should get back the same event ID
159+
eventID2 := c1.SendEventUnsyncedWithTxnID(t, roomID1, event1, txnId)
160+
161+
if eventID1 != eventID2 {
162+
t.Fatalf("Expected event IDs to be the same, but they were not")
163+
}
164+
165+
// even if we change the content we should still get back the same event ID as transaction ID is the same
166+
eventID3 := c1.SendEventUnsyncedWithTxnID(t, roomID1, event2, txnId)
167+
168+
if eventID1 != eventID3 {
169+
t.Fatalf("Expected event IDs to be the same even with different content, but they were not")
170+
}
171+
172+
// if we change the room ID we should be able to use the same transaction ID
173+
eventID4 := c1.SendEventUnsyncedWithTxnID(t, roomID2, event1, txnId)
174+
175+
if eventID4 == eventID3 {
176+
t.Fatalf("Expected event IDs to be the different, but they were not")
177+
}
178+
}

0 commit comments

Comments
 (0)