Migrate lists_test to end-to-end tests

Add more helper functions like `WithPos` and `MatchTimeline`.
This commit is contained in:
Kegan Dougal 2022-07-26 17:54:58 +01:00
parent 86570aaff4
commit a40441e963
7 changed files with 549 additions and 598 deletions

View File

@ -27,6 +27,11 @@ const (
SharedSecret = "complement"
)
var (
boolTrue = true
boolFalse = false
)
type Event struct {
ID string `json:"event_id"`
Type string `json:"type"`
@ -53,6 +58,18 @@ type Event struct {
Redacts string `json:"redacts"`
}
func NewEncryptionEvent() Event {
return Event{
Type: "m.room.encryption",
StateKey: ptr(""),
Content: map[string]interface{}{
"algorithm": "m.megolm.v1.aes-sha2",
"rotation_period_ms": 604800000,
"rotation_period_msgs": 100,
},
}
}
type MessagesBatch struct {
Chunk []json.RawMessage `json:"chunk"`
Start string `json:"start"`
@ -510,6 +527,11 @@ func (c *CSAPI) MustDoFunc(t *testing.T, method string, paths []string, opts ...
// SlidingSync performs a single sliding sync request
func (c *CSAPI) SlidingSync(t *testing.T, data sync3.Request, opts ...RequestOpt) (resBody *sync3.Response) {
t.Helper()
if len(opts) == 0 {
opts = append(opts, WithQueries(url.Values{
"timeout": []string{"500"},
}))
}
opts = append(opts, WithJSONBody(t, data))
res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "unstable", "org.matrix.msc3575", "sync"}, opts...)
body := ParseJSON(t, res)

504
tests-e2e/lists_test.go Normal file
View File

@ -0,0 +1,504 @@
package syncv3_test
import (
"fmt"
"testing"
"time"
"github.com/matrix-org/sync-v3/sync3"
"github.com/matrix-org/sync-v3/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++ {
unencryptedRoomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
unencryptedRoomIDs = append([]string{unencryptedRoomID}, unencryptedRoomIDs...) // push in array
encryptedRoomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"initial_state": []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: []sync3.RequestList{
{
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsEncrypted: &boolTrue,
},
},
{
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([]m.ListMatcher{
m.MatchV3Count(len(encryptedRoomIDs)),
m.MatchV3Ops(m.MatchV3SyncOp(0, 2, encryptedRoomIDs[:3])),
}, []m.ListMatcher{
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: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
{
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([]m.ListMatcher{
m.MatchV3Count(len(encryptedRoomIDs)),
}, []m.ListMatcher{
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: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
{
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([]m.ListMatcher{
m.MatchV3Count(len(encryptedRoomIDs)),
m.MatchV3Ops(
m.MatchV3DeleteOp(2),
m.MatchV3InsertOp(0, encryptedRoomIDs[0]),
),
}, []m.ListMatcher{
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
groupRoomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
groupRoomIDs = append([]string{groupRoomID}, groupRoomIDs...) // push in array
dmRoomID := alice.CreateRoom(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
alice.SetGlobalAccountData(t, "m.direct", dmContent)
// request 2 lists, one set DM, one set no DM
res := alice.SlidingSync(t, sync3.Request{
Lists: []sync3.RequestList{
{
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsDM: &boolTrue,
},
},
{
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([]m.ListMatcher{
m.MatchV3Count(len(dmRoomIDs)),
m.MatchV3Ops(m.MatchV3SyncOp(0, 2, dmRoomIDs[:3])),
}, []m.ListMatcher{
m.MatchV3Count(len(groupRoomIDs)),
m.MatchV3Ops(m.MatchV3SyncOp(0, 2, groupRoomIDs[:3])),
}))
// now bring the last DM room to the top with a notif
pingEventID := alice.SendEventSynced(t, dmRoomIDs[len(dmRoomIDs)-1], 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: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchLists([]m.ListMatcher{
m.MatchV3Count(len(dmRoomIDs)),
m.MatchV3Ops(
m.MatchV3DeleteOp(2),
m.MatchV3InsertOp(0, dmRoomIDs[0]),
),
}, []m.ListMatcher{
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++ {
roomID := alice.CreateRoom(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: []sync3.RequestList{},
})
m.MatchResponse(t, res, m.MatchLists())
// now add a list
res = alice.SlidingSync(t, sync3.Request{
Lists: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchList(0, 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 {
createContent["initial_state"] = []Event{
NewEncryptionEvent(),
}
}
if isDM {
createContent["is_direct"] = true
createContent["invite"] = []string{dmUserID}
}
roomID := alice.CreateRoom(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)
alice.SetGlobalAccountData(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-- {
roomToEventID[allRoomIDs[i]] = alice.SendEventSynced(t, allRoomIDs[i], Event{
Type: "m.room.message",
Content: map[string]interface{}{"body": "ping", "msgtype": "m.text"},
})
}
// 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: []sync3.RequestList{
{
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,
},
},
{
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(0, m.MatchV3Ops(m.MatchV3SyncOp(0, 4, encryptedRoomIDs[:5]))),
m.MatchList(1, 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),
MatchRoomRequiredState([]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),
MatchRoomRequiredState([]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),
MatchRoomRequiredState([]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),
MatchRoomRequiredState([]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),
MatchRoomRequiredState([]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),
MatchRoomRequiredState([]Event{
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimelineMostRecent(1, []Event{{ID: roomToEventID[dmRoomIDs[2]]}}),
},
dmRoomIDs[3]: {
m.MatchRoomInitial(true),
MatchRoomRequiredState([]Event{
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimelineMostRecent(1, []Event{{ID: roomToEventID[dmRoomIDs[3]]}}),
},
dmRoomIDs[4]: {
m.MatchRoomInitial(true),
MatchRoomRequiredState([]Event{
{
Type: "m.room.power_levels",
StateKey: ptr(""),
},
}),
MatchRoomTimelineMostRecent(1, []Event{{ID: roomToEventID[dmRoomIDs[4]]}}),
},
}),
)
}

View File

@ -3,6 +3,7 @@ package syncv3_test
import (
"encoding/json"
"fmt"
"net/url"
"os"
"reflect"
"strings"
@ -33,6 +34,13 @@ func TestMain(m *testing.M) {
os.Exit(exitCode)
}
func WithPos(pos string) RequestOpt {
return WithQueries(url.Values{
"pos": []string{pos},
"timeout": []string{"500"}, // 0.5s
})
}
func assertEventsEqual(t *testing.T, wantList []Event, gotList []json.RawMessage) {
t.Helper()
err := eventsEqual(wantList, gotList)
@ -77,11 +85,13 @@ func eventsEqual(wantList []Event, gotList []json.RawMessage) error {
func MatchRoomTimelineMostRecent(n int, events []Event) m.RoomMatcher {
subset := events[len(events)-n:]
return func(r sync3.Room) error {
if len(r.Timeline) < len(subset) {
return fmt.Errorf("timeline length mismatch: got %d want at least %d", len(r.Timeline), len(subset))
}
gotSubset := r.Timeline[len(r.Timeline)-n:]
return eventsEqual(events, gotSubset)
return MatchRoomTimeline(subset)(r)
}
}
func MatchRoomTimeline(events []Event) m.RoomMatcher {
return func(r sync3.Room) error {
return eventsEqual(events, r.Timeline)
}
}

View File

@ -1,7 +1,6 @@
package syncv3_test
import (
"net/url"
"testing"
"github.com/matrix-org/sync-v3/sync3"
@ -82,10 +81,7 @@ func TestRoomStateTransitions(t *testing.T) {
},
},
},
}, WithQueries(url.Values{
"timeout": []string{"500"},
"pos": []string{bobRes.Pos},
}))
}, WithPos(bobRes.Pos))
m.MatchResponse(t, bobRes, m.MatchNoV3Ops(), m.MatchList(0, m.MatchV3Count(2)), m.MatchRoomSubscription(inviteRoomID,
MatchRoomRequiredState([]Event{
{

View File

@ -2,7 +2,6 @@ package syncv3_test
import (
"encoding/json"
"net/url"
"testing"
"github.com/matrix-org/sync-v3/sync3"
@ -79,10 +78,7 @@ func TestSecurityLiveStreamEventLeftLeak(t *testing.T) {
},
},
}},
}, WithQueries(url.Values{
"timeout": []string{"500"}, // 0.5s
"pos": []string{aliceRes.Pos},
}))
}, WithPos(aliceRes.Pos))
timeline := aliceRes.Rooms[roomID].Timeline
lastTwoEvents := timeline[len(timeline)-2:]
@ -125,10 +121,7 @@ func TestSecurityLiveStreamEventLeftLeak(t *testing.T) {
},
},
}},
}, WithQueries(url.Values{
"timeout": []string{"500"}, // 0.5s
"pos": []string{eveRes.Pos},
}))
}, WithPos(eveRes.Pos))
// the room is deleted from eve's point of view and she sees up to and including her kick event
m.MatchResponse(t, eveRes, m.MatchList(0, m.MatchV3Count(0), m.MatchV3Ops(m.MatchV3DeleteOp(0))), m.MatchRoomSubscription(
roomID, m.MatchRoomName(""), m.MatchRoomRequiredState(nil), m.MatchRoomTimelineMostRecent(1, []json.RawMessage{kickEvent}),
@ -194,10 +187,7 @@ func TestSecurityRoomSubscriptionLeak(t *testing.T) {
[2]int64{0, 10}, // doesn't matter
},
}},
}, WithQueries(url.Values{
"timeout": []string{"500"}, // 0.5s
"pos": []string{eveRes.Pos},
}))
}, WithPos(eveRes.Pos))
// Assert that Eve doesn't see anything
m.MatchResponse(t, eveRes, m.MatchList(0, m.MatchV3Count(1)), m.MatchNoV3Ops(), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{}))

View File

@ -1,571 +0,0 @@
package syncv3
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/matrix-org/sync-v3/sync2"
"github.com/matrix-org/sync-v3/sync3"
"github.com/matrix-org/sync-v3/testutils"
"github.com/matrix-org/sync-v3/testutils/m"
)
// Test that multiple lists can be independently scrolled through
func TestMultipleLists(t *testing.T) {
boolTrue := true
boolFalse := false
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
var allRooms []roomEvents
var encryptedRooms []roomEvents
var unencryptedRooms []roomEvents
baseTimestamp := time.Now()
// make 10 encrypted rooms and make 10 unencrypted rooms. Room 0 is most recent to ease checks
for i := 0; i < 10; i++ {
ts := baseTimestamp.Add(time.Duration(-1*i) * time.Second)
encRoom := roomEvents{
roomID: fmt.Sprintf("!encrypted_%d:localhost", i),
events: append(createRoomState(t, alice, ts), []json.RawMessage{
testutils.NewStateEvent(
t, "m.room.encryption", "", alice, map[string]interface{}{
"algorithm": "m.megolm.v1.aes-sha2",
"rotation_period_ms": 604800000,
"rotation_period_msgs": 100,
}, testutils.WithTimestamp(ts),
),
}...),
}
room := roomEvents{
roomID: fmt.Sprintf("!unencrypted_%d:localhost", i),
events: createRoomState(t, alice, ts),
}
allRooms = append(allRooms, []roomEvents{encRoom, room}...)
encryptedRooms = append(encryptedRooms, encRoom)
unencryptedRooms = append(unencryptedRooms, room)
}
v2.addAccount(alice, aliceToken)
v2.queueResponse(alice, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(allRooms...),
},
})
// request 2 lists, one set encrypted, one set unencrypted
res := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: []sync3.RequestList{
{
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsEncrypted: &boolTrue,
},
},
{
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([]m.ListMatcher{
m.MatchV3Count(len(encryptedRooms)),
m.MatchV3Ops(m.MatchV3SyncOpFn(func(op *sync3.ResponseOpRange) error {
// first 3 encrypted rooms
return checkRoomList(res, op, encryptedRooms[:3])
}),
),
}, []m.ListMatcher{
m.MatchV3Count(len(unencryptedRooms)),
m.MatchV3Ops(m.MatchV3SyncOpFn(func(op *sync3.ResponseOpRange) error {
// first 3 unencrypted rooms
return checkRoomList(res, op, unencryptedRooms[:3])
})),
}),
)
// now scroll one of the lists
res = v3.mustDoV3RequestWithPos(t, aliceToken, res.Pos, sync3.Request{
Lists: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
[2]int64{3, 5}, // next 3 rooms
},
},
},
})
m.MatchResponse(t, res, m.MatchLists([]m.ListMatcher{
m.MatchV3Count(len(encryptedRooms)),
}, []m.ListMatcher{
m.MatchV3Count(len(unencryptedRooms)),
m.MatchV3Ops(
m.MatchV3SyncOpFn(func(op *sync3.ResponseOpRange) error {
return checkRoomList(res, op, unencryptedRooms[3:6])
}),
),
}))
// now shift the last/oldest unencrypted room to an encrypted room and make sure both lists update
v2.queueResponse(alice, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: map[string]sync2.SyncV2JoinResponse{
unencryptedRooms[len(unencryptedRooms)-1].roomID: {
Timeline: sync2.TimelineResponse{
Events: []json.RawMessage{
testutils.NewStateEvent(
t, "m.room.encryption", "", alice, map[string]interface{}{
"algorithm": "m.megolm.v1.aes-sha2",
"rotation_period_ms": 604800000,
"rotation_period_msgs": 100,
},
),
},
},
},
},
},
})
v2.waitUntilEmpty(t, alice)
// update our source of truth: the last unencrypted room is now the first encrypted room
encryptedRooms = append([]roomEvents{unencryptedRooms[len(unencryptedRooms)-1]}, encryptedRooms...)
unencryptedRooms = unencryptedRooms[:len(unencryptedRooms)-1]
res = v3.mustDoV3RequestWithPos(t, aliceToken, res.Pos, sync3.Request{
Lists: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
[2]int64{3, 5}, // next 3 rooms
},
},
},
})
// 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
m.MatchResponse(t, res, m.MatchLists([]m.ListMatcher{
m.MatchV3Count(len(encryptedRooms)),
m.MatchV3Ops(
m.MatchV3DeleteOp(2),
m.MatchV3InsertOp(0, encryptedRooms[0].roomID),
),
}, []m.ListMatcher{
m.MatchV3Count(len(unencryptedRooms)),
}))
}
// Test that highlights / 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) {
boolTrue := true
boolFalse := false
one := 1
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
var allRooms []roomEvents
var dmRooms []roomEvents
var groupRooms []roomEvents
baseTimestamp := time.Now()
dmContent := map[string][]string{} // user_id -> [room_id]
// make 10 group rooms and make 10 DMs rooms. Room 0 is most recent to ease checks
for i := 0; i < 10; i++ {
ts := baseTimestamp.Add(time.Duration(-1*i) * time.Second)
dmUser := fmt.Sprintf("@dm_%d:localhost", i)
dmRoomID := fmt.Sprintf("!dm_%d:localhost", i)
dmRoom := roomEvents{
roomID: dmRoomID,
events: append(createRoomState(t, alice, ts), []json.RawMessage{
testutils.NewJoinEvent(
t, dmUser, testutils.WithTimestamp(ts),
),
}...),
}
groupRoom := roomEvents{
roomID: fmt.Sprintf("!group_%d:localhost", i),
events: createRoomState(t, alice, ts),
}
allRooms = append(allRooms, []roomEvents{dmRoom, groupRoom}...)
dmRooms = append(dmRooms, dmRoom)
groupRooms = append(groupRooms, groupRoom)
dmContent[dmUser] = []string{dmRoomID}
}
v2.addAccount(alice, aliceToken)
v2.queueResponse(alice, sync2.SyncResponse{
AccountData: sync2.EventsResponse{
Events: []json.RawMessage{
testutils.NewEvent(t, "m.direct", alice, dmContent),
},
},
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(allRooms...),
},
})
// request 2 lists, one set DM, one set no DM
res := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: []sync3.RequestList{
{
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
Filters: &sync3.RequestFilters{
IsDM: &boolTrue,
},
},
{
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([]m.ListMatcher{
m.MatchV3Count(len(dmRooms)),
m.MatchV3Ops(m.MatchV3SyncOpFn(func(op *sync3.ResponseOpRange) error {
// first 3 DM rooms
return checkRoomList(res, op, dmRooms[:3])
})),
}, []m.ListMatcher{
m.MatchV3Count(len(groupRooms)),
m.MatchV3Ops(m.MatchV3SyncOpFn(func(op *sync3.ResponseOpRange) error {
// first 3 group rooms
return checkRoomList(res, op, groupRooms[:3])
})),
}))
// now bring the last DM room to the top with a notif
pingMessage := testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "ping"})
v2.queueResponse(alice, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: map[string]sync2.SyncV2JoinResponse{
dmRooms[len(dmRooms)-1].roomID: {
UnreadNotifications: sync2.UnreadNotifications{
HighlightCount: &one,
},
Timeline: sync2.TimelineResponse{
Events: []json.RawMessage{
pingMessage,
},
},
},
},
},
})
v2.waitUntilEmpty(t, alice)
// update our source of truth
dmRooms = append([]roomEvents{dmRooms[len(dmRooms)-1]}, dmRooms[1:]...)
dmRooms[0].events = append(dmRooms[0].events, pingMessage)
// now get the delta: only the DM room should change
res = v3.mustDoV3RequestWithPos(t, aliceToken, res.Pos, sync3.Request{
Lists: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms still
},
},
},
})
m.MatchResponse(t, res, m.MatchLists([]m.ListMatcher{
m.MatchV3Count(len(dmRooms)),
m.MatchV3Ops(
m.MatchV3DeleteOp(2),
m.MatchV3InsertOp(0, dmRooms[0].roomID),
),
}, []m.ListMatcher{
m.MatchV3Count(len(groupRooms)),
}), m.MatchRoomSubscription(dmRooms[0].roomID, m.MatchRoomHighlightCount(1), m.MatchRoomTimelineMostRecent(1, dmRooms[0].events)))
}
// Test that a new list can be added mid-connection
func TestNewListMidConnection(t *testing.T) {
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
var allRooms []roomEvents
baseTimestamp := time.Now()
// make 10 rooms
for i := 0; i < 10; i++ {
ts := baseTimestamp.Add(time.Duration(-1*i) * time.Second)
room := roomEvents{
roomID: fmt.Sprintf("!%d:localhost", i),
events: createRoomState(t, alice, ts),
}
allRooms = append(allRooms, room)
}
v2.addAccount(alice, aliceToken)
v2.queueResponse(alice, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(allRooms...),
},
})
// first request no list
res := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: []sync3.RequestList{},
})
m.MatchResponse(t, res, m.MatchLists())
// now add a list
res = v3.mustDoV3RequestWithPos(t, aliceToken, res.Pos, sync3.Request{
Lists: []sync3.RequestList{
{
Ranges: sync3.SliceRanges{
[2]int64{0, 2}, // first 3 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
},
},
})
m.MatchResponse(t, res, m.MatchList(0, m.MatchV3Count(len(allRooms)), m.MatchV3Ops(
m.MatchV3SyncOpFn(func(op *sync3.ResponseOpRange) error {
return checkRoomList(res, op, allRooms[0:3])
}),
)))
}
// Tests that if a room appears in >1 list that we union room subscriptions correctly.
func TestMultipleOverlappingLists(t *testing.T) {
boolTrue := true
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
var allRooms []roomEvents
var encryptedRooms []roomEvents
var encryptedRoomIDs []string
var dmRooms []roomEvents
var dmRoomIDs []string
dmContent := map[string][]string{} // user_id -> [room_id]
dmUser := bob
baseTimestamp := time.Now()
// make 3 encrypted rooms, 3 encrypted/dm rooms, 3 dm rooms.
for i := 0; i < 9; i++ {
isEncrypted := i < 6
isDM := i >= 3
ts := baseTimestamp.Add(time.Duration(-1*i) * time.Second)
room := roomEvents{
roomID: fmt.Sprintf("!room_%d:localhost", i),
events: createRoomState(t, alice, ts),
}
if isEncrypted {
room.events = append(room.events, testutils.NewStateEvent(
t, "m.room.encryption", "", alice, map[string]interface{}{
"algorithm": "m.megolm.v1.aes-sha2",
"rotation_period_ms": 604800000,
"rotation_period_msgs": 100,
}, testutils.WithTimestamp(ts),
))
}
if isDM {
dmContent[dmUser] = append(dmContent[dmUser], room.roomID)
}
allRooms = append(allRooms, room)
if isEncrypted {
encryptedRooms = append(encryptedRooms, room)
encryptedRoomIDs = append(encryptedRoomIDs, room.roomID)
}
if isDM {
dmRooms = append(dmRooms, room)
dmRoomIDs = append(dmRoomIDs, room.roomID)
}
}
v2.addAccount(alice, aliceToken)
v2.queueResponse(alice, sync2.SyncResponse{
AccountData: sync2.EventsResponse{
Events: []json.RawMessage{
testutils.NewEvent(t, "m.direct", alice, dmContent),
},
},
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(allRooms...),
},
})
// 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* ED* D D D
// `-----------`
// top 5 Encrypted
//
// Rooms with * are union'd
res := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: []sync3.RequestList{
{
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 4}, // first 5 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 3,
RequiredState: [][2]string{
{"m.room.join_rules", ""},
},
},
Filters: &sync3.RequestFilters{
IsEncrypted: &boolTrue,
},
},
{
Sort: []string{sync3.SortByRecency},
Ranges: sync3.SliceRanges{
[2]int64{0, 4}, // first 5 rooms
},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
RequiredState: [][2]string{
{"m.room.power_levels", ""},
},
},
Filters: &sync3.RequestFilters{
IsDM: &boolTrue,
},
},
},
})
m.MatchResponse(t, res,
m.MatchList(0, m.MatchV3Ops(m.MatchV3SyncOp(0, 4, encryptedRoomIDs[:5]))),
m.MatchList(1, 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),
m.MatchRoomRequiredState([]json.RawMessage{
encryptedRooms[0].getStateEvent("m.room.join_rules", ""),
}),
m.MatchRoomTimelineMostRecent(3, encryptedRooms[0].events),
},
encryptedRoomIDs[1]: {
m.MatchRoomInitial(true),
m.MatchRoomRequiredState([]json.RawMessage{
encryptedRooms[1].getStateEvent("m.room.join_rules", ""),
}),
m.MatchRoomTimelineMostRecent(3, encryptedRooms[1].events),
},
encryptedRoomIDs[2]: {
m.MatchRoomInitial(true),
m.MatchRoomRequiredState([]json.RawMessage{
encryptedRooms[2].getStateEvent("m.room.join_rules", ""),
}),
m.MatchRoomTimelineMostRecent(3, encryptedRooms[2].events),
},
// overlapping with DM rooms
encryptedRoomIDs[3]: {
m.MatchRoomInitial(true),
m.MatchRoomRequiredState([]json.RawMessage{
encryptedRooms[3].getStateEvent("m.room.join_rules", ""),
encryptedRooms[3].getStateEvent("m.room.power_levels", ""),
}),
m.MatchRoomTimelineMostRecent(3, encryptedRooms[3].events),
},
encryptedRoomIDs[4]: {
m.MatchRoomInitial(true),
m.MatchRoomRequiredState([]json.RawMessage{
encryptedRooms[4].getStateEvent("m.room.join_rules", ""),
encryptedRooms[4].getStateEvent("m.room.power_levels", ""),
}),
m.MatchRoomTimelineMostRecent(3, encryptedRooms[4].events),
},
// DM only rooms
dmRoomIDs[2]: {
m.MatchRoomInitial(true),
m.MatchRoomRequiredState([]json.RawMessage{
dmRooms[2].getStateEvent("m.room.power_levels", ""),
}),
m.MatchRoomTimelineMostRecent(1, dmRooms[2].events),
},
dmRoomIDs[3]: {
m.MatchRoomInitial(true),
m.MatchRoomRequiredState([]json.RawMessage{
dmRooms[3].getStateEvent("m.room.power_levels", ""),
}),
m.MatchRoomTimelineMostRecent(1, dmRooms[3].events),
},
dmRoomIDs[4]: {
m.MatchRoomInitial(true),
m.MatchRoomRequiredState([]json.RawMessage{
dmRooms[4].getStateEvent("m.room.power_levels", ""),
}),
m.MatchRoomTimelineMostRecent(1, dmRooms[4].events),
},
}),
)
}
// Check that the range op matches all the wantRooms
func checkRoomList(res *sync3.Response, op *sync3.ResponseOpRange, wantRooms []roomEvents) error {
if len(op.RoomIDs) != len(wantRooms) {
return fmt.Errorf("want %d rooms, got %d", len(wantRooms), len(op.RoomIDs))
}
for i := range wantRooms {
err := wantRooms[i].MatchRoom(op.RoomIDs[i],
res.Rooms[op.RoomIDs[i]],
m.MatchRoomTimelineMostRecent(1, wantRooms[i].events),
)
if err != nil {
return err
}
}
return nil
}

