Skip to content

Commit 3f60ae0

Browse files
Merge remote-tracking branch 'upstream/main' into famedly-release/v1.149.1
2 parents 3f92454 + 35b1745 commit 3f60ae0

4 files changed

Lines changed: 196 additions & 79 deletions

File tree

client/sync.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ type SyncReq struct {
3939
// since will be returned.
4040
// By default, this is false.
4141
FullState bool
42+
// Controls whether to set MSC422 `use_state_after` request parameter to get
43+
// `state_after` in the reponse (alternative to `state`).
44+
UseStateAfter bool
4245
// Controls whether the client is automatically marked as online by polling this API. If this
4346
// parameter is omitted then the client is automatically marked as online when it uses this API.
4447
// Otherwise if the parameter is set to “offline” then the client is not marked as being online
@@ -173,6 +176,13 @@ func (c *CSAPI) Sync(t ct.TestLike, syncReq SyncReq) (gjson.Result, *http.Respon
173176
if syncReq.FullState {
174177
query["full_state"] = []string{"true"}
175178
}
179+
if syncReq.UseStateAfter {
180+
// The spec is already stabilized
181+
query["use_state_after"] = []string{"true"}
182+
// FIXME: Some implementations haven't stabilized yet (Synapse) so we'll keep this
183+
// here until then.
184+
query["org.matrix.msc4222.use_state_after"] = []string{"true"}
185+
}
176186
if syncReq.SetPresence != "" {
177187
query["set_presence"] = []string{syncReq.SetPresence}
178188
}

tests/msc4140/delayed_event_test.go

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -360,85 +360,6 @@ func TestDelayedEvents(t *testing.T) {
360360
})
361361
})
362362

