sliding-sync/tests-integration/regressions_test.go

466 lines
15 KiB
Go
Raw Normal View History

2023-07-11 19:08:32 +01:00
package syncv3
import (
"encoding/json"
"net/http"
2023-07-11 19:08:32 +01:00
"testing"
"time"
"github.com/matrix-org/sliding-sync/sync2"
"github.com/matrix-org/sliding-sync/sync3"
2023-09-19 13:47:58 +01:00
"github.com/matrix-org/sliding-sync/sync3/extensions"
2023-07-11 19:08:32 +01:00
"github.com/matrix-org/sliding-sync/testutils"
"github.com/matrix-org/sliding-sync/testutils/m"
)
// catch all file for any kind of regression test which doesn't fall into a unique category
// Regression test for https://github.com/matrix-org/sliding-sync/issues/192
// - Bob on his server invites Alice to a room.
// - Alice joins the room first over federation. Proxy does the right thing and sets her membership to join. There is no timeline though due to not having backfilled.
// - Alice's client backfills in the room which pulls in the invite event, but the SS proxy doesn't see it as it's backfill, not /sync.
// - Charlie joins the same room via SS, which makes the SS proxy see 50 timeline events, which includes the invite.
// As the proxy has never seen this invite event before, it assumes it is newer than the join event and inserts it, corrupting state.
//
// Manually confirmed this can happen with 3x Element clients. We need to make sure we drop those earlier events.
// The first join over federation presents itself as a single join event in the timeline, with the create event, etc in state.
func TestBackfillInviteDoesntCorruptState(t *testing.T) {
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
fedBob := "@bob:over_federation"
charlie := "@charlie:localhost"
charlieToken := "CHARLIE_TOKEN"
joinEvent := testutils.NewJoinEvent(t, alice)
room := roomEvents{
roomID: "!TestBackfillInviteDoesntCorruptState:localhost",
events: []json.RawMessage{
joinEvent,
},
state: createRoomState(t, fedBob, time.Now()),
}
v2.addAccount(t, alice, aliceToken)
v2.queueResponse(alice, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(room),
},
})
// alice syncs and should see the room.
aliceRes := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 5,
},
},
},
})
m.MatchResponse(t, aliceRes, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(m.MatchV3SyncOp(0, 0, []string{room.roomID}))))
// Alice's client "backfills" new data in, meaning the next user who joins is going to see a different set of timeline events
dummyMsg := testutils.NewMessageEvent(t, fedBob, "you didn't see this before joining")
charlieJoinEvent := testutils.NewJoinEvent(t, charlie)
backfilledTimelineEvents := append(
room.state, []json.RawMessage{
dummyMsg,
testutils.NewStateEvent(t, "m.room.member", alice, fedBob, map[string]interface{}{
"membership": "invite",
}),
joinEvent,
charlieJoinEvent,
}...,
)
// now charlie also joins the room, causing a different response from /sync v2
v2.addAccount(t, charlie, charlieToken)
v2.queueResponse(charlie, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(roomEvents{
roomID: room.roomID,
events: backfilledTimelineEvents,
}),
},
})
// and now charlie hits SS, which might corrupt membership state for alice.
charlieRes := v3.mustDoV3Request(t, charlieToken, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
})
m.MatchResponse(t, charlieRes, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(m.MatchV3SyncOp(0, 0, []string{room.roomID}))))
// alice should not see dummyMsg or the invite
aliceRes = v3.mustDoV3RequestWithPos(t, aliceToken, aliceRes.Pos, sync3.Request{})
m.MatchResponse(t, aliceRes, m.MatchNoV3Ops(), m.LogResponse(t), m.MatchRoomSubscriptionsStrict(
map[string][]m.RoomMatcher{
room.roomID: {
m.MatchJoinCount(3), // alice, bob, charlie,
m.MatchNoInviteCount(),
m.MatchNumLive(1),
m.MatchRoomTimeline([]json.RawMessage{charlieJoinEvent}),
},
},
))
}
2023-07-25 14:18:58 +01:00
2023-07-25 14:41:23 +01:00
func TestMalformedEventsTimeline(t *testing.T) {
2023-07-25 14:18:58 +01:00
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
// unusual events ARE VALID EVENTS and should be sent to the client, but are unusual for some reason.
unusualEvents := []json.RawMessage{
testutils.NewStateEvent(t, "", "", alice, map[string]interface{}{
"empty string": "for event type",
}),
}
// malformed events are INVALID and should be ignored by the proxy.
malformedEvents := []json.RawMessage{
json.RawMessage(`{}`), // empty object
json.RawMessage(`{"type":5}`), // type is an integer
json.RawMessage(`{"type":"foo","content":{},"event_id":""}`), // 0-length string as event ID
json.RawMessage(`{"type":"foo","content":{}}`), // missing event ID
2023-07-25 14:18:58 +01:00
}
room := roomEvents{
2023-07-25 14:41:23 +01:00
roomID: "!TestMalformedEventsTimeline:localhost",
// append malformed after unusual. All malformed events should be dropped,
// leaving only unusualEvents.
events: append(unusualEvents, malformedEvents...),
2023-07-25 14:18:58 +01:00
state: createRoomState(t, alice, time.Now()),
}
v2.addAccount(t, alice, aliceToken)
v2.queueResponse(alice, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(room),
},
})
// alice syncs and should see the room.
aliceRes := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: int64(len(unusualEvents)),
},
},
},
})
m.MatchResponse(t, aliceRes, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(m.MatchV3SyncOp(0, 0, []string{room.roomID}))),
m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
room.roomID: {
m.MatchJoinCount(1),
m.MatchRoomTimeline(unusualEvents),
},
}))
2023-07-25 14:41:23 +01:00
}
func TestMalformedEventsState(t *testing.T) {
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
// unusual events ARE VALID EVENTS and should be sent to the client, but are unusual for some reason.
unusualEvents := []json.RawMessage{
testutils.NewStateEvent(t, "", "", alice, map[string]interface{}{
"empty string": "for event type",
}),
}
// malformed events are INVALID and should be ignored by the proxy.
malformedEvents := []json.RawMessage{
json.RawMessage(`{}`), // empty object
json.RawMessage(`{"type":5,"content":{},"event_id":"f","state_key":""}`), // type is an integer
json.RawMessage(`{"type":"foo","content":{},"event_id":"","state_key":""}`), // 0-length string as event ID
json.RawMessage(`{"type":"foo","content":{},"state_key":""}`), // missing event ID
}
2023-07-25 14:18:58 +01:00
2023-07-25 14:41:23 +01:00
latestEvent := testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "hi"})
room := roomEvents{
roomID: "!TestMalformedEventsState:localhost",
events: []json.RawMessage{latestEvent},
// append malformed after unusual. All malformed events should be dropped,
// leaving only unusualEvents.
state: append(createRoomState(t, alice, time.Now()), append(unusualEvents, malformedEvents...)...),
}
v2.addAccount(t, alice, aliceToken)
v2.queueResponse(alice, sync2.SyncResponse{
Rooms: sync2.SyncRoomsResponse{
Join: v2JoinTimeline(room),
},
})
// alice syncs and should see the room.
aliceRes := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: int64(len(unusualEvents)),
RequiredState: [][2]string{{"", ""}},
},
},
},
})
m.MatchResponse(t, aliceRes, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(m.MatchV3SyncOp(0, 0, []string{room.roomID}))),
m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
room.roomID: {
m.MatchJoinCount(1),
m.MatchRoomTimeline([]json.RawMessage{latestEvent}),
m.MatchRoomRequiredState([]json.RawMessage{
unusualEvents[0],
}),
},
}))
2023-07-25 14:18:58 +01:00
}
// Regression test for https://github.com/matrix-org/sliding-sync/issues/295
// This test:
// - injects a good room and a bad room in the v2 response
// - then injects a good room update if the since token advanced
// - checks we see all updates
// In the past, the bad room in the first v2 response caused the proxy to retry and never advance,
// wedging the poller.
func TestBadCreateInitialiseDoesntWedgePolling(t *testing.T) {
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
goodRoom := "!good:localhost"
badRoom := "!bad:localhost"
v2.addAccount(t, alice, aliceToken)
// we should see the since token increment, if we see repeats it means
// we aren't returning DataErrors when we should be.
wantSinces := []string{"", "1", "2"}
ch := make(chan bool)
v2.checkRequest = func(token string, req *http.Request) {
if len(wantSinces) == 0 {
return
}
gotSince := req.URL.Query().Get("since")
t.Logf("checkRequest got since=%v", gotSince)
want := wantSinces[0]
wantSinces = wantSinces[1:]
if gotSince != want {
t.Errorf("v2.checkRequest since got '%v' want '%v'", gotSince, want)
}
if len(wantSinces) == 0 {
close(ch)
}
}
// initial sync, everything fine
v2.queueResponse(alice, sync2.SyncResponse{
NextBatch: "1",
Rooms: sync2.SyncRoomsResponse{
Join: map[string]sync2.SyncV2JoinResponse{
goodRoom: {
Timeline: sync2.TimelineResponse{
Events: createRoomState(t, alice, time.Now()),
},
},
},
},
})
aliceRes := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
},
},
})
// we should only see 1 room
m.MatchResponse(t, aliceRes, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
goodRoom: {
m.MatchJoinCount(1),
},
},
))
// now inject a bad room and some extra good event
extraGoodEvent := testutils.NewMessageEvent(t, alice, "Extra!", testutils.WithTimestamp(time.Now().Add(time.Second)))
v2.queueResponse(alice, sync2.SyncResponse{
NextBatch: "2",
Rooms: sync2.SyncRoomsResponse{
Join: map[string]sync2.SyncV2JoinResponse{
goodRoom: {
Timeline: sync2.TimelineResponse{
Events: []json.RawMessage{extraGoodEvent},
},
},
badRoom: {
State: sync2.EventsResponse{
// BAD: missing create event
Events: createRoomState(t, alice, time.Now())[1:],
},
Timeline: sync2.TimelineResponse{
Events: []json.RawMessage{
testutils.NewMessageEvent(t, alice, "Hello World"),
},
},
},
},
},
})
v2.waitUntilEmpty(t, alice)
// we should see the extra good event and not the bad room
aliceRes = v3.mustDoV3RequestWithPos(t, aliceToken, aliceRes.Pos, sync3.Request{})
// we should only see 1 room
m.MatchResponse(t, aliceRes, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
goodRoom: {
m.MatchRoomTimelineMostRecent(1, []json.RawMessage{extraGoodEvent}),
},
},
))
// make sure we've seen all the v2 requests
select {
case <-ch:
case <-time.After(time.Second):
t.Fatalf("timed out waiting for all v2 requests")
}
2023-09-19 13:47:58 +01:00
}
// Regression test for https://github.com/matrix-org/sliding-sync/issues/295
// This test:
// - injects to-device msgs and a bad room in the v2 response
// - checks we see the to-device msgs
// It's possible for the poller to bail early and skip over processing the remaining response
// which this test is trying to safeguard against.
func TestBadPollDataDoesntDropToDeviceMsgs(t *testing.T) {
pqString := testutils.PrepareDBConnectionString()
// setup code
v2 := runTestV2Server(t)
v3 := runTestServer(t, v2, pqString)
defer v2.close()
defer v3.close()
goodRoom := "!good:localhost"
badRoom := "!bad:localhost"
v2.addAccount(t, alice, aliceToken)
// we should see the since token increment, if we see repeats it means
// we aren't returning DataErrors when we should be.
wantSinces := []string{"", "1", "2"}
ch := make(chan bool)
v2.checkRequest = func(token string, req *http.Request) {
if len(wantSinces) == 0 {
return
}
gotSince := req.URL.Query().Get("since")
t.Logf("checkRequest got since=%v", gotSince)
want := wantSinces[0]
wantSinces = wantSinces[1:]
if gotSince != want {
t.Errorf("v2.checkRequest since got '%v' want '%v'", gotSince, want)
}
if len(wantSinces) == 0 {
close(ch)
}
}
// initial sync, everything fine
v2.queueResponse(alice, sync2.SyncResponse{
NextBatch: "1",
Rooms: sync2.SyncRoomsResponse{
Join: map[string]sync2.SyncV2JoinResponse{
goodRoom: {
Timeline: sync2.TimelineResponse{
Events: createRoomState(t, alice, time.Now()),
},
},
},
},
})
aliceRes := v3.mustDoV3Request(t, aliceToken, sync3.Request{
Extensions: extensions.Request{
ToDevice: &extensions.ToDeviceRequest{
Core: extensions.Core{
Enabled: &boolTrue,
},
},
},
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
},
},
},
})
// we should only see 1 room
m.MatchResponse(t, aliceRes, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
goodRoom: {
m.MatchJoinCount(1),
},
},
))
2023-09-19 13:47:58 +01:00
// now inject a bad room and some to-device events
toDeviceEvent := testutils.NewEvent(t, "m.todevice", alice, map[string]interface{}{"body": "testio"})
v2.queueResponse(alice, sync2.SyncResponse{
NextBatch: "2",
ToDevice: sync2.EventsResponse{
Events: []json.RawMessage{toDeviceEvent},
},
Rooms: sync2.SyncRoomsResponse{
Join: map[string]sync2.SyncV2JoinResponse{
badRoom: {
State: sync2.EventsResponse{
// BAD: missing create event
Events: createRoomState(t, alice, time.Now())[1:],
},
Timeline: sync2.TimelineResponse{
Events: []json.RawMessage{
testutils.NewMessageEvent(t, alice, "Hello World"),
},
},
},
},
},
})
v2.waitUntilEmpty(t, alice)
// we should see the to-device event and not the bad room
aliceRes = v3.mustDoV3RequestWithPos(t, aliceToken, aliceRes.Pos, sync3.Request{})
// we should only see the to-device event
m.MatchResponse(t, aliceRes,
m.LogResponse(t),
m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{}),
m.MatchToDeviceMessages([]json.RawMessage{toDeviceEvent}),
)
// make sure we've seen all the v2 requests
select {
case <-ch:
case <-time.After(time.Second):
t.Fatalf("timed out waiting for all v2 requests")
}
}