View File

@ -378,10 +378,10 @@ func MatchAccountData(globals []json.RawMessage, rooms map[string][]json.RawMess
}
}
func CheckList(res sync3.ResponseList, matchers ...ListMatcher) error {
func CheckList(i int, res sync3.ResponseList, matchers ...ListMatcher) error {
for _, m := range matchers {
if err := m(res); err != nil {
return fmt.Errorf("MatchList: %v", err)
return fmt.Errorf("MatchList[%d]: %v", i, err)
}
}
return nil
@ -393,7 +393,7 @@ func MatchList(i int, matchers ...ListMatcher) RespMatcher {
return fmt.Errorf("MatchSingleList: index %d does not exist, got %d lists", i, len(res.Lists))
}
list := res.Lists[i]
return CheckList(list, matchers...)
return CheckList(i, list, matchers...)
}
}
@ -403,7 +403,7 @@ func MatchLists(matchers ...[]ListMatcher) RespMatcher {
return fmt.Errorf("MatchLists: got %d matchers for %d lists", len(matchers), len(res.Lists))
}
for i := range matchers {
if err := CheckList(res.Lists[i], matchers[i]...); err != nil {
if err := CheckList(i, res.Lists[i], matchers[i]...); err != nil {
return fmt.Errorf("MatchLists[%d]: %v", i, err)
}
}