sliding-sync/tests-e2e/membership_transitions_test.go

863 lines
28 KiB
Go
Raw Permalink Normal View History

package syncv3_test
import (
2023-09-08 14:01:22 +02:00
"fmt"
"testing"
"time"
2023-10-11 12:23:46 +01:00
"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/client"
"github.com/matrix-org/sliding-sync/sync3"
"github.com/matrix-org/sliding-sync/testutils/m"
"github.com/tidwall/gjson"
)
func TestRoomStateTransitions(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
// make 4 rooms and set bob's membership state in each to a different value.
2023-10-11 12:23:46 +01:00
joinRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
bob.JoinRoom(t, joinRoomID, nil)
2023-10-11 12:23:46 +01:00
kickRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
bob.JoinRoom(t, kickRoomID, nil)
2023-10-11 12:23:46 +01:00
alice.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", kickRoomID, "kick"}, client.WithJSONBody(t, map[string]interface{}{
"user_id": bob.UserID,
}))
2023-10-11 12:23:46 +01:00
banRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
bob.JoinRoom(t, banRoomID, nil)
2023-10-11 12:23:46 +01:00
alice.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", banRoomID, "ban"}, client.WithJSONBody(t, map[string]interface{}{
"user_id": bob.UserID,
}))
2023-10-11 12:23:46 +01:00
inviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
alice.InviteRoom(t, inviteRoomID, bob.UserID)
// seed the proxy with Alice data
alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
{0, 100},
},
Sort: []string{sync3.SortByRecency},
},
},
})
// bob should see the invited/joined rooms
bobRes := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
{0, 100},
},
Sort: []string{sync3.SortByRecency},
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 1,
RequiredState: [][2]string{
{"m.room.create", ""},
},
},
},
},
})
m.MatchResponse(t, bobRes, m.MatchList("a", m.MatchV3Count(2), m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3SyncOp(0, 1, []string{inviteRoomID, joinRoomID}),
)), m.MatchRoomSubscriptions(map[string][]m.RoomMatcher{
inviteRoomID: {
m.MatchRoomHighlightCount(1),
m.MatchRoomInitial(true),
m.MatchRoomRequiredState(nil),
m.MatchInviteCount(1),
m.MatchJoinCount(1),
MatchRoomInviteState([]Event{
{
Type: "m.room.create",
StateKey: ptr(""),
// no content as it includes the room version which we don't want to guess/hardcode
},
{
Type: "m.room.join_rules",
StateKey: ptr(""),
Content: map[string]interface{}{
"join_rule": "public",
},
},
}, true),
},
joinRoomID: {},
}),
)
// now bob accepts the invite
bob.JoinRoom(t, inviteRoomID, nil)
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
// wait until the proxy has got this data
alice.SlidingSyncUntilMembership(t, "", inviteRoomID, bob, "join")
// the room should be updated with the initial flag set to replace what was in the invite state
bobRes = bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
{0, 100},
},
},
},
}, WithPos(bobRes.Pos))
m.MatchResponse(t, bobRes, m.MatchNoV3Ops(), m.MatchList("a", m.MatchV3Count(2)), m.MatchRoomSubscription(inviteRoomID,
MatchRoomRequiredStateStrict([]Event{
{
Type: "m.room.create",
StateKey: ptr(""),
},
}),
MatchRoomTimelineMostRecent(1, []Event{
{
Type: "m.room.member",
StateKey: ptr(bob.UserID),
Content: map[string]interface{}{
"membership": "join",
"displayname": bob.Localpart,
},
Sender: bob.UserID,
},
}),
m.MatchRoomInitial(true),
m.MatchJoinCount(2),
m.MatchInviteCount(0),
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
m.MatchRoomHighlightCount(0),
))
}
func TestInviteRejection(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
// ensure that invite state correctly propagates. One room will already be in 'invite' state
// prior to the first proxy sync, whereas the 2nd will transition.
2023-10-11 12:23:46 +01:00
firstInviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "name": "First"})
alice.InviteRoom(t, firstInviteRoomID, bob.UserID)
2023-10-11 12:23:46 +01:00
secondInviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "name": "Second"})
t.Logf("TestInviteRejection first %s second %s", firstInviteRoomID, secondInviteRoomID)
// sync as bob, we should see 1 invite
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Filters: &sync3.RequestFilters{
IsInvite: &boolTrue,
},
},
},
})
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(
2023-04-05 01:07:07 +01:00
m.MatchV3SyncOp(0, 0, []string{firstInviteRoomID}),
)), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
firstInviteRoomID: {
m.MatchRoomInitial(true),
m.MatchInviteCount(1),
m.MatchJoinCount(1),
MatchRoomInviteState([]Event{
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "First",
},
},
}, true),
},
}))
2023-10-11 12:23:46 +01:00
_, since := bob.MustSync(t, client.SyncReq{})
// now invite bob
alice.InviteRoom(t, secondInviteRoomID, bob.UserID)
2023-10-11 12:23:46 +01:00
since = bob.MustSyncUntil(t, client.SyncReq{Since: since}, client.SyncInvitedTo(bob.UserID, secondInviteRoomID))
res = bob.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(2), m.MatchV3Ops(
m.MatchV3DeleteOp(1),
m.MatchV3InsertOp(0, secondInviteRoomID),
)), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
secondInviteRoomID: {
m.MatchRoomInitial(true),
MatchRoomInviteState([]Event{
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "Second",
},
},
}, true),
},
}))
// now reject the invites
bob.LeaveRoom(t, firstInviteRoomID)
bob.LeaveRoom(t, secondInviteRoomID)
2023-10-11 12:23:46 +01:00
bob.MustSyncUntil(t, client.SyncReq{Since: since}, client.SyncLeftFrom(bob.UserID, secondInviteRoomID))
// TODO: proxy needs to have processed this event enough for it to be waiting for us
time.Sleep(100 * time.Millisecond)
// the list should be purged
res = bob.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(0), m.MatchV3Ops(
m.MatchV3DeleteOp(1),
m.MatchV3DeleteOp(0),
)))
// fresh sync -> no invites
res = bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Filters: &sync3.RequestFilters{
IsInvite: &boolTrue,
},
},
},
})
m.MatchResponse(t, res, m.MatchNoV3Ops(), m.MatchRoomSubscriptionsStrict(nil), m.MatchList("a", m.MatchV3Count(0)))
}
func TestInviteAcceptance(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
// ensure that invite state correctly propagates. One room will already be in 'invite' state
// prior to the first proxy sync, whereas the 2nd will transition.
t.Logf("Alice creates two rooms and invites Bob to the first.")
2023-10-11 12:23:46 +01:00
firstInviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "name": "First"})
alice.InviteRoom(t, firstInviteRoomID, bob.UserID)
2023-10-11 12:23:46 +01:00
secondInviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "name": "Second"})
t.Logf("first %s second %s", firstInviteRoomID, secondInviteRoomID)
t.Log("Sync as Bob, requesting invites only. He should see 1 invite")
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Filters: &sync3.RequestFilters{
IsInvite: &boolTrue,
},
},
},
})
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3SyncOp(0, 0, []string{firstInviteRoomID}),
)), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
firstInviteRoomID: {
m.MatchRoomInitial(true),
m.MatchInviteCount(1),
m.MatchJoinCount(1),
MatchRoomInviteState([]Event{
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "First",
},
},
}, true),
},
}))
t.Log("Alice invites bob to room 2.")
alice.InviteRoom(t, secondInviteRoomID, bob.UserID)
t.Log("Alice syncs until she sees Bob's invite.")
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
alice.SlidingSyncUntilMembership(t, "", secondInviteRoomID, bob, "invite")
t.Log("Bob syncs. He should see the invite to room 2 as well.")
res = bob.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(2), m.MatchV3Ops(
m.MatchV3DeleteOp(1),
m.MatchV3InsertOp(0, secondInviteRoomID),
)), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
secondInviteRoomID: {
m.MatchRoomInitial(true),
m.MatchInviteCount(1),
m.MatchJoinCount(1),
MatchRoomInviteState([]Event{
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "Second",
},
},
}, true),
},
}))
t.Log("Bob accept the invites.")
bob.JoinRoom(t, firstInviteRoomID, nil)
bob.JoinRoom(t, secondInviteRoomID, nil)
t.Log("Alice syncs until she sees Bob join room 1.")
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
alice.SlidingSyncUntilMembership(t, "", firstInviteRoomID, bob, "join")
t.Log("Alice syncs until she sees Bob join room 2.")
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
alice.SlidingSyncUntilMembership(t, "", secondInviteRoomID, bob, "join")
t.Log("Bob does an incremental sync")
res = bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
t.Log("Both of his invites should be purged.")
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(0), m.MatchV3Ops(
m.MatchV3DeleteOp(1),
m.MatchV3DeleteOp(0),
)))
t.Log("Bob makes a fresh sliding sync request.")
res = bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Filters: &sync3.RequestFilters{
IsInvite: &boolTrue,
},
},
},
})
t.Log("He should see no invites.")
m.MatchResponse(t, res, m.MatchNoV3Ops(), m.MatchRoomSubscriptionsStrict(nil), m.MatchList("a", m.MatchV3Count(0)))
}
2023-04-27 18:46:35 +01:00
// Regression test for https://github.com/matrix-org/sliding-sync/issues/66
// whereby the invite fails to appear to clients when you invite->reject->invite
func TestInviteRejectionTwice(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
roomName := "It's-a-me-invitio"
2023-10-11 12:23:46 +01:00
inviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "name": roomName})
2023-04-27 18:46:35 +01:00
t.Logf("TestInviteRejectionTwice room %s", inviteRoomID)
// sync as bob, we see no invites yet.
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Filters: &sync3.RequestFilters{
IsInvite: &boolTrue,
},
},
},
})
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(0)), m.MatchRoomSubscriptionsStrict(nil))
// now invite bob
alice.InviteRoom(t, inviteRoomID, bob.UserID)
// sync as bob until we see the room (we aren't interested in this invite)
res = bob.SlidingSyncUntilMembership(t, res.Pos, inviteRoomID, bob, "invite")
t.Logf("bob is invited")
// reject the invite and sync until we see it disappear (we aren't interested in this rejection either!)
bob.LeaveRoom(t, inviteRoomID)
res = bob.SlidingSyncUntilMembership(t, res.Pos, inviteRoomID, bob, "leave")
// now invite bob again, we should see this (the regression was that we didn't until we initial synced!)
alice.InviteRoom(t, inviteRoomID, bob.UserID)
bob.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(r *sync3.Response) error {
return m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
inviteRoomID: {
m.LogRoom(t),
m.MatchInviteCount(1),
MatchRoomInviteState([]Event{
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": roomName,
},
},
}, true),
},
})(r)
})
}
2023-09-08 12:01:59 +02:00
func TestLeavingRoomReturnsOneEvent(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
roomName := "It's-a-me-invitio"
2023-09-08 14:01:22 +02:00
for _, aliceSyncing := range []bool{false, true} {
t.Run(fmt.Sprintf("leaving a room returns one leave event (multiple poller=%v)", aliceSyncing), func(t *testing.T) {
2023-10-11 12:23:46 +01:00
inviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "name": roomName})
2023-09-08 14:01:22 +02:00
t.Logf("TestLeavingRoomReturnsOneEvent room %s", inviteRoomID)
2023-09-08 12:01:59 +02:00
2023-09-08 14:01:22 +02:00
if aliceSyncing {
alice.SlidingSync(t, sync3.Request{})
}
2023-09-08 12:01:59 +02:00
2023-09-08 14:01:22 +02:00
// sync as bob, we see no invites yet.
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Filters: &sync3.RequestFilters{
IsInvite: &boolTrue,
},
},
},
})
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(0)), m.MatchRoomSubscriptionsStrict(nil))
// now invite bob
alice.InviteRoom(t, inviteRoomID, bob.UserID)
// sync as bob until we see the room
res = bob.SlidingSyncUntilMembership(t, res.Pos, inviteRoomID, bob, "invite")
t.Logf("bob is invited")
// join the room
bob.JoinRoom(t, inviteRoomID, []string{})
res = bob.SlidingSyncUntilMembership(t, res.Pos, inviteRoomID, bob, "join")
t.Logf("bob joined")
// leave the room again, we should receive exactly one leave response
bob.LeaveRoom(t, inviteRoomID)
res = bob.SlidingSyncUntilMembership(t, res.Pos, inviteRoomID, bob, "leave")
if room, ok := res.Rooms[inviteRoomID]; ok {
// If alice is NOT syncing, we run into this failure mode
if c := len(room.Timeline); c > 1 {
for _, ev := range res.Rooms[inviteRoomID].Timeline {
t.Logf("[multiple poller=%v] Event: %s", aliceSyncing, ev)
}
t.Errorf("[multiple poller=%v] expected 1 timeline event, got %d", aliceSyncing, c)
}
} else {
t.Errorf("[multiple poller=%v] expected room %s in response, but didn't find it", aliceSyncing, inviteRoomID)
}
res = bob.SlidingSync(t, sync3.Request{}, WithPos(res.Pos))
// this should not happen, as we already send down the leave event
// If alice is syncing, we run into this failure mode
if room, ok := res.Rooms[inviteRoomID]; ok {
for _, ev := range room.Timeline {
t.Logf("[multiple poller=%v] Event: %s", aliceSyncing, ev)
}
t.Errorf("[multiple poller=%v] expected room not to be in response", aliceSyncing)
}
})
}
}
// Basically the same as above, without joining the room (separate test to make sure
// we don't already have a poller running for Alice)
func TestRejectingInviteReturnsOneEvent(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
roomName := "It's-a-me-invitio"
for _, aliceSyncing := range []bool{false, true} {
t.Run(fmt.Sprintf("rejecting an invite returns one leave event (multiple poller=%v)", aliceSyncing), func(t *testing.T) {
2023-10-11 12:23:46 +01:00
inviteRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "name": roomName})
t.Logf("TestRejectingInviteReturnsOneEvent room %s", inviteRoomID)
if aliceSyncing {
alice.SlidingSync(t, sync3.Request{})
}
// sync as bob, we see no invites yet.
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
Filters: &sync3.RequestFilters{
IsInvite: &boolTrue,
},
},
},
})
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(0)), m.MatchRoomSubscriptionsStrict(nil))
// now invite bob
alice.InviteRoom(t, inviteRoomID, bob.UserID)
// sync as bob until we see the room
res = bob.SlidingSyncUntilMembership(t, res.Pos, inviteRoomID, bob, "invite")
t.Logf("bob is invited")
// reject the invite, we should receive exactly one leave response
bob.LeaveRoom(t, inviteRoomID)
res = bob.SlidingSyncUntilMembership(t, res.Pos, inviteRoomID, bob, "leave")
t.Logf("bob rejected the invite")
2023-09-08 14:01:22 +02:00
if room, ok := res.Rooms[inviteRoomID]; ok {
// If alice is NOT syncing, we run into this failure mode
if c := len(room.Timeline); c > 1 {
for _, ev := range res.Rooms[inviteRoomID].Timeline {
t.Logf("[multiple poller=%v] Event: %s", aliceSyncing, ev)
}
t.Errorf("[multiple poller=%v] expected 1 timeline event, got %d", aliceSyncing, c)
}
} else {
t.Errorf("[multiple poller=%v] expected room %s in response, but didn't find it", aliceSyncing, inviteRoomID)
2023-09-08 12:01:59 +02:00
}
2023-09-08 14:01:22 +02:00
res = bob.SlidingSync(t, sync3.Request{}, WithPos(res.Pos))
2023-09-08 12:01:59 +02:00
2023-09-08 14:01:22 +02:00
// this should not happen, as we already send down the leave event
// If alice is syncing, we run into this failure mode
2023-09-08 14:01:22 +02:00
if room, ok := res.Rooms[inviteRoomID]; ok {
for _, ev := range room.Timeline {
t.Logf("[multiple poller=%v] Event: %s", aliceSyncing, ev)
}
t.Errorf("[multiple poller=%v] expected room not to be in response", aliceSyncing)
}
})
2023-09-08 12:01:59 +02:00
}
}
2023-09-15 15:08:07 +02:00
// Test to check that room heroes are returned if the membership changes
func TestHeroesOnMembershipChanges(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
charlie := registerNewUser(t)
t.Run("nameless room uses heroes to calculate roomname", func(t *testing.T) {
// create a room without a name, to ensure we calculate the room name based on
// room heroes
2023-10-11 12:23:46 +01:00
roomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
2023-09-15 15:08:07 +02:00
bob.JoinRoom(t, roomID, []string{})
res := alice.SlidingSyncUntilMembership(t, "", roomID, bob, "join")
// we expect to see Bob as a hero
if c := len(res.Rooms[roomID].Heroes); c > 1 {
t.Errorf("expected 1 room hero, got %d", c)
}
if gotUserID := res.Rooms[roomID].Heroes[0].ID; gotUserID != bob.UserID {
t.Errorf("expected userID %q, got %q", gotUserID, bob.UserID)
}
// Now join with Charlie, the heroes and the room name should change
charlie.JoinRoom(t, roomID, []string{})
res = alice.SlidingSyncUntilMembership(t, res.Pos, roomID, charlie, "join")
// we expect to see Bob as a hero
if c := len(res.Rooms[roomID].Heroes); c > 2 {
t.Errorf("expected 2 room hero, got %d", c)
}
if gotUserID := res.Rooms[roomID].Heroes[0].ID; gotUserID != bob.UserID {
t.Errorf("expected userID %q, got %q", gotUserID, bob.UserID)
}
if gotUserID := res.Rooms[roomID].Heroes[1].ID; gotUserID != charlie.UserID {
t.Errorf("expected userID %q, got %q", gotUserID, charlie.UserID)
}
// Send a message, the heroes shouldn't change
2023-10-11 12:23:46 +01:00
msgEv := bob.SendEventSynced(t, roomID, b.Event{
2023-09-15 15:08:07 +02:00
Type: "m.room.roomID",
Content: map[string]interface{}{"body": "Hello world", "msgtype": "m.text"},
})
res = alice.SlidingSyncUntilEventID(t, res.Pos, roomID, msgEv)
if len(res.Rooms[roomID].Heroes) > 0 {
t.Errorf("expected no change to room heros")
}
// Now leave with Charlie, only Bob should be in the heroes list
charlie.LeaveRoom(t, roomID)
res = alice.SlidingSyncUntilMembership(t, res.Pos, roomID, charlie, "leave")
if c := len(res.Rooms[roomID].Heroes); c > 1 {
t.Errorf("expected 1 room hero, got %d", c)
}
if gotUserID := res.Rooms[roomID].Heroes[0].ID; gotUserID != bob.UserID {
t.Errorf("expected userID %q, got %q", gotUserID, bob.UserID)
}
})
t.Run("named rooms don't have heroes", func(t *testing.T) {
2023-10-11 12:23:46 +01:00
namedRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "my room without heroes"})
2023-09-15 15:08:07 +02:00
// this makes sure that even if bob is joined, we don't return any heroes
bob.JoinRoom(t, namedRoomID, []string{})
res := alice.SlidingSyncUntilMembership(t, "", namedRoomID, bob, "join")
if len(res.Rooms[namedRoomID].Heroes) > 0 {
t.Errorf("expected no heroes, got %#v", res.Rooms[namedRoomID].Heroes)
}
})
t.Run("rooms with aliases don't have heroes", func(t *testing.T) {
2023-10-11 12:23:46 +01:00
aliasRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
2023-09-15 15:08:07 +02:00
alias := fmt.Sprintf("#%s-%d:%s", t.Name(), time.Now().Unix(), alice.Domain)
2023-10-11 12:23:46 +01:00
alice.MustDo(t, "PUT", []string{"_matrix", "client", "v3", "directory", "room", alias},
client.WithJSONBody(t, map[string]any{"room_id": aliasRoomID}),
2023-09-15 15:08:07 +02:00
)
2023-10-11 12:23:46 +01:00
alice.Unsafe_SendEventUnsynced(t, aliasRoomID, b.Event{
Type: "m.room.canonical_alias",
StateKey: ptr(""),
Content: map[string]any{
"alias": alias,
},
2023-09-15 15:08:07 +02:00
})
bob.JoinRoom(t, aliasRoomID, []string{})
res := alice.SlidingSyncUntilMembership(t, "", aliasRoomID, bob, "join")
if len(res.Rooms[aliasRoomID].Heroes) > 0 {
t.Errorf("expected no heroes, got %#v", res.Rooms[aliasRoomID].Heroes)
}
})
2023-09-15 16:57:12 +02:00
t.Run("can set heroes=true on room subscriptions", func(t *testing.T) {
2023-10-11 12:23:46 +01:00
subRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
2023-09-15 16:57:12 +02:00
bob.JoinRoom(t, subRoomID, []string{})
res := alice.SlidingSyncUntilMembership(t, "", subRoomID, bob, "join")
2023-09-15 16:57:12 +02:00
if c := len(res.Rooms[subRoomID].Heroes); c > 1 {
t.Errorf("expected 1 room hero, got %d", c)
}
if gotUserID := res.Rooms[subRoomID].Heroes[0].ID; gotUserID != bob.UserID {
t.Errorf("expected userID %q, got %q", gotUserID, bob.UserID)
}
})
2023-09-19 08:28:06 +02:00
t.Run("can set heroes=true in lists", func(t *testing.T) {
2023-10-11 12:23:46 +01:00
listRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
2023-09-19 08:28:06 +02:00
bob.JoinRoom(t, listRoomID, []string{})
res := alice.SlidingSyncUntil(t, "", sync3.Request{
Lists: map[string]sync3.RequestList{
"all_rooms": {
2023-09-19 09:37:58 +02:00
Ranges: sync3.SliceRanges{{0, 20}},
2023-09-19 08:28:06 +02:00
RoomSubscription: sync3.RoomSubscription{
Heroes: &boolTrue,
TimelineLimit: 10,
},
},
},
}, func(response *sync3.Response) error {
r, ok := response.Rooms[listRoomID]
if !ok {
return fmt.Errorf("room %q not in response", listRoomID)
}
// wait for bob to be joined
for _, ev := range r.Timeline {
if gjson.GetBytes(ev, "type").Str != "m.room.member" {
continue
}
if gjson.GetBytes(ev, "state_key").Str != bob.UserID {
continue
}
if gjson.GetBytes(ev, "content.membership").Str == "join" {
return nil
}
}
return fmt.Errorf("%s is not joined to room %q", bob.UserID, listRoomID)
})
if c := len(res.Rooms[listRoomID].Heroes); c > 1 {
t.Errorf("expected 1 room hero, got %d", c)
}
if gotUserID := res.Rooms[listRoomID].Heroes[0].ID; gotUserID != bob.UserID {
t.Errorf("expected userID %q, got %q", gotUserID, bob.UserID)
}
})
2023-09-15 15:08:07 +02:00
}
// test invite/join counts update and are accurate
func TestMemberCounts(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
charlie := registerNewUser(t)
2023-10-11 12:23:46 +01:00
firstRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "First"})
alice.InviteRoom(t, firstRoomID, bob.UserID)
2023-10-11 12:23:46 +01:00
secondRoomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "Second"})
alice.InviteRoom(t, secondRoomID, bob.UserID)
charlie.JoinRoom(t, secondRoomID, nil)
t.Logf("first %s second %s", firstRoomID, secondRoomID)
// sync as bob, we should see 2 invited rooms with the same join counts so as not to leak join counts
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
})
m.MatchResponse(t, res, m.MatchList("a", m.MatchV3Count(2), m.MatchV3Ops(
2023-04-04 23:10:39 +01:00
m.MatchV3SyncOp(0, 1, []string{firstRoomID, secondRoomID}, true),
)), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
firstRoomID: {
m.MatchRoomInitial(true),
m.MatchInviteCount(1),
m.MatchJoinCount(1),
MatchRoomInviteState([]Event{
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "First",
},
},
}, true),
},
secondRoomID: {
m.MatchRoomInitial(true),
m.MatchInviteCount(1),
m.MatchJoinCount(1),
MatchRoomInviteState([]Event{
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "Second",
},
},
}, true),
},
}))
// join both rooms, the counts should now reflect reality
bob.JoinRoom(t, firstRoomID, nil)
bob.JoinRoom(t, secondRoomID, nil)
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
alice.SlidingSyncUntilMembership(t, "", firstRoomID, bob, "join")
alice.SlidingSyncUntilMembership(t, "", secondRoomID, bob, "join")
res = bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
firstRoomID: {
m.MatchRoomInitial(true),
m.MatchInviteCount(0),
m.MatchJoinCount(2),
},
secondRoomID: {
m.MatchRoomInitial(true),
m.MatchInviteCount(0),
m.MatchJoinCount(3),
},
}))
// sending a message shouldn't update the count as it wastes bandwidth
2023-10-11 12:23:46 +01:00
charlie.SendEventSynced(t, secondRoomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{"body": "ping", "msgtype": "m.text"},
})
res = bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
secondRoomID: {
m.MatchRoomInitial(false),
m.MatchNoInviteCount(),
m.MatchJoinCount(0), // omitempty
},
}))
// leaving a room should update the count
charlie.LeaveRoom(t, secondRoomID)
2023-10-11 12:23:46 +01:00
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(charlie.UserID, secondRoomID))
res = bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
}, WithPos(res.Pos))
m.MatchResponse(t, res, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
secondRoomID: {
m.MatchRoomInitial(false),
m.MatchNoInviteCount(),
m.MatchJoinCount(2),
},
}))
}
2023-10-17 17:43:21 +01:00
func TestPreemptiveBanIsNotLeaked(t *testing.T) {
alice := registerNamedUser(t, "alice")
nigel := registerNamedUser(t, "nigel")
t.Log("Alice creates a public room and a DM with Nigel.")
public := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"})
dm := alice.MustCreateRoom(t, map[string]interface{}{"preset": "private_chat", "invite": []string{nigel.UserID}})
t.Log("Nigel joins the DM")
nigel.JoinRoom(t, dm, nil)
t.Log("Alice sends a sentinel message into the DM.")
dmSentinel := alice.SendEventSynced(t, dm, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{"body": "sentinel, sentinel, where have you been?", "msgtype": "m.text"},
})
t.Log("Nigel does an initial sliding sync.")
nigelRes := nigel.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 20,
},
Ranges: sync3.SliceRanges{{0, 10}},
},
},
})
t.Log("Nigel sees the sentinel.")
m.MatchResponse(t, nigelRes, m.MatchRoomSubscription(dm, MatchRoomTimelineMostRecent(1, []Event{{ID: dmSentinel}})))
t.Log("Alice pre-emptively bans Nigel from the public room.")
alice.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", public, "ban"},
client.WithJSONBody(t, map[string]any{"user_id": nigel.UserID}))
t.Log("Alice sliding syncs until she sees the ban.")
alice.SlidingSyncUntilMembership(t, "", public, nigel, "ban")
t.Log("Alice sends a second sentinel in Nigel's DM.")
dmSentinel2 := alice.SendEventSynced(t, dm, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{"body": "sentinel 2 placeholder boogaloo", "msgtype": "m.text"},
})
t.Log("Nigel syncs until he sees the second sentinel. He should NOT see his ban event.")
nigelRes = nigel.SlidingSyncUntil(t, nigelRes.Pos, sync3.Request{}, func(response *sync3.Response) error {
seenPublicRoom := m.MatchRoomSubscription(public)
if seenPublicRoom(response) == nil {
t.Errorf("Nigel had a room subscription for the public room, but shouldn't have.")
m.LogResponse(t)(response)
t.FailNow()
}
seenSentinel := m.MatchRoomSubscription(dm, MatchRoomTimelineMostRecent(1, []Event{{ID: dmSentinel2}}))
return seenSentinel(response)
})
}