sliding-sync/tests-e2e/lists_test.go

1819 lines
56 KiB
Go
Raw Normal View History

package syncv3_test
import (
"fmt"
"sync"
"testing"
"time"
2023-10-11 12:23:46 +01:00
"github.com/matrix-org/complement/b"
"github.com/matrix-org/sliding-sync/sync3/extensions"
"github.com/tidwall/gjson"
"github.com/matrix-org/sliding-sync/sync3"
"github.com/matrix-org/sliding-sync/testutils/m"
)
// Test that multiple lists can be independently scrolled through
func TestMultipleLists(t *testing.T) {
alice := registerNewUser(t)
// make 10 encrypted rooms and make 10 unencrypted rooms. [0] is most recent
var encryptedRoomIDs []string
var unencryptedRoomIDs []string
for i := 0; i < 10; i++ {
2023-10-11 12:23:46 +01:00
unencryptedRoomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
unencryptedRoomIDs = append([]string{unencryptedRoomID}, unencryptedRoomIDs...) // push in array
2023-10-11 12:23:46 +01:00
encryptedRoomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
2023-10-11 12:23:46 +01:00
"initial_state": []b.Event{
NewEncryptionEvent(),
},
})
encryptedRoomIDs = append([]string{encryptedRoomID}, encryptedRoomIDs...) // push in array
time.Sleep(time.Millisecond) // ensure timestamp changes
}
// request 2 lists, one set encrypted, one set unencrypted
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"enc": {
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsEncrypted: &boolTrue,
},
},
"unenc": {
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsEncrypted: &boolFalse,
},
},
},
})
m.MatchResponse(t, res,
m.MatchLists(map[string][]m.ListMatcher{
"enc": {
m.MatchV3Count(len(encryptedRoomIDs)),
m.MatchV3Ops(m.MatchV3SyncOp(0, 2, encryptedRoomIDs[:3])),
},
"unenc": {
m.MatchV3Count(len(unencryptedRoomIDs)),
m.MatchV3Ops(m.MatchV3SyncOp(0, 2, unencryptedRoomIDs[:3])),
},
}),
m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
encryptedRoomIDs[0]: {},
encryptedRoomIDs[1]: {},
encryptedRoomIDs[2]: {},
unencryptedRoomIDs[0]: {},
unencryptedRoomIDs[1]: {},
unencryptedRoomIDs[2]: {},
}),
)
// now scroll one of the lists
res = alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"enc": {
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
"unenc": {
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
[2]int64{3, 5}, // next 3 rooms
},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
"enc": {
m.MatchV3Count(len(encryptedRoomIDs)),
},
"unenc": {
m.MatchV3Count(len(unencryptedRoomIDs)),
m.MatchV3Ops(
m.MatchV3SyncOp(3, 5, unencryptedRoomIDs[3:6]),
),
},
}), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
unencryptedRoomIDs[3]: {},
unencryptedRoomIDs[4]: {},
unencryptedRoomIDs[5]: {},
}))
// now shift the last/oldest unencrypted room to an encrypted room and make sure both lists update
alice.SendEventSynced(t, unencryptedRoomIDs[len(unencryptedRoomIDs)-1], NewEncryptionEvent())
// update our source of truth: the last unencrypted room is now the first encrypted room
encryptedRoomIDs = append([]string{unencryptedRoomIDs[len(unencryptedRoomIDs)-1]}, encryptedRoomIDs...)
unencryptedRoomIDs = unencryptedRoomIDs[:len(unencryptedRoomIDs)-1]
// We are tracking the first few encrypted rooms so we expect list 0 to update
// However we do not track old unencrypted rooms so we expect no change in list 1
res = alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"enc": {
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
"unenc": {
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
[2]int64{3, 5}, // next 3 rooms
},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
"enc": {
m.MatchV3Count(len(encryptedRoomIDs)),
m.MatchV3Ops(
m.MatchV3DeleteOp(2),
m.MatchV3InsertOp(0, encryptedRoomIDs[0]),
),
},
"unenc": {
m.MatchV3Count(len(unencryptedRoomIDs)),
},
}))
}
// Test that bumps only update a single list and not both. Regression test for when
// DM rooms get bumped they appeared in the is_dm:false list.
func TestMultipleListsDMUpdate(t *testing.T) {
alice := registerNewUser(t)
var dmRoomIDs []string
var groupRoomIDs []string
dmContent := map[string]interface{}{} // user_id -> [room_id]
// make 5 group rooms and make 5 DMs rooms. Room 0 is most recent to ease checks
for i := 0; i < 5; i++ {
dmUserID := fmt.Sprintf("@dm_%d:synapse", i) // TODO: domain brittle
2023-10-11 12:23:46 +01:00
groupRoomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
groupRoomIDs = append([]string{groupRoomID}, groupRoomIDs...) // push in array
2023-10-11 12:23:46 +01:00
dmRoomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "trusted_private_chat",
"is_direct": true,
"invite": []string{dmUserID},
})
dmRoomIDs = append([]string{dmRoomID}, dmRoomIDs...) // push in array
dmContent[dmUserID] = []string{dmRoomID}
time.Sleep(time.Millisecond) // ensure timestamp changes
}
// set the account data
2023-10-11 12:23:46 +01:00
alice.MustSetGlobalAccountData(t, "m.direct", dmContent)
// request 2 lists, one set DM, one set no DM
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"dm": {
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsDM: &boolTrue,
},
},
"nodm": {
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsDM: &boolFalse,
},
},
},
})
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
"dm": {
m.MatchV3Count(len(dmRoomIDs)),
m.MatchV3Ops(m.MatchV3SyncOp(0, 2, dmRoomIDs[:3])),
},
"nodm": {
m.MatchV3Count(len(groupRoomIDs)),
m.MatchV3Ops(m.MatchV3SyncOp(0, 2, groupRoomIDs[:3])),
},
}))
// now bring the last DM room to the top with a notif
2023-10-11 12:23:46 +01:00
pingEventID := alice.SendEventSynced(t, dmRoomIDs[len(dmRoomIDs)-1], b.Event{
Type: "m.room.message",
Content: map[string]interface{}{"body": "ping", "msgtype": "m.text"},
})
// update our source of truth: swap the last and first elements
dmRoomIDs[0], dmRoomIDs[len(dmRoomIDs)-1] = dmRoomIDs[len(dmRoomIDs)-1], dmRoomIDs[0]
// now get the delta: only the DM room should change
res = alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"dm": {
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
"nodm": {
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
"dm": {
m.MatchV3Count(len(dmRoomIDs)),
m.MatchV3Ops(
m.MatchV3DeleteOp(2),
m.MatchV3InsertOp(0, dmRoomIDs[0]),
),
},
"nodm": {
m.MatchV3Count(len(groupRoomIDs)),
},
}), m.MatchRoomSubscription(dmRoomIDs[0], MatchRoomTimelineMostRecent(1, []Event{
{
Type: "m.room.message",
ID: pingEventID,
},
})))
}
// Test that a new list can be added mid-connection
func TestNewListMidConnection(t *testing.T) {
alice := registerNewUser(t)
var roomIDs []string
// make rooms
for i := 0; i < 4; i++ {
2023-10-11 12:23:46 +01:00
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
roomIDs = append([]string{roomID}, roomIDs...) // push in array
time.Sleep(time.Millisecond) // ensure timestamp changes
}
// first request no list
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{},
})
m.MatchResponse(t, res, m.MatchLists(nil))
// now add a list
res = alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(len(roomIDs)), m.MatchV3Ops(
m.MatchV3SyncOp(0, 2, roomIDs[:3]),
)))
}
// Tests that if a room appears in >1 list that we union room subscriptions correctly.
func TestMultipleOverlappingLists(t *testing.T) {
alice := registerNewUser(t)
var allRoomIDs []string
var encryptedRoomIDs []string
var dmRoomIDs []string
dmContent := map[string]interface{}{} // user_id -> [room_id]
dmUserID := "@bob:synapse"
// make 3 encrypted rooms, 3 encrypted/dm rooms, 3 dm rooms.
// [0] is the newest room.
for i := 9; i >= 0; i-- {
isEncrypted := i < 6
isDM := i >= 3
createContent := map[string]interface{}{
"preset": "private_chat",
}
if isEncrypted {
2023-10-11 12:23:46 +01:00
createContent["initial_state"] = []b.Event{
NewEncryptionEvent(),
}
}
if isDM {
createContent["is_direct"] = true
createContent["invite"] = []string{dmUserID}
}
2023-10-11 12:23:46 +01:00
roomID := alice.MustCreateRoom(t, createContent)
time.Sleep(time.Millisecond)
if isDM {
var roomIDs []string
roomIDsInt, ok := dmContent[dmUserID]
if ok {
roomIDs = roomIDsInt.([]string)
}
dmContent[dmUserID] = append(roomIDs, roomID)
dmRoomIDs = append([]string{roomID}, dmRoomIDs...)
}
if isEncrypted {
encryptedRoomIDs = append([]string{roomID}, encryptedRoomIDs...)
}
allRoomIDs = append([]string{roomID}, allRoomIDs...) // push entries so [0] is newest
}
// set the account data
t.Logf("DM rooms: %v", dmRoomIDs)
t.Logf("Encrypted rooms: %v", encryptedRoomIDs)
2023-10-11 12:23:46 +01:00
alice.MustSetGlobalAccountData(t, "m.direct", dmContent)
// seed the proxy: so we can get timeline correctly as it uses limit:1 initially.
alice.SlidingSync(t, sync3.Request{})
// send messages to track timeline. The most recent messages are:
// - ENCRYPTION EVENT (if set)
// - DM INVITE EVENT (if set)
// - This ping message (always)
roomToEventID := make(map[string]string, len(allRoomIDs))
for i := len(allRoomIDs) - 1; i >= 0; i-- {
2023-10-11 12:23:46 +01:00
roomToEventID[allRoomIDs[i]] = alice.SendEventSynced(t, allRoomIDs[i], b.Event{
Type: "m.room.message",
Content: map[string]interface{}{"body": "ping", "msgtype": "m.text"},
})
}
add extensions for typing and receipts; bugfixes and additional perf improvements Features: - Add `typing` extension. - Add `receipts` extension. - Add comprehensive prometheus `/metrics` activated via `SYNCV3_PROM`. - Add `SYNCV3_PPROF` support. - Add `by_notification_level` sort order. - Add `include_old_rooms` support. - Add support for `$ME` and `$LAZY`. - Add correct filtering when `*,*` is used as `required_state`. - Add `num_live` to each room response to indicate how many timeline entries are live. Bug fixes: - Use a stricter comparison function on ranges: fixes an issue whereby UTs fail on go1.19 due to change in sorting algorithm. - Send back an `errcode` on HTTP errors (e.g expired sessions). - Remove `unsigned.txn_id` on insertion into the DB. Otherwise other users would see other users txn IDs :( - Improve range delta algorithm: previously it didn't handle cases like `[0,20] -> [20,30]` and would panic. - Send HTTP 400 for invalid range requests. - Don't publish no-op unread counts which just adds extra noise. - Fix leaking DB connections which could eventually consume all available connections. - Ensure we always unblock WaitUntilInitialSync even on invalid access tokens. Other code relies on WaitUntilInitialSync() actually returning at _some_ point e.g on startup we have N workers which bound the number of concurrent pollers made at any one time, we need to not just hog a worker forever. Improvements: - Greatly improve startup times of sync3 handlers by improving `JoinedRoomsTracker`: a modest amount of data would take ~28s to create the handler, now it takes 4s. - Massively improve initial initial v3 sync times, by refactoring `JoinedRoomsTracker`, from ~47s to <1s. - Add `SlidingSyncUntil...` in tests to reduce races. - Tweak the API shape of JoinedUsersForRoom to reduce state block processing time for large rooms from 63s to 39s. - Add trace task for initial syncs. - Include the proxy version in UA strings. - HTTP errors now wait 1s before returning to stop clients tight-looping on error. - Pending event buffer is now 2000. - Index the room ID first to cull the most events when returning timeline entries. Speeds up `SelectLatestEventsBetween` by a factor of 8. - Remove cancelled `m.room_key_requests` from the to-device inbox. Cuts down the amount of events in the inbox by ~94% for very large (20k+) inboxes, ~50% for moderate sized (200 events) inboxes. Adds book-keeping to remember the unacked to-device position for each client.
2022-12-14 18:53:55 +00:00
lastEventID := roomToEventID[allRoomIDs[0]]
alice.SlidingSyncUntilEventID(t, "", allRoomIDs[0], lastEventID)
// request 2 lists: one DMs, one encrypted. The room subscriptions are different so they should be UNION'd correctly.
// We request 5 rooms to ensure there is some overlap but not total overlap:
// newest top 5 DM
// v .-----------.
// E E E ED* ED* D D D D
// `-----------`
// top 5 Encrypted
//
// Rooms with * are union'd
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"enc": {
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 4}, // first 5 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 2, // pull in the ping msg + some state event depending on the room type
RequiredState: [][2]string{
{"m.room.join_rules", ""},
},
},
Filters: &sync3.RequestFilters{
IsEncrypted: &boolTrue,
},
},
"dm": {
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 4}, // first 5 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1, // pull in ping message only
RequiredState: [][2]string{
{"m.room.power_levels", ""},
},
},
Filters: &sync3.RequestFilters{
IsDM: &boolTrue,
},
},
},
})
m.MatchResponse(t, res,
m.MatchList("enc", m.MatchV3Ops(m.MatchV3SyncOp(0, 4, encryptedRoomIDs[:5]))),
m.MatchList("dm", m.MatchV3Ops(m.MatchV3SyncOp(0, 4, dmRoomIDs[:5]))),
m.MatchRoomSubscriptions(map[string][]m.RoomMatcher{
// encrypted rooms just come from the encrypted only list
encryptedRoomIDs[0]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.join_rules",
StateKey: ptr(""),
},
}),
MatchRoomTimeline([]Event{
{Type: "m.room.encryption", StateKey: ptr("")},
{ID: roomToEventID[encryptedRoomIDs[0]]},
}),
},
encryptedRoomIDs[1]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.join_rules",
StateKey: ptr(""),
},
}),
MatchRoomTimeline([]Event{
{Type: "m.room.encryption", StateKey: ptr("")},
{ID: roomToEventID[encryptedRoomIDs[1]]},
}),
},
encryptedRoomIDs[2]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.join_rules",
StateKey: ptr(""),
},
}),
MatchRoomTimeline([]Event{
{Type: "m.room.encryption", StateKey: ptr("")},
{ID: roomToEventID[encryptedRoomIDs[2]]},
}),
},
// overlapping with DM rooms
encryptedRoomIDs[3]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.join_rules",
StateKey: ptr(""),
},
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimeline([]Event{
{Type: "m.room.member", StateKey: ptr(dmUserID)},
{ID: roomToEventID[encryptedRoomIDs[3]]},
}),
},
encryptedRoomIDs[4]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.join_rules",
StateKey: ptr(""),
},
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimeline([]Event{
{Type: "m.room.member", StateKey: ptr(dmUserID)},
{ID: roomToEventID[encryptedRoomIDs[4]]},
}),
},
// DM only rooms
dmRoomIDs[2]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimelineMostRecent(1, []Event{{ID: roomToEventID[dmRoomIDs[2]]}}),
},
dmRoomIDs[3]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimelineMostRecent(1, []Event{{ID: roomToEventID[dmRoomIDs[3]]}}),
},
dmRoomIDs[4]: {
m.MatchRoomInitial(true),
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimelineMostRecent(1, []Event{{ID: roomToEventID[dmRoomIDs[4]]}}),
},
}),
)
}
// Regression test for a panic when new rooms were live-streamed to the client in Element-Web
func TestNot500OnNewRooms(t *testing.T) {
boolTrue := true
boolFalse := false
mSpace := "m.space"
alice := registerNewUser(t)
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
SlowGetAllRooms: &boolTrue,
Filters: &sync3.RequestFilters{
RoomTypes: []*string{&mSpace},
},
},
},
})
2023-10-11 12:23:46 +01:00
alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
SlowGetAllRooms: &boolTrue,
Filters: &sync3.RequestFilters{
RoomTypes: []*string{&mSpace},
},
},
"b": {
Filters: &sync3.RequestFilters{
IsDM: &boolFalse,
},
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
2023-10-11 12:23:46 +01:00
alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
// should not 500
alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
SlowGetAllRooms: &boolTrue,
},
"b": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
}
// Regression test for room name calculations, which could be incorrect for new rooms due to caches
// not being populated yet e.g "and 1 others" or "Empty room" when a room name has been set.
func TestNewRoomNameCalculations(t *testing.T) {
alice := registerNewUser(t)
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
SlowGetAllRooms: &boolTrue,
},
},
})
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(0)))
// create 10 room in parallel and at the same time spam sliding sync to ensure we get bits of
// rooms before they are fully loaded.
numRooms := 10
ch := make(chan int, numRooms)
var roomIDToName sync.Map
// start the goroutines
for i := 0; i < numRooms; i++ {
go func() {
for i := range ch {
roomName := fmt.Sprintf("room %d", i)
2023-10-11 12:23:46 +01:00
roomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": roomName})
roomIDToName.Store(roomID, roomName)
}
}()
}
// inject the work
for i := 0; i < numRooms; i++ {
ch <- i
}
close(ch)
seenRoomNames := make(map[string]string)
start := time.Now()
var err error
for {
res = alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
SlowGetAllRooms: &boolTrue,
},
},
}, WithPos(res.Pos))
for roomID, sub := range res.Rooms {
if sub.Name != "" {
seenRoomNames[roomID] = sub.Name
}
}
add extensions for typing and receipts; bugfixes and additional perf improvements Features: - Add `typing` extension. - Add `receipts` extension. - Add comprehensive prometheus `/metrics` activated via `SYNCV3_PROM`. - Add `SYNCV3_PPROF` support. - Add `by_notification_level` sort order. - Add `include_old_rooms` support. - Add support for `$ME` and `$LAZY`. - Add correct filtering when `*,*` is used as `required_state`. - Add `num_live` to each room response to indicate how many timeline entries are live. Bug fixes: - Use a stricter comparison function on ranges: fixes an issue whereby UTs fail on go1.19 due to change in sorting algorithm. - Send back an `errcode` on HTTP errors (e.g expired sessions). - Remove `unsigned.txn_id` on insertion into the DB. Otherwise other users would see other users txn IDs :( - Improve range delta algorithm: previously it didn't handle cases like `[0,20] -> [20,30]` and would panic. - Send HTTP 400 for invalid range requests. - Don't publish no-op unread counts which just adds extra noise. - Fix leaking DB connections which could eventually consume all available connections. - Ensure we always unblock WaitUntilInitialSync even on invalid access tokens. Other code relies on WaitUntilInitialSync() actually returning at _some_ point e.g on startup we have N workers which bound the number of concurrent pollers made at any one time, we need to not just hog a worker forever. Improvements: - Greatly improve startup times of sync3 handlers by improving `JoinedRoomsTracker`: a modest amount of data would take ~28s to create the handler, now it takes 4s. - Massively improve initial initial v3 sync times, by refactoring `JoinedRoomsTracker`, from ~47s to <1s. - Add `SlidingSyncUntil...` in tests to reduce races. - Tweak the API shape of JoinedUsersForRoom to reduce state block processing time for large rooms from 63s to 39s. - Add trace task for initial syncs. - Include the proxy version in UA strings. - HTTP errors now wait 1s before returning to stop clients tight-looping on error. - Pending event buffer is now 2000. - Index the room ID first to cull the most events when returning timeline entries. Speeds up `SelectLatestEventsBetween` by a factor of 8. - Remove cancelled `m.room_key_requests` from the to-device inbox. Cuts down the amount of events in the inbox by ~94% for very large (20k+) inboxes, ~50% for moderate sized (200 events) inboxes. Adds book-keeping to remember the unacked to-device position for each client.
2022-12-14 18:53:55 +00:00
if time.Since(start) > 15*time.Second {
t.Errorf("timed out, did not see all rooms, seen %d/%d", len(seenRoomNames), numRooms)
break
}
// do assertions and bail if they all pass
err = nil
seenRooms := 0
roomIDToName.Range(func(key, value interface{}) bool {
seenRooms++
createRoomID := key.(string)
name := value.(string)
gotName := seenRoomNames[createRoomID]
if name != gotName {
err = fmt.Errorf("[%s: got %s want %s] %w", createRoomID, gotName, name, err)
}
return true
})
if seenRooms != numRooms {
continue // wait for all /createRoom calls to return
}
if err == nil {
t.Logf("%+v\n", seenRoomNames)
break // we saw all the rooms with the right names
}
}
if err != nil {
// we didn't see all the right room names after timeout secs
t.Errorf(err.Error())
}
}
// Regression test for when you swap from sorting by recency to sorting by name and it didn't take effect.
func TestChangeSortOrder(t *testing.T) {
alice := registerNewUser(t)
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Sort: []string{sync3.SortByRecency},
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{{"m.room.name", ""}},
},
},
},
})
m.MatchResponse(t, res, m.MatchRoomSubscriptionsStrict(nil), m.MatchNoV3Ops())
roomNames := []string{
"Kiwi", "Lemon", "Apple", "Orange",
}
roomIDs := make([]string, len(roomNames))
gotNameToIDs := make(map[string]string)
waitFor := func(roomID, roomName string) {
start := time.Now()
for {
if time.Since(start) > time.Second {
t.Fatalf("didn't see room name '%s' for room %s", roomName, roomID)
break
}
res = alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
for roomID, sub := range res.Rooms {
gotNameToIDs[sub.Name] = roomID
}
if gotNameToIDs[roomName] == roomID {
break
}
time.Sleep(10 * time.Millisecond)
}
}
for i, name := range roomNames {
2023-10-11 12:23:46 +01:00
roomIDs[i] = alice.MustCreateRoom(t, map[string]interface{}{
"name": name,
})
// we cannot guarantee we will see the right state yet, so just keep track of the room names
waitFor(roomIDs[i], name)
}
// now change the sort order: first send the request up then keep hitting sliding sync until
// we see the txn ID confirming it has been applied
txnID := "a"
res = alice.SlidingSync(t, sync3.Request{
TxnID: "a",
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Sort: []string{sync3.SortByName},
},
},
}, WithPos(res.Pos))
for res.TxnID != txnID {
res = alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
}
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(4), m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3InvalidateOp(0, 3),
m.MatchV3SyncOp(0, 3, []string{gotNameToIDs["Apple"], gotNameToIDs["Kiwi"], gotNameToIDs["Lemon"], gotNameToIDs["Orange"]}),
)))
}
// Regression test that a window can be shrunk and the INVALIDATE appears correctly for the low/high ends of the range
func TestShrinkRange(t *testing.T) {
alice := registerNewUser(t)
var roomIDs []string // most recent first
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond) // ensure creation timestamp changes
2023-10-11 12:23:46 +01:00
roomIDs = append([]string{alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": fmt.Sprintf("Room %d", i),
})}, roomIDs...)
}
res := alice.SlidingSync(t, sync3.Request{
2023-01-18 15:31:44 +00:00
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
})
2023-01-18 15:31:44 +00:00
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(10), m.MatchV3Ops(
2023-04-05 01:07:07 +01:00
m.MatchV3SyncOp(0, 9, roomIDs),
)))
// now shrink the window on both ends
res = alice.SlidingSync(t, sync3.Request{
2023-01-18 15:31:44 +00:00
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{2, 6}},
},
},
}, WithPos(res.Pos))
2023-01-18 15:31:44 +00:00
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(10), m.MatchV3Ops(
m.MatchV3InvalidateOp(0, 1),
2023-04-05 01:07:07 +01:00
m.MatchV3InvalidateOp(7, 9),
)))
}
// Regression test that you can expand a window without it causing problems. Previously, it would cause
// a spurious {"op":"SYNC","range":[11,20],"room_ids":["!jHVHDxEWqTIcbDYoah:synapse"]} op for a bad index
func TestExpandRange(t *testing.T) {
alice := registerNewUser(t)
var roomIDs []string // most recent first
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond) // ensure creation timestamp changes
2023-10-11 12:23:46 +01:00
roomIDs = append([]string{alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": fmt.Sprintf("Room %d", i),
})}, roomIDs...)
}
res := alice.SlidingSync(t, sync3.Request{
2023-01-18 15:31:44 +00:00
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 10}},
},
},
})
2023-01-18 15:31:44 +00:00
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(10), m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3SyncOp(0, 9, roomIDs),
)))
// now expand the window
res = alice.SlidingSync(t, sync3.Request{
2023-01-18 15:31:44 +00:00
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
2023-01-18 15:31:44 +00:00
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(10), m.MatchV3Ops()))
}
// Regression test for Element X which has 2 identical lists and then changes the ranges in weird ways,
// causing the proxy to send back bad data. The request data here matches what EX sent.
func TestMultipleSameList(t *testing.T) {
alice := registerNewUser(t)
// the account had 16 rooms, so do this now
var roomIDs []string // most recent first
for i := 0; i < 16; i++ {
time.Sleep(time.Millisecond) // ensure creation timestamp changes
2023-10-11 12:23:46 +01:00
roomIDs = append([]string{alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": fmt.Sprintf("Room %d", i),
})}, roomIDs...)
}
firstList := sync3.RequestList{
Sort: []string{sync3.SortByRecency, sync3.SortByName},
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{{"m.room.avatar", ""}, {"m.room.encryption", ""}},
TimelineLimit: 10,
},
}
secondList := sync3.RequestList{
Sort: []string{sync3.SortByRecency, sync3.SortByName},
Ranges: sync3.SliceRanges{{0, 16}},
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{{"m.room.avatar", ""}, {"m.room.encryption", ""}},
},
}
res := alice.SlidingSync(t, sync3.Request{
2023-01-18 15:31:44 +00:00
Lists: map[string]sync3.RequestList{
"1": firstList, "2": secondList,
},
})
m.MatchResponse(t, res,
2023-01-18 15:31:44 +00:00
m.MatchList("1", m.MatchV3Count(16), m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3SyncOp(0, 15, roomIDs, false),
)),
2023-01-18 15:31:44 +00:00
m.MatchList("2", m.MatchV3Count(16), m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3SyncOp(0, 15, roomIDs, false),
)),
)
// now change both list ranges in a valid but strange way, and get back bad responses
firstList.Ranges = sync3.SliceRanges{{2, 15}} // from [0,20]
secondList.Ranges = sync3.SliceRanges{{0, 20}} // from [0,16]
res = alice.SlidingSync(t, sync3.Request{
2023-01-18 15:31:44 +00:00
Lists: map[string]sync3.RequestList{
"1": firstList, "2": secondList,
},
}, WithPos(res.Pos))
m.MatchResponse(t, res,
2023-01-18 15:31:44 +00:00
m.MatchList("1", m.MatchV3Count(16), m.MatchV3Ops(
m.MatchV3InvalidateOp(0, 1),
)),
2023-01-18 15:31:44 +00:00
m.MatchList("2", m.MatchV3Count(16), m.MatchV3Ops()),
)
}
// NB: assumes bump_event_types is sticky
func TestBumpEventTypesHandling(t *testing.T) {
2023-05-24 18:00:52 +01:00
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
charlie := registerNamedUser(t, "charlie")
t.Log("Alice creates two rooms")
2023-10-11 12:23:46 +01:00
room1 := alice.MustCreateRoom(
t,
map[string]interface{}{
"preset": "public_chat",
"name": "room1",
},
)
2023-10-11 12:23:46 +01:00
room2 := alice.MustCreateRoom(
t,
map[string]interface{}{
"preset": "public_chat",
"name": "room2",
},
)
t.Logf("room1=%s room2=%s", room1, room2)
t.Log("Bob joins both rooms.")
bob.JoinRoom(t, room1, nil)
bob.JoinRoom(t, room2, nil)
t.Log("Bob sends a message in room 2 then room 1.")
2023-10-11 12:23:46 +01:00
bob.SendEventSynced(t, room2, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": "Hi room 2",
"msgtype": "m.text",
},
})
2023-10-11 12:23:46 +01:00
bob.SendEventSynced(t, room1, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": "Hello world",
"msgtype": "m.text",
},
})
t.Log("Alice requests a sliding sync that bumps rooms on messages only.")
aliceReqList := sync3.RequestList{
Sort: []string{sync3.SortByRecency, sync3.SortByName},
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{{"m.room.avatar", ""}, {"m.room.encryption", ""}},
TimelineLimit: 10,
},
2023-05-24 15:50:53 +01:00
BumpEventTypes: []string{"m.room.message", "m.room.encrypted"},
}
aliceSyncRequest := sync3.Request{
Lists: map[string]sync3.RequestList{
2023-05-24 18:00:52 +01:00
"alice_list": aliceReqList,
},
}
2023-03-21 13:20:14 +00:00
// This sync should include both of Bob's messages. The proxy will make an initial
// V2 sync to the HS, which should include the latest event in both rooms.
// TODO: we could capture the event IDs above and assert this explicitly.
aliceRes := alice.SlidingSync(t, aliceSyncRequest)
t.Log("Alice's sync response should include room1 ahead of room 2.")
2023-05-24 18:00:52 +01:00
matchRoom1ThenRoom2 := []m.ListMatcher{
m.MatchV3Count(2),
m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3SyncOp(0, 1, []string{room1, room2}, false),
2023-05-24 18:00:52 +01:00
),
}
m.MatchResponse(t, aliceRes, m.MatchList("alice_list", matchRoom1ThenRoom2...))
t.Log("Bob requests a sliding sync that bumps rooms on messages and memberships.")
bobReqList := sync3.RequestList{
Sort: []string{sync3.SortByRecency, sync3.SortByName},
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{{"m.room.avatar", ""}, {"m.room.encryption", ""}},
TimelineLimit: 10,
},
2023-05-24 15:50:53 +01:00
BumpEventTypes: []string{"m.room.message", "m.room.encrypted", "m.room.member"},
}
bobSyncRequest := sync3.Request{
Lists: map[string]sync3.RequestList{
2023-05-24 18:00:52 +01:00
"bob_list": bobReqList,
},
}
bobRes := bob.SlidingSync(t, bobSyncRequest)
t.Log("Bob should also see room 1 ahead of room 2 in his sliding sync response.")
2023-05-24 18:00:52 +01:00
m.MatchResponse(t, bobRes, m.MatchList("bob_list", matchRoom1ThenRoom2...))
t.Log("Charlie joins room 2.")
charlie.JoinRoom(t, room2, nil)
t.Log("Alice syncs until she sees Charlie's membership.")
aliceRes = alice.SlidingSyncUntilMembership(t, aliceRes.Pos, room2, charlie, "join")
t.Log("Alice shouldn't see any rooms' positions change.")
2023-03-21 13:20:14 +00:00
m.MatchResponse(
t,
aliceRes,
2023-05-24 18:00:52 +01:00
m.MatchList("alice_list", m.MatchV3Count(2)),
2023-03-21 13:20:14 +00:00
m.MatchNoV3Ops(),
)
t.Log("Bob syncs until he sees Charlie's membership.")
bobRes = bob.SlidingSyncUntilMembership(t, bobRes.Pos, room2, charlie, "join")
t.Log("Bob should see room 2 at the top of his list.")
2023-05-24 18:00:52 +01:00
matchBobSeesRoom2Bumped := m.MatchList("bob_list",
m.MatchV3Count(2),
m.MatchV3Ops(
m.MatchV3DeleteOp(1),
m.MatchV3InsertOp(0, room2),
),
)
m.MatchResponse(t, bobRes, matchBobSeesRoom2Bumped)
2023-04-25 17:45:18 +01:00
// The read receipt stuff here specifically checks for the bug in
// https://github.com/matrix-org/sliding-sync/issues/83
aliceRoom2Timeline := aliceRes.Rooms[room2].Timeline
aliceLastSeenEvent := aliceRoom2Timeline[len(aliceRoom2Timeline)-1]
aliceLastSeenEventID := gjson.ParseBytes(aliceLastSeenEvent).Get("event_id").Str
if aliceLastSeenEventID == "" {
t.Error("Could not find event ID for the last event in Alice's timeline.")
}
2023-04-25 17:45:18 +01:00
t.Log("Alice marks herself as having seen Charlie's join.")
alice.SendReceipt(t, room2, aliceLastSeenEventID, "m.read")
2023-04-25 17:45:18 +01:00
t.Log("Alice syncs until she sees her receipt. At no point should see see any room list operations.")
alice.SlidingSyncUntil(
t,
aliceRes.Pos,
sync3.Request{Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue},
},
}},
func(response *sync3.Response) error {
if err := m.MatchNoV3Ops()(response); err != nil {
t.Fatalf("expected no ops while waiting for receipt: %s", err)
}
matchReceipt := m.MatchReceipts(room2, []m.Receipt{{
EventID: aliceLastSeenEventID,
2023-04-25 17:45:18 +01:00
UserID: alice.UserID,
Type: "m.read",
}})
return matchReceipt(response)
},
)
}
2023-05-25 18:10:27 +01:00
func TestBumpEventTypesInOverlappingLists(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
t.Log("Alice creates four rooms")
2023-10-11 12:23:46 +01:00
room1 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room1"})
room2 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room2"})
room3 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room3"})
room4 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room4"})
2023-05-25 18:10:27 +01:00
t.Log("Alice writes a message in all four rooms.")
// Note: all lists bump on messages, so this will ensure the recency order is sensible.
helloWorld := map[string]interface{}{"body": "Hello world", "msgtype": "m.text"}
2023-10-11 12:23:46 +01:00
alice.Unsafe_SendEventUnsynced(t, room1, b.Event{Type: "m.room.message", Content: helloWorld})
alice.Unsafe_SendEventUnsynced(t, room2, b.Event{Type: "m.room.message", Content: helloWorld})
alice.Unsafe_SendEventUnsynced(t, room3, b.Event{Type: "m.room.message", Content: helloWorld})
alice.SendEventSynced(t, room4, b.Event{Type: "m.room.message", Content: helloWorld})
2023-05-25 18:10:27 +01:00
t.Log("Alice requests a sync with three lists: one bumping on messages, a second bumping on messages and memberships, and a third bumping on all events.")
const listMsg = "message"
const listMsgMember = "message_membership"
const listAll = "all"
req := sync3.Request{
Lists: map[string]sync3.RequestList{
listMsg: {
Sort: []string{sync3.SortByRecency},
RoomSubscription: sync3.RoomSubscription{TimelineLimit: 10},
Ranges: sync3.SliceRanges{{0, 10}},
BumpEventTypes: []string{"m.room.message"},
},
listMsgMember: {
Sort: []string{sync3.SortByRecency},
RoomSubscription: sync3.RoomSubscription{TimelineLimit: 10},
Ranges: sync3.SliceRanges{{0, 10}},
BumpEventTypes: []string{"m.room.message", "m.room.member"},
},
listAll: {
Sort: []string{sync3.SortByRecency},
RoomSubscription: sync3.RoomSubscription{TimelineLimit: 10},
Ranges: sync3.SliceRanges{{0, 10}},
BumpEventTypes: nil,
},
},
}
res := alice.SlidingSync(t, req)
t.Log("Alice sees the rooms in order 4, 3, 2, 1.")
// Note: first sync, so first poll, so should see all four message events.
see4321 := []m.ListMatcher{
m.MatchV3Count(4),
m.MatchV3Ops(m.MatchV3SyncOp(0, 3, []string{room4, room3, room2, room1}, false)),
}
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
listMsg: see4321,
listMsgMember: see4321,
listAll: see4321,
}))
t.Log("Bob joins room 1. Alice syncs until she sees Bob's join.")
bob.JoinRoom(t, room1, nil)
res = alice.SlidingSyncUntilMembership(t, res.Pos, room1, bob, "join")
t.Logf("Alice should see room1 bumped in the lists %s and %s, but not %s", listMsgMember, listAll, listMsg)
noMovement := []m.ListMatcher{
m.MatchV3Count(4),
m.MatchV3Ops(),
}
bumpToTop := func(roomID string, fromIdx int) []m.ListMatcher {
return []m.ListMatcher{
m.MatchV3Count(4),
m.MatchV3Ops(m.MatchV3DeleteOp(fromIdx), m.MatchV3InsertOp(0, roomID)),
}
}
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
listMsg: noMovement, // 4321
listMsgMember: bumpToTop(room1, 3), // 4321 -> 1432
listAll: bumpToTop(room1, 3), // 4321 -> 1432
}))
t.Log("Alice sets a room topic in room 3, and syncs until she sees the topic.")
2023-10-11 12:23:46 +01:00
topicEventID := alice.Unsafe_SendEventUnsynced(t, room3, b.Event{
Type: "m.room.topic",
StateKey: ptr(""),
Content: map[string]interface{}{"topic": "spicy meatballs"},
})
2023-05-25 18:10:27 +01:00
res = alice.SlidingSyncUntilEventID(t, res.Pos, room3, topicEventID)
t.Logf("Alice sees room3 bump in the %s list only", listAll)
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
listMsg: noMovement, // 4321
listMsgMember: noMovement, // 1432
listAll: bumpToTop(room3, 2), // 1432 -> 3142
}))
t.Logf("Alice sends a message in room 2, and syncs until she sees it.")
2023-10-11 12:23:46 +01:00
msgEventID := alice.Unsafe_SendEventUnsynced(t, room2, b.Event{Type: "m.room.message", Content: helloWorld})
2023-05-25 18:10:27 +01:00
res = alice.SlidingSyncUntilEventID(t, res.Pos, room2, msgEventID)
t.Logf("Alice sees room2 bump in all lists")
m.MatchResponse(t, res, m.MatchLists(map[string][]m.ListMatcher{
listMsg: bumpToTop(room2, 2), // 4321 -> 2431
listMsgMember: bumpToTop(room2, 3), // 1432 -> 2143
listAll: bumpToTop(room2, 3), // 3142 -> 2314
}))
}
func TestBumpEventTypesDoesntLeakOnNewConnAfterJoin(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
t.Log("Alice creates a room and sends a secret state event.")
2023-10-11 12:23:46 +01:00
room1 := alice.MustCreateRoom(
t,
map[string]interface{}{
"preset": "public_chat",
"name": "room1",
},
)
2023-10-11 12:23:46 +01:00
alice.Unsafe_SendEventUnsynced(t, room1, b.Event{
Type: "secret",
StateKey: ptr(""),
Content: map[string]interface{}{},
})
t.Log("Bob creates a room and sends a secret state event.")
time.Sleep(1 * time.Millisecond)
2023-10-11 12:23:46 +01:00
room2 := bob.MustCreateRoom(
t,
map[string]interface{}{
"preset": "public_chat",
"name": "room1",
},
)
2023-10-11 12:23:46 +01:00
bob.Unsafe_SendEventUnsynced(t, room2, b.Event{
Type: "secret",
StateKey: ptr(""),
Content: map[string]interface{}{},
})
t.Log("Alice invites Bob, who accepts.")
alice.InviteRoom(t, room1, bob.UserID)
bob.JoinRoom(t, room1, nil)
t.Log("Bob sliding syncs, requesting that rooms are bumped on the secret event type.")
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
RoomSubscription: sync3.RoomSubscription{},
Ranges: sync3.SliceRanges{{0, 1}},
Sort: []string{sync3.SortByRecency},
BumpEventTypes: []string{"secret"},
},
},
})
t.Log("Bob should see room1 ahead of room2.")
// The order in which things happen:
// 1. alice: room1 secret event
// 2. bob: room2 secret event
// 3. alice: invite bob to room 1
// 4. bob: join room 1
// Bob can only see (2), (3) and (4), which means that room 1 has had most recent activity.
// (If we use the secret events' timestamps alone, without considering what Bob has
// permission to see, we will only consider (1) and (2), which would mean room 2
// has had the most recent activity.)
m.MatchResponse(
t,
res,
m.MatchList(
"a",
m.MatchV3Count(2),
m.MatchV3Ops(m.MatchV3SyncOp(0, 1, []string{room1, room2})),
),
)
}
// Like TestBumpEventTypesDoesntLeakOnNewConnAfterJoin, but Bob never accepts the invite.
func TestBumpEventTypesDoesntLeakOnNewConnAfterInvite(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
t.Log("Alice creates a room and sends a secret state event.")
2023-10-11 12:23:46 +01:00
room1 := alice.MustCreateRoom(
t,
map[string]interface{}{
"preset": "public_chat",
"name": "room1",
},
)
2023-10-11 12:23:46 +01:00
alice.Unsafe_SendEventUnsynced(t, room1, b.Event{
Type: "secret",
StateKey: ptr(""),
Content: map[string]interface{}{},
})
t.Log("Bob creates a room and sends a secret state event.")
time.Sleep(1 * time.Millisecond)
2023-10-11 12:23:46 +01:00
room2 := bob.MustCreateRoom(
t,
map[string]interface{}{
"preset": "public_chat",
"name": "room1",
},
)
2023-10-11 12:23:46 +01:00
bob.Unsafe_SendEventUnsynced(t, room2, b.Event{
Type: "secret",
StateKey: ptr(""),
Content: map[string]interface{}{},
})
t.Log("Alice invites Bob, who does not respond.")
alice.InviteRoom(t, room1, bob.UserID)
t.Log("Bob sliding syncs, requesting that rooms are bumped on the secret event type.")
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
RoomSubscription: sync3.RoomSubscription{},
Ranges: sync3.SliceRanges{{0, 1}},
Sort: []string{sync3.SortByRecency},
BumpEventTypes: []string{"secret"},
},
},
})
t.Log("Bob should see room1 ahead of room2.")
// The order in which things happen:
// 1. alice: room1 secret event
// 2. bob: room2 secret event
// 3. alice: invite bob to room 1
// Bob can only see (2) and (3), which means that room 1 has had most recent activity.
// (If we use the secret events' timestamps alone, without considering what Bob has
// permission to see, we will only consider (1) and (2), which would mean room 2
// has had the most recent activity.)
m.MatchResponse(
t,
res,
m.MatchList(
"a",
m.MatchV3Count(2),
m.MatchV3Ops(m.MatchV3SyncOp(0, 1, []string{room1, room2})),
),
)
}
// Tests the scenario described at
// https://github.com/matrix-org/sliding-sync/pull/58#discussion_r1159850458
func TestRangeOutsideTotalRooms(t *testing.T) {
alice := registerNewUser(t)
t.Log("Alice makes three public rooms.")
2023-10-11 12:23:46 +01:00
room0 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "A"})
room1 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "B"})
room2 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "C"})
t.Log("Alice initial syncs, requesting room ranges [0, 1] and [8, 9]")
syncRes := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Sort: []string{sync3.SortByName},
Ranges: sync3.SliceRanges{{0, 1}, {8, 9}},
},
},
})
t.Log("Alice should only see rooms 01 in the sync response.")
m.MatchResponse(
t,
syncRes,
m.MatchList(
"a",
m.MatchV3Count(3),
m.MatchV3Ops(
m.MatchV3SyncOp(0, 1, []string{room0, room1}),
),
),
)
t.Log("Alice changes the sort order")
syncRes = alice.SlidingSync(
t,
sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Sort: []string{sync3.SortByRecency},
},
},
},
WithPos(syncRes.Pos),
)
m.MatchResponse(
t,
syncRes,
m.MatchList(
"a",
m.MatchV3Count(3),
m.MatchV3Ops(
m.MatchV3InvalidateOp(0, 1),
m.MatchV3SyncOp(0, 1, []string{room2, room1}),
),
),
)
}
2023-07-18 11:00:10 +01:00
// Nicked from Synapse's tests, see
// https://github.com/matrix-org/synapse/blob/2cacd0849a02d43f88b6c15ee862398159ab827c/tests/test_utils/__init__.py#L154-L161
// Resolution: 1×1, MIME type: image/png, Extension: png, Size: 67 B
var smallPNG = []byte(
"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82",
)
func TestAvatarFieldInRoomResponse(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
chris := registerNamedUser(t, "chris")
avatarURLs := map[string]struct{}{}
uploadAvatar := func(client *CSAPI, filename string) string {
avatar := alice.UploadContent(t, smallPNG, filename, "image/png")
if _, exists := avatarURLs[avatar]; exists {
t.Fatalf("New avatar %s has already been uploaded", avatar)
}
t.Logf("%s is uploaded as %s", filename, avatar)
avatarURLs[avatar] = struct{}{}
return avatar
}
t.Log("Alice, Bob and Chris upload and set an avatar.")
aliceAvatar := uploadAvatar(alice, "alice.png")
bobAvatar := uploadAvatar(bob, "bob.png")
chrisAvatar := uploadAvatar(chris, "chris.png")
alice.SetAvatar(t, aliceAvatar)
bob.SetAvatar(t, bobAvatar)
chris.SetAvatar(t, chrisAvatar)
t.Log("Alice makes a public room, a DM with herself, a DM with Bob, a DM with Chris, and a group-DM with Bob and Chris.")
2023-10-11 12:23:46 +01:00
public := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
2023-07-18 11:00:10 +01:00
// TODO: you can create a DM with yourself e.g. as below. It probably ought to have
// your own face as an avatar.
2023-10-11 12:23:46 +01:00
// dmAlice := alice.MustCreateRoom(t, map[string]interface{}{
2023-07-18 11:00:10 +01:00
// "preset": "trusted_private_chat",
// "is_direct": true,
// })
2023-10-11 12:23:46 +01:00
dmBob := alice.MustCreateRoom(t, map[string]interface{}{
2023-07-18 11:00:10 +01:00
"preset": "trusted_private_chat",
"is_direct": true,
"invite": []string{bob.UserID},
})
2023-10-11 12:23:46 +01:00
dmChris := alice.MustCreateRoom(t, map[string]interface{}{
2023-07-18 11:00:10 +01:00
"preset": "trusted_private_chat",
"is_direct": true,
"invite": []string{chris.UserID},
})
2023-10-11 12:23:46 +01:00
dmBobChris := alice.MustCreateRoom(t, map[string]interface{}{
2023-07-18 11:00:10 +01:00
"preset": "trusted_private_chat",
"is_direct": true,
"invite": []string{bob.UserID, chris.UserID},
})
alice.MustSetGlobalAccountData(t, "m.direct", map[string]any{
bob.UserID: []string{dmBob, dmBobChris},
chris.UserID: []string{dmChris, dmBobChris},
})
2023-07-18 11:00:10 +01:00
t.Logf("Rooms:\npublic=%s\ndmBob=%s\ndmChris=%s\ndmBobChris=%s", public, dmBob, dmChris, dmBobChris)
t.Log("Bob accepts his invites. Chris accepts none.")
bob.JoinRoom(t, dmBob, nil)
bob.JoinRoom(t, dmBobChris, nil)
t.Log("Alice makes an initial sliding sync.")
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"rooms": {
Ranges: sync3.SliceRanges{{0, 4}},
},
},
})
t.Log("Alice should see each room in the sync response with an appropriate avatar and DM flag")
2023-07-18 11:00:10 +01:00
m.MatchResponse(
t,
res,
m.MatchRoomSubscription(public, m.MatchRoomUnsetAvatar(), m.MatchRoomIsDM(false)),
m.MatchRoomSubscription(dmBob, m.MatchRoomAvatar(bob.AvatarURL), m.MatchRoomIsDM(true)),
m.MatchRoomSubscription(dmChris, m.MatchRoomAvatar(chris.AvatarURL), m.MatchRoomIsDM(true)),
m.MatchRoomSubscription(dmBobChris, m.MatchRoomUnsetAvatar(), m.MatchRoomIsDM(true)),
2023-07-18 11:00:10 +01:00
)
t.Run("Avatar not resent on message", func(t *testing.T) {
t.Log("Bob sends a sentinel message.")
2023-10-11 12:23:46 +01:00
sentinel := bob.SendEventSynced(t, dmBob, b.Event{
2023-07-18 11:00:10 +01:00
Type: "m.room.message",
Content: map[string]interface{}{
"body": "Hello world",
"msgtype": "m.text",
},
})
t.Log("Alice syncs until she sees the sentinel. She should not see the DM avatar change.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(response *sync3.Response) error {
matchNoAvatarChange := m.MatchRoomSubscription(dmBob, m.MatchRoomUnchangedAvatar())
if err := matchNoAvatarChange(response); err != nil {
t.Fatalf("Saw DM avatar change: %s", err)
}
matchSentinel := m.MatchRoomSubscription(dmBob, MatchRoomTimelineMostRecent(1, []Event{{ID: sentinel}}))
return matchSentinel(response)
})
})
t.Run("DM declined", func(t *testing.T) {
t.Log("Chris leaves his DM with Alice.")
chris.LeaveRoom(t, dmChris)
t.Log("Alice syncs until she sees Chris's leave.")
res = alice.SlidingSyncUntilMembership(t, res.Pos, dmChris, chris, "leave")
t.Log("Alice sees Chris's avatar vanish.")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmChris, m.MatchRoomUnsetAvatar()))
})
t.Run("Group DM declined", func(t *testing.T) {
t.Log("Chris leaves his group DM with Alice and Bob.")
chris.LeaveRoom(t, dmBobChris)
t.Log("Alice syncs until she sees Chris's leave.")
res = alice.SlidingSyncUntilMembership(t, res.Pos, dmBobChris, chris, "leave")
t.Log("Alice sees the room's avatar change to Bob's avatar.")
// Because this is now a DM room with exactly one other (joined|invited) member.
m.MatchResponse(t, res, m.MatchRoomSubscription(dmBobChris, m.MatchRoomAvatar(bob.AvatarURL)))
})
t.Run("Bob's avatar change propagates", func(t *testing.T) {
t.Log("Bob changes his avatar.")
bobAvatar2 := uploadAvatar(bob, "bob2.png")
bob.SetAvatar(t, bobAvatar2)
avatarChangeInDM := false
avatarChangeInGroupDM := false
t.Log("Alice syncs until she sees Bob's new avatar.")
res = alice.SlidingSyncUntil(
t,
res.Pos,
sync3.Request{},
func(response *sync3.Response) error {
if !avatarChangeInDM {
err := m.MatchRoomSubscription(dmBob, m.MatchRoomAvatar(bob.AvatarURL))(response)
if err == nil {
avatarChangeInDM = true
}
}
if !avatarChangeInGroupDM {
err := m.MatchRoomSubscription(dmBobChris, m.MatchRoomAvatar(bob.AvatarURL))(response)
if err == nil {
avatarChangeInGroupDM = true
}
}
if avatarChangeInDM && avatarChangeInGroupDM {
return nil
}
return fmt.Errorf("still waiting: avatarChangeInDM=%t avatarChangeInGroupDM=%t", avatarChangeInDM, avatarChangeInGroupDM)
},
)
t.Log("Bob removes his avatar.")
bob.SetAvatar(t, "")
avatarChangeInDM = false
avatarChangeInGroupDM = false
t.Log("Alice syncs until she sees Bob's avatars vanish.")
res = alice.SlidingSyncUntil(
t,
res.Pos,
sync3.Request{},
func(response *sync3.Response) error {
if !avatarChangeInDM {
err := m.MatchRoomSubscription(dmBob, m.MatchRoomUnsetAvatar())(response)
if err == nil {
avatarChangeInDM = true
} else {
t.Log(err)
}
}
if !avatarChangeInGroupDM {
err := m.MatchRoomSubscription(dmBobChris, m.MatchRoomUnsetAvatar())(response)
if err == nil {
avatarChangeInGroupDM = true
} else {
t.Log(err)
}
}
if avatarChangeInDM && avatarChangeInGroupDM {
return nil
}
return fmt.Errorf("still waiting: avatarChangeInDM=%t avatarChangeInGroupDM=%t", avatarChangeInDM, avatarChangeInGroupDM)
},
)
})
t.Run("Explicit avatar propagates in non-DM room", func(t *testing.T) {
t.Log("Alice sets an avatar for the public room.")
publicAvatar := uploadAvatar(alice, "public.png")
2023-10-11 12:23:46 +01:00
alice.Unsafe_SendEventUnsynced(t, public, b.Event{
Type: "m.room.avatar",
StateKey: ptr(""),
Content: map[string]interface{}{
"url": publicAvatar,
},
2023-07-18 11:00:10 +01:00
})
t.Log("Alice syncs until she sees that avatar.")
res = alice.SlidingSyncUntil(
t,
res.Pos,
sync3.Request{},
m.MatchRoomSubscriptions(map[string][]m.RoomMatcher{
public: {m.MatchRoomAvatar(publicAvatar)},
}),
)
t.Log("Alice changes the avatar for the public room.")
publicAvatar2 := uploadAvatar(alice, "public2.png")
2023-10-11 12:23:46 +01:00
alice.Unsafe_SendEventUnsynced(t, public, b.Event{
Type: "m.room.avatar",
StateKey: ptr(""),
Content: map[string]interface{}{
"url": publicAvatar2,
},
2023-07-18 11:00:10 +01:00
})
t.Log("Alice syncs until she sees that avatar.")
res = alice.SlidingSyncUntil(
t,
res.Pos,
sync3.Request{},
m.MatchRoomSubscriptions(map[string][]m.RoomMatcher{
public: {m.MatchRoomAvatar(publicAvatar2)},
}),
)
t.Log("Alice removes the avatar for the public room.")
2023-10-11 12:23:46 +01:00
alice.Unsafe_SendEventUnsynced(t, public, b.Event{
Type: "m.room.avatar",
StateKey: ptr(""),
Content: map[string]interface{}{},
})
2023-07-18 11:00:10 +01:00
t.Log("Alice syncs until she sees that avatar vanish.")
res = alice.SlidingSyncUntil(
t,
res.Pos,
sync3.Request{},
m.MatchRoomSubscriptions(map[string][]m.RoomMatcher{
public: {m.MatchRoomUnsetAvatar()},
}),
)
})
t.Run("Explicit avatar propagates in DM room", func(t *testing.T) {
t.Log("Alice re-invites Chris to their DM.")
alice.InviteRoom(t, dmChris, chris.UserID)
t.Log("Alice syncs until she sees her invitation to Chris.")
res = alice.SlidingSyncUntilMembership(t, res.Pos, dmChris, chris, "invite")
t.Log("Alice should see the DM with Chris's avatar.")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmChris, m.MatchRoomAvatar(chris.AvatarURL)))
t.Log("Chris joins the room.")
chris.JoinRoom(t, dmChris, nil)
t.Log("Alice syncs until she sees Chris's join.")
res = alice.SlidingSyncUntilMembership(t, res.Pos, dmChris, chris, "join")
t.Log("Alice shouldn't see the DM's avatar change..")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmChris, m.MatchRoomUnchangedAvatar()))
t.Log("Chris gives their DM a bespoke avatar.")
dmAvatar := uploadAvatar(chris, "dm.png")
2023-10-11 12:23:46 +01:00
chris.Unsafe_SendEventUnsynced(t, dmChris, b.Event{
Type: "m.room.avatar",
StateKey: ptr(""),
Content: map[string]interface{}{
"url": dmAvatar,
},
2023-07-18 11:00:10 +01:00
})
t.Log("Alice syncs until she sees that avatar.")
alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, m.MatchRoomSubscription(dmChris, m.MatchRoomAvatar(dmAvatar)))
t.Log("Chris changes his global avatar, which adds a join event to the room.")
chrisAvatar2 := uploadAvatar(chris, "chris2.png")
chris.SetAvatar(t, chrisAvatar2)
t.Log("Alice syncs until she sees that join event.")
res = alice.SlidingSyncUntilMembership(t, res.Pos, dmChris, chris, "join")
t.Log("Her response should have either no avatar change, or the same bespoke avatar.")
// No change, ideally, but repeating the same avatar isn't _wrong_
m.MatchResponse(t, res, m.MatchRoomSubscription(dmChris, func(r sync3.Room) error {
noChangeErr := m.MatchRoomUnchangedAvatar()(r)
sameBespokeAvatarErr := m.MatchRoomAvatar(dmAvatar)(r)
if noChangeErr == nil || sameBespokeAvatarErr == nil {
return nil
}
return fmt.Errorf("expected no change or the same bespoke avatar (%s), got '%s'", dmAvatar, r.AvatarChange)
}))
t.Log("Chris updates the DM's avatar.")
dmAvatar2 := uploadAvatar(chris, "dm2.png")
2023-10-11 12:23:46 +01:00
chris.Unsafe_SendEventUnsynced(t, dmChris, b.Event{
Type: "m.room.avatar",
StateKey: ptr(""),
Content: map[string]interface{}{
"url": dmAvatar2,
},
2023-07-18 11:00:10 +01:00
})
t.Log("Alice syncs until she sees that avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, m.MatchRoomSubscription(dmChris, m.MatchRoomAvatar(dmAvatar2)))
t.Log("Chris removes the DM's avatar.")
2023-10-11 12:23:46 +01:00
chris.Unsafe_SendEventUnsynced(t, dmChris, b.Event{
Type: "m.room.avatar",
StateKey: ptr(""),
Content: map[string]interface{}{},
})
2023-07-18 11:00:10 +01:00
t.Log("Alice syncs until the DM avatar returns to Chris's most recent avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, m.MatchRoomSubscription(dmChris, m.MatchRoomAvatar(chris.AvatarURL)))
})
t.Run("Changing DM flag", func(t *testing.T) {
t.Skip("TODO: unimplemented")
t.Log("Alice clears the DM flag on Bob's room.")
2023-10-11 12:23:46 +01:00
alice.MustSetGlobalAccountData(t, "m.direct", map[string]interface{}{
2023-07-18 11:00:10 +01:00
"content": map[string][]string{
bob.UserID: {}, // no dmBob here
chris.UserID: {dmChris, dmBobChris},
},
})
t.Log("Alice syncs until she sees a new set of account data.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{
Extensions: extensions.Request{
AccountData: &extensions.AccountDataRequest{
extensions.Core{Enabled: &boolTrue},
},
},
}, func(response *sync3.Response) error {
if response.Extensions.AccountData == nil {
return fmt.Errorf("no account data yet")
}
if len(response.Extensions.AccountData.Global) == 0 {
return fmt.Errorf("no global account data yet")
}
return nil
})
t.Log("The DM with Bob should no longer be a DM and should no longer have an avatar.")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmBob, func(r sync3.Room) error {
if r.IsDM {
return fmt.Errorf("dmBob is still a DM")
}
return m.MatchRoomUnsetAvatar()(r)
}))
t.Log("Alice sets the DM flag on Bob's room.")
2023-10-11 12:23:46 +01:00
alice.MustSetGlobalAccountData(t, "m.direct", map[string]interface{}{
2023-07-18 11:00:10 +01:00
"content": map[string][]string{
bob.UserID: {dmBob}, // dmBob reinstated
chris.UserID: {dmChris, dmBobChris},
},
})
t.Log("Alice syncs until she sees a new set of account data.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{
Extensions: extensions.Request{
AccountData: &extensions.AccountDataRequest{
extensions.Core{Enabled: &boolTrue},
},
},
}, func(response *sync3.Response) error {
if response.Extensions.AccountData == nil {
return fmt.Errorf("no account data yet")
}
if len(response.Extensions.AccountData.Global) == 0 {
return fmt.Errorf("no global account data yet")
}
return nil
})
t.Log("The room should have Bob's avatar again.")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmBob, func(r sync3.Room) error {
if !r.IsDM {
return fmt.Errorf("dmBob is still not a DM")
}
return m.MatchRoomAvatar(bob.AvatarURL)(r)
}))
})
t.Run("See avatar when invited", func(t *testing.T) {
t.Log("Chris invites Alice to a DM.")
2023-10-11 12:23:46 +01:00
dmInvited := chris.MustCreateRoom(t, map[string]interface{}{
2023-07-18 11:00:10 +01:00
"preset": "trusted_private_chat",
"is_direct": true,
"invite": []string{alice.UserID},
})
t.Log("Alice syncs until she sees the invite.")
res = alice.SlidingSyncUntilMembership(t, res.Pos, dmInvited, alice, "invite")
t.Log("The new room should appear as a DM and use Chris's avatar.")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmInvited, m.MatchRoomIsDM(true), m.MatchRoomAvatar(chris.AvatarURL)))
t.Run("Creator of a non-DM never sees an avatar", func(t *testing.T) {
t.Log("Alice makes a new room which is not a DM.")
privateGroup := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "trusted_private_chat",
"is_direct": false,
})
t.Log("Alice sees the group. It has no avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, m.MatchRoomSubscription(privateGroup, m.MatchRoomUnsetAvatar()))
m.MatchResponse(t, res, m.MatchRoomSubscription(privateGroup, m.MatchRoomIsDM(false)))
t.Log("Alice invites Bob to the group, who accepts.")
alice.MustInviteRoom(t, privateGroup, bob.UserID)
bob.MustJoinRoom(t, privateGroup, nil)
t.Log("Alice sees Bob join. The room still has no avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(response *sync3.Response) error {
matchNoAvatarChange := m.MatchRoomSubscription(privateGroup, m.MatchRoomUnchangedAvatar())
if err := matchNoAvatarChange(response); err != nil {
t.Fatalf("Saw group avatar change: %s", err)
}
matchJoin := m.MatchRoomSubscription(privateGroup, MatchRoomTimelineMostRecent(1, []Event{
{
Type: "m.room.member",
Sender: bob.UserID,
StateKey: ptr(bob.UserID),
},
}))
return matchJoin(response)
})
t.Log("Alice invites Chris to the group, who accepts.")
alice.MustInviteRoom(t, privateGroup, chris.UserID)
chris.MustJoinRoom(t, privateGroup, nil)
t.Log("Alice sees Chris join. The room still has no avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(response *sync3.Response) error {
matchNoAvatarChange := m.MatchRoomSubscription(privateGroup, m.MatchRoomUnchangedAvatar())
if err := matchNoAvatarChange(response); err != nil {
t.Fatalf("Saw group avatar change: %s", err)
}
matchJoin := m.MatchRoomSubscription(privateGroup, MatchRoomTimelineMostRecent(1, []Event{
{
Type: "m.room.member",
Sender: chris.UserID,
StateKey: ptr(chris.UserID),
},
}))
return matchJoin(response)
})
})
2023-07-18 11:00:10 +01:00
})
}
2024-01-22 10:05:32 +00:00
// Regression test for https://github.com/element-hq/element-x-ios/issues/2003
// Ensure that a group chat with 1 other person has no avatar field set. Only DMs should have this set.
func TestAvatarUnsetInTwoPersonRoom(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
bobAvatar := alice.UploadContent(t, smallPNG, "bob.png", "image/png")
bob.SetAvatar(t, bobAvatar)
2024-01-22 10:05:32 +00:00
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "trusted_private_chat",
"name": "Nice test room",
"invite": []string{bob.UserID},
})
res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
})
m.MatchResponse(t, res, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
roomID: {
m.MatchRoomUnsetAvatar(),
m.MatchInviteCount(1),
m.MatchRoomName("Nice test room"),
},
}))
}