sliding-sync/tests-e2e/receipts_test.go
2023-10-11 12:23:46 +01:00

434 lines
12 KiB
Go

package syncv3_test
import (
"fmt"
"testing"
"time"
"github.com/matrix-org/complement/b"
"github.com/matrix-org/sliding-sync/sync3"
"github.com/matrix-org/sliding-sync/sync3/extensions"
"github.com/matrix-org/sliding-sync/testutils/m"
)
// Test that receipts are sent initially and during live streams
func TestReceipts(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
bob.JoinRoom(t, roomID, nil)
eventID := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Hello world",
},
})
alice.SendReceipt(t, roomID, eventID, "m.read")
// bob syncs -> should see receipt
res := bob.SlidingSync(t, sync3.Request{
RoomSubscriptions: map[string]sync3.RoomSubscription{
roomID: {
TimelineLimit: 1,
},
},
Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue},
},
},
})
m.MatchResponse(t, res, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
roomID: {
MatchRoomTimeline([]Event{
{
ID: eventID,
},
}),
},
}), m.MatchReceipts(roomID, []m.Receipt{
{
EventID: eventID,
UserID: alice.UserID,
Type: "m.read",
},
}))
// bob sends receipt -> should see it on live stream
bob.SendReceipt(t, roomID, eventID, "m.read")
bob.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(r *sync3.Response) error {
return m.MatchReceipts(roomID, []m.Receipt{
{
EventID: eventID,
UserID: bob.UserID,
Type: "m.read",
},
})(r)
})
}
func TestReceiptsLazy(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
charlie := registerNewUser(t)
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
bob.JoinRoom(t, roomID, nil)
charlie.JoinRoom(t, roomID, nil)
alice.SlidingSync(t, sync3.Request{}) // proxy begins tracking
eventID := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Hello world",
},
})
// everyone sends a receipt for this event
alice.SendReceipt(t, roomID, eventID, "m.read")
bob.SendReceipt(t, roomID, eventID, "m.read")
charlie.SendReceipt(t, roomID, eventID, "m.read")
// alice sends 5 new events, bob and alice ACK the last event
var fifthEventID string
for i := 0; i < 5; i++ {
fifthEventID = alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Hello world",
},
})
}
alice.SendReceipt(t, roomID, fifthEventID, "m.read")
bob.SendReceipt(t, roomID, fifthEventID, "m.read")
// alice sends another 5 events and ACKs nothing
var lastEventID string
for i := 0; i < 5; i++ {
lastEventID = alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Hello world",
},
})
}
// ensure the proxy has the last event processed
alice.SlidingSyncUntilEventID(t, "", roomID, lastEventID)
// Test that:
// - Bob syncs with timeline_limit: 1 => only own receipt, as you always get your own receipts.
// - Bob sync with timeline limit: 6 => receipts for fifthEventID only (self + alice)
res := bob.SlidingSync(t, sync3.Request{
RoomSubscriptions: map[string]sync3.RoomSubscription{
roomID: {
TimelineLimit: 1,
},
},
Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue},
},
},
})
m.MatchResponse(t, res, m.MatchReceipts(roomID, []m.Receipt{
{
EventID: fifthEventID,
UserID: bob.UserID,
Type: "m.read",
},
}))
res = bob.SlidingSync(t, sync3.Request{
RoomSubscriptions: map[string]sync3.RoomSubscription{
roomID: {
TimelineLimit: 6,
},
},
Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue},
},
},
})
m.MatchResponse(t, res, m.MatchReceipts(roomID, []m.Receipt{
{
EventID: fifthEventID,
UserID: alice.UserID,
Type: "m.read",
},
{
EventID: fifthEventID,
UserID: bob.UserID,
Type: "m.read",
},
}))
}
func TestReceiptsPrivate(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})
bob.JoinRoom(t, roomID, nil)
eventID := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Hello world",
},
})
// bob secretly reads this
bob.SendReceipt(t, roomID, eventID, "m.read.private")
time.Sleep(300 * time.Millisecond) // TODO: find a better way to wait until the proxy has processed this.
// bob does sliding sync -> sees private RR
res := bob.SlidingSync(t, sync3.Request{
RoomSubscriptions: map[string]sync3.RoomSubscription{
roomID: {
TimelineLimit: 1,
},
},
Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue},
},
},
})
m.MatchResponse(t, res, m.MatchReceipts(roomID, []m.Receipt{
{
UserID: bob.UserID,
EventID: eventID,
Type: "m.read.private",
},
}))
// alice does sliding sync -> does not see private RR
// We do this _after_ bob's sync request so we know we got the private RR and it is actively
// suppressed, rather than the private RR not making it to the proxy yet.
res = alice.SlidingSync(t, sync3.Request{
RoomSubscriptions: map[string]sync3.RoomSubscription{
roomID: {
TimelineLimit: 1,
},
},
Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue},
},
},
})
m.MatchResponse(t, res, m.MatchNoReceiptsExtension())
}
func TestReceiptsRespectsExtensionScope(t *testing.T) {
alice := registerNewUser(t)
bob := registerNewUser(t)
var syncResp *sync3.Response
t.Log("Alice creates four rooms.")
room1 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room 1"})
room2 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room 2"})
room3 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room 3"})
room4 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room 4"})
t.Logf("room1=%s room2=%s room3=%s room4=%s", room1, room2, room3, room4)
t.Log("Bob joins those rooms.")
bob.JoinRoom(t, room1, nil)
bob.JoinRoom(t, room2, nil)
bob.JoinRoom(t, room3, nil)
bob.JoinRoom(t, room4, nil)
t.Log("Alice posts a message to each room")
messageEvent := b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Hello, room!",
},
}
message1 := alice.SendEventSynced(t, room1, messageEvent)
message2 := alice.SendEventSynced(t, room2, messageEvent)
message3 := alice.SendEventSynced(t, room3, messageEvent)
message4 := alice.SendEventSynced(t, room4, messageEvent)
t.Log("Bob posts a public read receipt for the messages in rooms 1 and 2.")
bob.SendReceipt(t, room1, message1, "m.read")
bob.SendReceipt(t, room2, message2, "m.read")
t.Log("Bob makes an initial sliding sync, requesting receipts in rooms 2 and 4 only.")
syncResp = bob.SlidingSync(t, sync3.Request{
Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue, Lists: []string{}, Rooms: []string{room2, room4}},
},
},
Lists: map[string]sync3.RequestList{
"window": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
})
t.Log("Bob should see his receipt in room 2, but not his receipt in room 1.")
m.MatchResponse(
t,
syncResp,
m.MatchReceipts(room1, nil),
m.MatchReceipts(room2, []m.Receipt{{
EventID: message2,
UserID: bob.UserID,
Type: "m.read",
}}),
)
t.Log("Bob posts receipts for rooms 3 and 4.")
bob.SendReceipt(t, room3, message3, "m.read")
bob.SendReceipt(t, room4, message4, "m.read")
t.Log("Bob sees his receipt in room 4, but not in room 3.")
syncResp = bob.SlidingSyncUntil(
t,
syncResp.Pos,
sync3.Request{}, // no change
func(response *sync3.Response) error {
// Need to check some receipts data exist, or room3 matcher will fail
if response.Extensions.Receipts == nil {
return fmt.Errorf("no receipts data in sync response")
}
// Bob never sees a receipt in room 3.
matcherRoom3 := m.MatchReceipts(room3, nil)
err := matcherRoom3(response)
if err != nil {
t.Error(err)
}
matcherRoom4 := m.MatchReceipts(room4, []m.Receipt{{
EventID: message4,
UserID: bob.UserID,
Type: "m.read",
}})
return matcherRoom4(response)
},
)
}
func TestReceiptsOnRoomsOnly(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
t.Log("Alice creates two rooms.")
room1 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room 1"})
room2 := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat", "name": "room 2"})
t.Logf("room1=%s room2=%s", room1, room2)
t.Log("Bob joins those rooms.")
bob.JoinRoom(t, room1, nil)
bob.JoinRoom(t, room2, nil)
t.Log("Alice posts a message to each room")
messageEvent := b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Hello, room!",
},
}
message1 := alice.SendEventSynced(t, room1, messageEvent)
message2 := alice.SendEventSynced(t, room2, messageEvent)
t.Log("Bob posts a public read receipt for the messages in both rooms.")
bob.SendReceipt(t, room1, message1, "m.read")
bob.SendReceipt(t, room2, message2, "m.read")
t.Log("Bob makes an initial sliding sync, with a window to capture room 1 and a subscription to room 2.")
t.Log("He requests receipts for his explicit room subscriptions only.")
res := bob.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"room1": {
RoomSubscription: sync3.RoomSubscription{
TimelineLimit: 20,
},
Ranges: sync3.SliceRanges{{0, 0}},
Sort: []string{"by_name"},
},
},
RoomSubscriptions: map[string]sync3.RoomSubscription{
room2: {
TimelineLimit: 20,
},
},
Extensions: extensions.Request{
Receipts: &extensions.ReceiptsRequest{
Core: extensions.Core{Enabled: &boolTrue, Lists: []string{}, Rooms: nil},
},
},
})
t.Log("Bob should see the messages in both rooms, but only a receipt in room 2.")
m.MatchResponse(
t,
res,
m.MatchList("room1", m.MatchV3Count(2), m.MatchV3Ops(m.MatchV3SyncOp(0, 0, []string{room1}))),
m.MatchRoomSubscription(room1, MatchRoomTimelineMostRecent(1, []Event{{ID: message1}})),
m.MatchRoomSubscription(room2, MatchRoomTimelineMostRecent(1, []Event{{ID: message2}})),
m.MatchReceipts(room1, nil),
m.MatchReceipts(room2, []m.Receipt{{
EventID: message2,
UserID: bob.UserID,
Type: "m.read",
}}),
)
// Now do the same, but live-streaming.
t.Log("Alice posts another message to each room")
message3 := alice.SendEventSynced(t, room1, messageEvent)
message4 := alice.SendEventSynced(t, room2, messageEvent)
t.Log("Bob posts a public read receipt for the messages both rooms.")
bob.SendReceipt(t, room1, message3, "m.read")
bob.SendReceipt(t, room2, message4, "m.read")
seenMsg3 := false
seenMsg4 := false
seenReceipt4 := false
res = bob.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(response *sync3.Response) error {
matchMsg3 := m.MatchRoomSubscription(room1, MatchRoomTimelineMostRecent(1, []Event{{ID: message3}}))
matchMsg4 := m.MatchRoomSubscription(room2, MatchRoomTimelineMostRecent(1, []Event{{ID: message4}}))
if matchMsg3(response) == nil {
seenMsg3 = true
}
if matchMsg4(response) == nil {
seenMsg4 = true
}
matchNoReceiptsRoom1 := m.MatchReceipts(room1, nil)
matchReceiptRoom2 := m.MatchReceipts(room2, []m.Receipt{{
EventID: message4,
UserID: bob.UserID,
Type: "m.read",
}})
if err := matchNoReceiptsRoom1(response); err != nil {
return err
}
if matchReceiptRoom2(response) == nil {
seenReceipt4 = true
}
if !(seenMsg3 && seenMsg4 && seenReceipt4) {
return fmt.Errorf("still waiting: seenMsg3 = %t, seenMsg4 = %t, seenReceipt4 = %t", seenMsg3, seenMsg4, seenReceipt4)
}
return nil
})
}