sliding-sync/tests-e2e/security_test.go

412 lines
13 KiB
Go
Raw Permalink Normal View History

2022-07-26 11:39:19 +01:00
package syncv3_test
import (
"encoding/json"
"fmt"
2022-07-26 11:39:19 +01:00
"testing"
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"
2022-07-26 11:39:19 +01:00
)
// The purpose of these tests is to ensure that events for one user cannot leak to another user.
// Test that events do not leak to users who have left a room.
// Rationale: When a user leaves a room they should not receive events in that room anymore. However,
// the v3 server may still be receiving events in that room from other joined members. We need to
// make sure these events don't find their way to the client.
// Attack vector:
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 is using the sync server and is in room !A.
// - Eve joins the room !A.
// - Alice kicks Eve.
// - Alice sends event $X in !A.
// - Ensure Eve does not see event $X.
2022-07-26 11:39:19 +01:00
func TestSecurityLiveStreamEventLeftLeak(t *testing.T) {
alice := registerNewUser(t)
eve := registerNewUser(t)
// Alice and Eve in the room
2023-10-11 12:23:46 +01:00
roomID := alice.MustCreateRoom(t, map[string]interface{}{
2022-07-26 11:39:19 +01:00
"preset": "public_chat",
})
eve.JoinRoom(t, roomID, nil)
// start sync streams for Alice and Eve
aliceRes := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
[2]int64{0, 10}, // doesn't matter
},
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{
{"m.room.name", ""},
},
TimelineLimit: 2,
},
}},
2022-07-26 11:39:19 +01:00
})
m.MatchResponse(t, aliceRes, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(
2023-04-05 01:07:07 +01:00
m.MatchV3SyncOp(0, 0, []string{roomID}),
2022-07-26 11:39:19 +01:00
)))
eveRes := eve.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
[2]int64{0, 10}, // doesn't matter
},
}},
2022-07-26 11:39:19 +01:00
})
m.MatchResponse(t, eveRes, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(
2023-04-05 01:07:07 +01:00
m.MatchV3SyncOp(0, 0, []string{roomID}),
2022-07-26 11:39:19 +01:00
)))
// kick Eve
2023-10-11 12:23:46 +01:00
alice.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "kick"}, client.WithJSONBody(t, map[string]interface{}{
2022-07-26 11:39:19 +01:00
"user_id": eve.UserID,
}))
// send message as Alice, note it shouldn't go down Eve's v2 stream
2023-10-11 12:23:46 +01:00
sensitiveEventID := alice.SendEventSynced(t, roomID, b.Event{
2022-07-26 11:39:19 +01:00
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "I hate Eve",
},
})
// Ensure Alice sees both events, wait till she gets them
var timeline []json.RawMessage
aliceRes = alice.SlidingSyncUntil(t, aliceRes.Pos, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
[2]int64{0, 10}, // doesn't matter
2022-07-26 11:39:19 +01:00
},
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{
{"m.room.name", ""},
},
TimelineLimit: 2,
},
}},
}, func(r *sync3.Response) error {
// keep syncing until we see 2 events in the timeline
timeline = append(timeline, r.Rooms[roomID].Timeline...)
if len(timeline) != 2 {
return fmt.Errorf("waiting for more messages, got %v", len(timeline))
}
return nil
})
2022-07-26 11:39:19 +01:00
// check Alice sees both events
kickEvent := Event{
Type: "m.room.member",
StateKey: ptr(eve.UserID),
Content: map[string]interface{}{
"membership": "leave",
2022-07-26 11:39:19 +01:00
},
Sender: alice.UserID,
}
assertEventsEqual(t, []Event{
kickEvent,
2022-07-26 11:39:19 +01:00
{
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "I hate Eve",
},
Sender: alice.UserID,
ID: sensitiveEventID,
},
}, timeline)
2022-07-26 11:39:19 +01:00
// Ensure Eve doesn't see this message in the timeline, name calc or required_state
eveRes = eve.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
[2]int64{0, 10}, // doesn't matter
2022-07-26 11:39:19 +01:00
},
// Note we are _adding_ this to the list which will kick in logic to return required state / timeline limits
// so we need to make sure that this returns no data.
RoomSubscription: sync3.RoomSubscription{
RequiredState: [][2]string{
{"m.room.name", ""},
},
TimelineLimit: 2,
},
}},
}, WithPos(eveRes.Pos))
2022-07-26 11:39:19 +01:00
// 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("a", m.MatchV3Count(0), m.MatchV3Ops(m.MatchV3DeleteOp(0))), m.MatchRoomSubscription(
roomID, m.MatchRoomName(""), m.MatchRoomRequiredState(nil), MatchRoomTimelineMostRecent(1, []Event{kickEvent}),
2022-07-26 11:39:19 +01:00
))
}
// Test that events do not leak via direct room subscriptions.
// Rationale: Unlike sync v2, in v3 clients can subscribe to any room ID they want as a room_subscription.
// We need to make sure that the user is allowed to see events in that room before delivering those events.
// Attack vector:
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 is using the sync server and is in room !A.
// - Eve works out the room ID !A (this isn't sensitive information).
// - Eve starts using the sync server and makes a room_subscription for !A.
// - Ensure that Eve does not see any events in !A.
2022-07-26 11:39:19 +01:00
func TestSecurityRoomSubscriptionLeak(t *testing.T) {
alice := registerNewUser(t)
eve := registerNewUser(t)
// Alice in the room
2023-10-11 12:23:46 +01:00
alicePrivateRoomID := alice.MustCreateRoom(t, map[string]interface{}{
2022-07-26 11:39:19 +01:00
"preset": "private_chat",
})
// Eve is in an unrelated room
2023-10-11 12:23:46 +01:00
eveUnrelatedRoomID := eve.MustCreateRoom(t, map[string]interface{}{
2022-07-26 11:39:19 +01:00
"preset": "private_chat",
})
// seed the proxy with alice's data
alice.SlidingSync(t, sync3.Request{})
2022-07-26 11:39:19 +01:00
// start sync streams for Eve, with a room subscription to alice's private room
eveRes := eve.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
[2]int64{0, 10}, // doesn't matter
},
}},
2022-07-26 11:39:19 +01:00
RoomSubscriptions: map[string]sync3.RoomSubscription{
alicePrivateRoomID: {
TimelineLimit: 5,
RequiredState: [][2]string{
{"m.room.join_rules", ""},
},
},
},
})
// Assert that Eve doesn't see anything
m.MatchResponse(t, eveRes, m.MatchList("a", m.MatchV3Count(1), m.MatchV3Ops(
2023-04-05 01:07:07 +01:00
m.MatchV3SyncOp(0, 0, []string{eveUnrelatedRoomID}),
2022-07-26 11:39:19 +01:00
)), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
eveUnrelatedRoomID: {},
}))
// Assert that live updates still don't feed through to Eve
2023-10-11 12:23:46 +01:00
alice.SendEventSynced(t, alicePrivateRoomID, b.Event{
2022-07-26 11:39:19 +01:00
Type: "m.room.name",
StateKey: ptr(""),
Content: map[string]interface{}{
"name": "I hate Eve",
},
})
eveRes = eve.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{
[2]int64{0, 10}, // doesn't matter
},
}},
}, WithPos(eveRes.Pos))
2022-07-26 11:39:19 +01:00
// Assert that Eve doesn't see anything
m.MatchResponse(t, eveRes, m.MatchList("a", m.MatchV3Count(1)), m.MatchNoV3Ops(), m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{}))
2022-07-26 11:39:19 +01:00
}
// Test that events do not leak via direct space subscriptions.
// Rationale: Unlike sync v2, in v3 clients can subscribe to any room ID they want as a space.
// We need to make sure that the user is allowed to see events in that room before delivering those events.
// Attack vector:
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 is using the sync server and is in space !A with room !B.
// - Eve works out the room ID !A (this isn't sensitive information).
// - Eve starts using the sync server and makes a request for !A as the space filter.
// - Ensure that Eve does not see any events in !A or !B.
func TestSecuritySpaceDataLeak(t *testing.T) {
alice := registerNewUser(t)
eve := registerNewUser(t)
2023-10-11 12:23:46 +01:00
roomA := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"creation_content": map[string]string{
"type": "m.space",
},
})
2023-10-11 12:23:46 +01:00
roomB := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "private_chat",
})
2023-10-11 12:23:46 +01:00
alice.SendEventSynced(t, roomA, b.Event{
Type: "m.space.child",
StateKey: &roomB,
Content: map[string]interface{}{
"via": []string{"example.com"},
},
})
// seed the proxy with alice's data
alice.SlidingSync(t, sync3.Request{})
// ensure eve sees nothing
res := eve.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: [][2]int64{{0, 20}},
Filters: &sync3.RequestFilters{
Spaces: []string{roomA},
},
},
},
})
m.MatchResponse(t, res, m.MatchNoV3Ops(), m.MatchRoomSubscriptionsStrict(nil))
}
// Test that knowledge of a room being in a hidden space does not leak via direct space subscriptions.
// Rationale: Unlike sync v2, in v3 clients can subscribe to any room ID they want as a space.
// We need to make sure that if a user is in a room in multiple spaces (only 1 of them the user is joined to)
// then they cannot see the room if they apply a filter for a parent space they are not joined to.
// Attack vector:
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 is using the sync server and is in space !A with room !B.
// - Eve is using the sync server and is in space !C with room !B as well.
// - Eve works out the room ID !A (this isn't sensitive information).
// - Eve starts using the sync server and makes a request for !A as the space filter.
// - Ensure that Eve does not see anything, even though they are joined to !B and the proxy knows it.
func TestSecuritySpaceMetadataLeak(t *testing.T) {
alice := registerNewUser(t)
eve := registerNewUser(t)
2023-10-11 12:23:46 +01:00
roomA := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"creation_content": map[string]string{
"type": "m.space",
},
})
2023-10-11 12:23:46 +01:00
roomB := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
// Alice has a space A -> B
2023-10-11 12:23:46 +01:00
alice.SendEventSynced(t, roomA, b.Event{
Type: "m.space.child",
StateKey: &roomB,
Content: map[string]interface{}{
"via": []string{"example.com"},
},
})
// seed the proxy with alice's data
alice.SlidingSync(t, sync3.Request{})
// now Eve also has a space... C -> B
2023-10-11 12:23:46 +01:00
roomC := eve.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"creation_content": map[string]string{
"type": "m.space",
},
})
eve.JoinRoom(t, roomB, nil)
2023-10-11 12:23:46 +01:00
eve.SendEventSynced(t, roomC, b.Event{
Type: "m.space.child",
StateKey: &roomB,
Content: map[string]interface{}{
"via": []string{"example.com"},
},
})
// ensure eve sees nothing
res := eve.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: [][2]int64{{0, 20}},
Filters: &sync3.RequestFilters{
Spaces: []string{roomA},
},
},
},
})
m.MatchResponse(t, res, m.MatchNoV3Ops(), m.MatchRoomSubscriptionsStrict(nil))
}
// Test that adding a child room to a space does not leak global room metadata about that
// child room to users in the parent space. This information isn't strictly confidential as
// the /rooms/{roomId}/hierarchy endpoint will include such metadata (room name, avatar, join count, etc)
// because the user is part of the parent space. There isn't an attack vector here, but repro steps:
// - Alice and Bob are in a parent space.
// - Bob has a poller on SS running.
// - Alice is live streaming from SS.
// - Bob creates a child room in that space, and sends both the m.space.parent in the child room AND
// the m.space.child in the parent space.
// - Ensure that no information about the child room comes down Alice's connection.
func TestSecuritySpaceChildMetadataLeakFromParent(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
parentName := "The Parent Room Name"
childName := "The Child Room Name"
// Alice and Bob are in a parent space.
parentSpace := bob.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": parentName,
"creation_content": map[string]string{
"type": "m.space",
},
})
alice.MustJoinRoom(t, parentSpace, []string{"hs1"})
// Bob has a poller on SS running.
bobRes := bob.SlidingSync(t, sync3.Request{})
// Alice is live streaming from SS.
aliceRes := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: [][2]int64{{0, 20}},
},
},
})
m.MatchResponse(t, aliceRes, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
parentSpace: {
m.MatchJoinCount(2),
m.MatchRoomName(parentName),
},
}))
// Bob creates a child room in that space, and sends both the m.space.parent in the child room AND
// the m.space.child in the parent space.
childRoom := bob.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": childName,
"initial_state": []map[string]interface{}{
{
"type": "m.space.parent",
"state_key": parentSpace,
"content": map[string]interface{}{
"canonical": true,
"via": []string{"hs1"},
},
},
},
})
chlidEventID := bob.SendEventSynced(t, parentSpace, b.Event{
Type: "m.space.child",
StateKey: ptr(childRoom),
Content: map[string]interface{}{
"via": []string{"hs1"},
},
})
// wait for SS to process it
bob.SlidingSyncUntilEventID(t, bobRes.Pos, parentSpace, chlidEventID)
// Ensure that no information about the child room comes down Alice's connection.
aliceRes = alice.SlidingSync(t, sync3.Request{}, WithPos(aliceRes.Pos))
m.MatchResponse(t, aliceRes, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
parentSpace: {
MatchRoomTimeline([]Event{{
Type: "m.space.child",
StateKey: ptr(childRoom),
Content: map[string]interface{}{
"via": []interface{}{"hs1"},
},
}}),
},
}))
}