diff --git a/internal/api/client.go b/internal/api/client.go index 66db642..95af909 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -34,6 +34,8 @@ type Client interface { // Wait until an event with the given body is seen. Not all impls expose event IDs // hence needing to use body as a proxy. WaitUntilEventInRoom(t *testing.T, roomID, wantBody string) Waiter + // Backpaginate in this room by `count` events. + MustBackpaginate(t *testing.T, roomID string, count int) Type() ClientType } diff --git a/internal/api/js.go b/internal/api/js.go index 7619797..6be0c16 100644 --- a/internal/api/js.go +++ b/internal/api/js.go @@ -56,7 +56,7 @@ func NewJSClient(t *testing.T, opts ClientCreationOpts) (Client, error) { continue } // TODO: debug mode only? - fmt.Printf("[%s] console.log %s\n", opts.UserID, s) + colorify("[%s] console.log %s\n", opts.UserID, s) if strings.HasPrefix(s, CONSOLE_LOG_CONTROL_STRING) { val := strings.TrimPrefix(s, CONSOLE_LOG_CONTROL_STRING) @@ -186,12 +186,22 @@ func (c *JSClient) SendMessage(t *testing.T, roomID, text string) { must.NotError(t, "failed to sendMessage", err) } +func (c *JSClient) MustBackpaginate(t *testing.T, roomID string, count int) { + chrome.MustAwaitExecute(t, c.ctx, fmt.Sprintf( + `window.__client.scrollback(window.__client.getRoom("%s"), %d);`, roomID, count, + )) +} + func (c *JSClient) WaitUntilEventInRoom(t *testing.T, roomID, wantBody string) Waiter { - // TODO: check if this event already exists + exists := chrome.MustExecuteInto[bool](t, c.ctx, fmt.Sprintf( + `window.__client.getRoom("%s").getLiveTimeline().getEvents().map((e)=>{return e.getContent().body}).includes("%s");`, roomID, wantBody, + )) + return &jsTimelineWaiter{ roomID: roomID, wantBody: wantBody, client: c, + exists: exists, } } @@ -211,9 +221,13 @@ type jsTimelineWaiter struct { roomID string wantBody string client *JSClient + exists bool } func (w *jsTimelineWaiter) Wait(t *testing.T, s time.Duration) { + if w.exists { + return + } updates := make(chan bool, 3) cancel := w.client.listenForUpdates(func(roomID, gotText string) { if w.roomID != roomID { @@ -241,8 +255,10 @@ func (w *jsTimelineWaiter) Wait(t *testing.T, s time.Duration) { } } -func (w *jsTimelineWaiter) callback(gotRoomID, gotText string) { - if w.roomID == gotRoomID && w.wantBody == gotText { +const ansiYellowForeground = "\x1b[33m" +const ansiResetForeground = "\x1b[39m" - } +func colorify(format string, args ...any) { + format = ansiYellowForeground + format + ansiResetForeground + fmt.Printf(format, args...) } diff --git a/internal/api/rust.go b/internal/api/rust.go index be95258..a0253f4 100644 --- a/internal/api/rust.go +++ b/internal/api/rust.go @@ -10,8 +10,16 @@ import ( "github.com/matrix-org/complement/must" ) +func init() { + matrix_sdk_ffi.SetupTracing(matrix_sdk_ffi.TracingConfiguration{ + WriteToStdoutOrSystem: true, + Filter: "debug", + }) +} + type RustRoomInfo struct { attachedListener bool + room *matrix_sdk_ffi.Room timeline []*Event } @@ -98,28 +106,43 @@ func (c *RustClient) SendMessage(t *testing.T, roomID, text string) { r.Send(matrix_sdk_ffi.MessageEventContentFromHtml(text, text)) } +func (c *RustClient) MustBackpaginate(t *testing.T, roomID string, count int) { + t.Helper() + t.Logf("[%s] MustBackpaginate %d %s", c.userID, count, roomID) + r := c.findRoom(roomID) + must.NotEqual(t, r, nil, "unknown room") + must.NotError(t, "failed to backpaginate", r.PaginateBackwards(matrix_sdk_ffi.PaginationOptionsSingleRequest{ + EventLimit: uint16(count), + })) +} + func (c *RustClient) findRoom(roomID string) *matrix_sdk_ffi.Room { rooms := c.FFIClient.Rooms() - for _, r := range rooms { + for i, r := range rooms { + rid := r.Id() + // ensure we only store rooms once + _, exists := c.rooms[rid] + if !exists { + c.rooms[rid] = &RustRoomInfo{ + room: rooms[i], + } + } if r.Id() == roomID { - return r + return c.rooms[rid].room } } return nil } func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ffi.Room { - info, ok := c.rooms[roomID] - if !ok { - info = &RustRoomInfo{} - } - if info.attachedListener { - // TODO: will this work - can you send msgs twice? - return c.findRoom(roomID) - } r := c.findRoom(roomID) must.NotEqual(t, r, nil, fmt.Sprintf("room %s does not exist", roomID)) - c.rooms[roomID] = info + + info := c.rooms[roomID] + if info.attachedListener { + return r + } + t.Logf("[%s]AddTimelineListenerBlocking[%s]", c.userID, roomID) // we need a timeline listener before we can send messages r.AddTimelineListenerBlocking(&timelineListener{fn: func(diff []*matrix_sdk_ffi.TimelineDiff) { @@ -136,7 +159,15 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff t.Logf("TimelineListener[%s] INSERT %d out of bounds of events timeline of size %d", roomID, i, len(timeline)) continue } - timeline[i] = timelineItemToEvent(insertData.Item) + if timeline[i] != nil { + // shift the item in this position right and insert this item + timeline = append(timeline, nil) + copy(timeline[i+1:], timeline[i:]) + timeline[i] = timelineItemToEvent(insertData.Item) + } else { + timeline[i] = timelineItemToEvent(insertData.Item) + } + fmt.Printf("[%s]_______ INSERT %+v\n", c.userID, timeline[i]) case matrix_sdk_ffi.TimelineChangeAppend: appendItems := d.Append() if appendItems == nil { @@ -145,6 +176,7 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff for _, item := range *appendItems { ev := timelineItemToEvent(item) timeline = append(timeline, ev) + fmt.Printf("[%s]_______ APPEND %+v\n", c.userID, ev) } case matrix_sdk_ffi.TimelineChangePushBack: pbData := d.PushBack() @@ -153,6 +185,7 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff } ev := timelineItemToEvent(*pbData) timeline = append(timeline, ev) + fmt.Printf("[%s]_______ PUSH BACK %+v\n", c.userID, ev) case matrix_sdk_ffi.TimelineChangeSet: setData := d.Set() if setData == nil { @@ -164,6 +197,9 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff continue } timeline[i] = timelineItemToEvent(setData.Item) + fmt.Printf("[%s]_______ SET %+v\n", c.userID, timeline[i]) + default: + t.Logf("Unhandled TimelineDiff change %v", d.Change()) } } c.rooms[roomID].timeline = timeline diff --git a/tests/invite_test.go b/tests/invite_test.go index ef0f4b4..c62c688 100644 --- a/tests/invite_test.go +++ b/tests/invite_test.go @@ -12,13 +12,16 @@ import ( // This test checks that Bob can decrypt messages sent before he was joined but after he was invited. // - Alice creates the room. Alice invites Bob. // - Alice sends an encrypted message. -// - Bob joins the room. +// - Bob joins the room and backpaginates. // - Ensure Bob can see the decrypted content. -func TestCanSeeMessagesAfterInviteButBeforeJoin(t *testing.T) { - ClientTypeMatrix(t, testCanSeeMessagesAfterInviteButBeforeJoin) +func TestCanDecryptMessagesAfterInviteButBeforeJoin(t *testing.T) { + ClientTypeMatrix(t, testCanDecryptMessagesAfterInviteButBeforeJoin) } -func testCanSeeMessagesAfterInviteButBeforeJoin(t *testing.T, clientTypeA, clientTypeB api.ClientType) { +func testCanDecryptMessagesAfterInviteButBeforeJoin(t *testing.T, clientTypeA, clientTypeB api.ClientType) { + if clientTypeA == api.ClientTypeRust && clientTypeB == api.ClientTypeRust { + t.Skip("Skipping rust/rust as SS proxy sends invite/join in timeline, omitting the invite msg") + } // Setup Code // ---------- deployment := Deploy(t) @@ -77,6 +80,10 @@ func testCanSeeMessagesAfterInviteButBeforeJoin(t *testing.T, clientTypeA, clien // Alice sends the message whilst Bob is still invited. alice.SendMessage(t, roomID, wantMsgBody) + // wait for SS proxy to get it. Only needed when testing Rust TODO FIXME + // Without this, the join will race with sending the msg and you could end up with the + // message being sent POST join, which breaks the point of this test. + time.Sleep(time.Second) // Bob joins the room (via Complement, but it shouldn't matter) csapiBob.MustJoinRoom(t, roomID, []string{"hs1"}) @@ -86,8 +93,18 @@ func testCanSeeMessagesAfterInviteButBeforeJoin(t *testing.T, clientTypeA, clien must.Equal(t, isEncrypted, true, "room is not encrypted") t.Logf("bob room encrypted = %v", isEncrypted) - // TODO: this is sensitive to the timeline limit used on the SDK. If 1, then the message won't - // be here (and will need to be fetched via /messages). - waiter := bob.WaitUntilEventInRoom(t, roomID, wantMsgBody) + // send a sentinel message and wait for it to ensure we are joined and syncing + sentinelBody := "Sentinel" + waiter := bob.WaitUntilEventInRoom(t, roomID, sentinelBody) + alice.SendMessage(t, roomID, sentinelBody) waiter.Wait(t, 2*time.Second) + + // Explicitly ask for a pagination, rather than assuming the SDK will return events + // earlier than the join by default. This is important because: + // - sync v2 (JS SDK) it depends on the timeline limit, which is 20 by default but we don't want to assume. + // - sliding sync (FFI) it won't return events before the join by default, relying on clients using the prev_batch token. + waiter = bob.WaitUntilEventInRoom(t, roomID, wantMsgBody) + bob.MustBackpaginate(t, roomID, 5) // number is arbitrary, just needs to be >=2 + waiter.Wait(t, 2*time.Second) + // time.Sleep(time.Hour) }