363-
t.Run("delayed state is not cancelled by new state from the same user", func(t *testing.T) {
364-
var res *http.Response
365-
366-
stateKey := "to_not_be_cancelled_by_same_user"
367-
368-
defer cleanupDelayedEvents(t, user)
369-
370-
setterKey := "setter"
371-
setterExpected := "on_timeout"
372-
user.MustDo(
373-
t,
374-
"PUT",
375-
getPathForState(roomID, eventType, stateKey),
376-
client.WithJSONBody(t, map[string]interface{}{
377-
setterKey: setterExpected,
378-
}),
379-
getDelayQueryParam("900"),
380-
)
381-
matchDelayedEvents(t, user, delayedEventsNumberEqual(1))
382-
383-
user.MustDo(
384-
t,
385-
"PUT",
386-
getPathForState(roomID, eventType, stateKey),
387-
client.WithJSONBody(t, map[string]interface{}{
388-
setterKey: "manual",
389-
}),
390-
)
391-
matchDelayedEvents(t, user, delayedEventsNumberEqual(1))
392-
393-
time.Sleep(1 * time.Second)
394-
res = user.MustDo(t, "GET", getPathForState(roomID, eventType, stateKey))
395-
must.MatchResponse(t, res, match.HTTPResponse{
396-
JSON: []match.JSON{
397-
match.JSONKeyEqual(setterKey, setterExpected),
398-
},
399-
})
400-
})
401-
402-
t.Run("delayed state is cancelled by new state from another user", func(t *testing.T) {
403-
var res *http.Response
404-
405-
stateKey := "to_be_cancelled_by_other_user"
406-
407-
defer cleanupDelayedEvents(t, user)
408-
defer cleanupDelayedEvents(t, user2)
409-
410-
setterKey := "setter"
411-
user.MustDo(
412-
t,
413-
"PUT",
414-
getPathForState(roomID, eventType, stateKey),
415-
client.WithJSONBody(t, map[string]interface{}{
416-
setterKey: "on_timeout",
417-
}),
418-
getDelayQueryParam("900"),
419-
)
420-
matchDelayedEvents(t, user, delayedEventsNumberEqual(1))
421-
422-
setterExpected := "manual"
423-
user2.MustDo(
424-
t,
425-
"PUT",
426-
getPathForState(roomID, eventType, stateKey),
427-
client.WithJSONBody(t, map[string]interface{}{
428-
setterKey: setterExpected,
429-
}),
430-
)
431-
matchDelayedEvents(t, user, delayedEventsNumberEqual(0))
432-
433-
time.Sleep(1 * time.Second)
434-
res = user.MustDo(t, "GET", getPathForState(roomID, eventType, stateKey))
435-
must.MatchResponse(t, res, match.HTTPResponse{
436-
JSON: []match.JSON{
437-
match.JSONKeyEqual(setterKey, setterExpected),
438-
},
439-
})
440-
})
441-
442363
t.Run("delayed state events are kept on server restart", func(t *testing.T) {
443364
// Spec cannot enforce server restart behaviour
444365
runtime.SkipIf(t, runtime.Dendrite, runtime.Conduit, runtime.Conduwuit)

tests/msc4222/main_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/matrix-org/complement"
7+
)
8+
9+
func TestMain(m *testing.M) {
10+
complement.TestMain(m, "msc4222")
11+
}

tests/msc4222/msc4222_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package tests
2+
3+
import (
4+
"maps"
5+
"slices"
6+
"testing"
7+
8+
"github.com/matrix-org/complement"
9+
"github.com/matrix-org/complement/client"
10+
"github.com/matrix-org/complement/helpers"
11+
"github.com/matrix-org/complement/should"
12+
"github.com/tidwall/gjson"
13+
)
14+
15+
func TestSync(t *testing.T) {
16+
deployment := complement.Deploy(t, 1)
17+
defer deployment.Destroy(t)
18+
alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"})
19+
bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob"})
20+
21+
t.Run("parallel", func(t *testing.T) {
22+
// When lazy-loading room members is enabled, for a public room, the `state_after`
23+
// in an initial sync request should include membership from every `sender` in the
24+
// `timeline`
25+
//
26+
// We're specifically testing the scenario where a new "DM" is created and the other person
27+
// joins without speaking yet.
28+
t.Run("Initial sync with lazy-loading room members -> public room `state_after` includes all members from timeline", func(t *testing.T) {
29+
t.Parallel()
30+
31+
// Alice creates a room
32+
roomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
33+
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
34+
35+
// Bob joins the room
36+
bob.MustJoinRoom(t, roomID, nil)
37+
38+
// Wait for Bob's join to be seen by Alice's sync (this is not necessarily instant)
39+
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
40+
41+
// Ensure `state_after` looks correct
42+
expectedSendersFromTimeline := []string{ alice.UserID, bob.UserID }
43+
syncFilter := `{
44+
"room": {
45+
"timeline": { "limit": 20 },
46+
"state": { "lazy_load_members": true }
47+
}
48+
}`
49+
testInitialSyncStateAfterIncludesTimelineSenders(t, alice, roomID, expectedSendersFromTimeline, syncFilter)
50+
})
51+
52+
// When lazy-loading room members is enabled, for a private room, the `state_after`
53+
// in an initial sync request should include membership from every `sender` in the
54+
// `timeline`
55+
//
56+
// We're specifically testing the scenario where a new "DM" is created and the other person
57+
// joins without speaking yet.
58+
t.Run("Initial sync with lazy-loading room members -> private room `state_after` includes all members from timeline", func(t *testing.T) {
59+
t.Parallel()
60+
61+
// Alice creates a room
62+
roomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat"})
63+
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
64+
65+
// Alice invites Bob
66+
alice.MustInviteRoom(t, roomID, bob.UserID)
67+
68+
// Wait for Bob to get the invite
69+
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
70+
71+
// Bob joins the room
72+
bob.MustJoinRoom(t, roomID, nil)
73+
74+
// Wait for Bob's join to be seen by Alice's sync (this is not necessarily instant)
75+
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
76+
77+
// Ensure `state_after` looks correct
78+
expectedSendersFromTimeline := []string{ alice.UserID, bob.UserID }
79+
syncFilter := `{
80+
"room": {
81+
"timeline": { "limit": 20 },
82+
"state": { "lazy_load_members": true }
83+
}
84+
}`
85+
testInitialSyncStateAfterIncludesTimelineSenders(t, alice, roomID, expectedSendersFromTimeline, syncFilter)
86+
})
87+
})
88+
}
89+
90+
91+
// The `state_after` in an initial sync request should at-least include membership from
92+
// every `sender` in the `timeline`.
93+
func testInitialSyncStateAfterIncludesTimelineSenders(
94+
t *testing.T,
95+
syncingUser *client.CSAPI,
96+
roomID string,
97+
expectedSendersFromTimeline []string,
98+
syncFilter string,
99+
) {
100+
t.Helper()
101+
102+
// `syncingUser` makes an initial sync request with lazy-loading members enabled
103+
//
104+
// The spec says `lazy_load_members` is valid field for both `timeline` and
105+
// `state` but as far as I can tell, only makes sense for `state` and that's
106+
// what Synapse keys off of.
107+
res, _ := syncingUser.MustSync(t, client.SyncReq{UseStateAfter: true, Filter: syncFilter})
108+
joinedRoomRes := res.Get("rooms.join." + client.GjsonEscape(roomID))
109+
if !joinedRoomRes.Exists() {
110+
t.Fatalf("Unable to find roomID=%s in the join part of the sync response: %s", roomID, res)
111+
}
112+
113+
// Collect the senders of all the time timeline events.
114+
roomTimelineRes := joinedRoomRes.Get("timeline.events");
115+
if !roomTimelineRes.IsArray() {
116+
t.Fatalf("Timeline events is not an array (found %s) %s", roomTimelineRes.Type.String(), res)
117+
}
118+
sendersFromTimeline := make(map[string]struct{}, 0)
119+
for _, event := range roomTimelineRes.Array() {
120+
sendersFromTimeline[event.Get("sender").Str] = struct{}{}
121+
}
122+
// We expect to see timeline events from `expectedSendersFromTimeline`
123+
err := should.ContainSubset(
124+
slices.Collect(maps.Keys(sendersFromTimeline)),
125+
expectedSendersFromTimeline,
126+
)
127+
if err != nil {
128+
t.Fatalf(
129+
"Expected to see timeline events from (%s) but only saw %s. " +
130+
"Got error: %s. join part of the sync response: %s",
131+
expectedSendersFromTimeline,
132+
slices.Collect(maps.Keys(sendersFromTimeline)),
133+
err.Error(),
134+
res,
135+
)
136+
}
137+
138+
// Collect the `m.room.membership` from `state_after`
139+
//
140+
// Try looking up the stable variant `state_after` first, then fallback to the
141+
// unstable version
142+
roomStateAfterResStable := joinedRoomRes.Get("state_after.events");
143+
roomStateAfterResUnstable := joinedRoomRes.Get("org\\.matrix\\.msc4222\\.state_after.events");
144+
var roomStateAfterRes gjson.Result
145+
if roomStateAfterResStable.Exists() {
146+
roomStateAfterRes = roomStateAfterResStable
147+
} else if roomStateAfterResUnstable.Exists() {
148+
roomStateAfterRes = roomStateAfterResUnstable
149+
}
150+
// Sanity check syntax
151+
if !roomStateAfterRes.IsArray() {
152+
t.Fatalf("state_after events is not an array (found %s) %s", roomStateAfterRes.Type.String(), res)
153+
}
154+
membershipFromState := make(map[string]struct{}, 0)
155+
for _, event := range roomStateAfterRes.Array() {
156+
if event.Get("type").Str == "m.room.member" {
157+
membershipFromState[event.Get("sender").Str] = struct{}{}
158+
}
159+
}
160+
// We should see membership state from every `sender` in the `timeline`.
161+
err = should.ContainSubset(
162+
slices.Collect(maps.Keys(membershipFromState)),
163+
slices.Collect(maps.Keys(sendersFromTimeline)),
164+
)
165+
if err != nil {
166+
t.Fatalf(
167+
"Expected to see membership state (%s) from every sender in the timeline (%s). " +
168+
"Got error: %s. join part of the sync response: %s",
169+
slices.Collect(maps.Keys(membershipFromState)),
170+
slices.Collect(maps.Keys(sendersFromTimeline)),
171+
err.Error(),
172+
res,
173+
)
174+
}
175+
}

0 commit comments

Comments
 (0)