Skip to content
121 changes: 75 additions & 46 deletions tests/room_timestamp_to_event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,19 +204,88 @@ func TestJumpToDateEndpoint(t *testing.T) {
mustCheckEventisReturnedForTime(t, remoteCharlie, roomID, timeBeforeRoomCreation, "b", importedEventID)
})

t.Run("can paginate after getting remote event from timestamp to event endpoint", func(t *testing.T) {
t.Run("can paginate backwards after getting remote event from timestamp to event endpoint (start)", func(t *testing.T) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test will fail until element-hq/synapse#19611 is merged

Copy link
Copy Markdown
Collaborator Author

@MadLittleMods MadLittleMods Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to have a test for paginating from the start and end tokens from /context after getting an event from /timestamp_to_event.

Previously, we only had a test for end

t.Parallel()
roomID, eventA, eventB := createTestRoom(t, alice)
remoteCharlie.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})
// After Charlie's homeserver finds the event, it "should try to backfill this
// event" (per the spec,
// https://spec.matrix.org/v1.17/server-server-api/#get_matrixfederationv1timestamp_to_eventroomid)
mustCheckEventisReturnedForTime(t, remoteCharlie, roomID, eventB.AfterTimestamp, "b", eventB.EventID)

// Get a pagination token from eventB
// And then "clients can call /rooms/{roomId}/context/{eventId} to obtain a
// pagination token to retrieve the events around the returned event." (per the
// spec, https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1roomsroomidtimestamp_to_event).
//
// Get a pagination token that represents the position just *before* eventB
contextRes := remoteCharlie.MustDo(t, "GET", []string{"_matrix", "client", "r0", "rooms", roomID, "context", eventB.EventID},
client.WithContentType("application/json"), client.WithQueries(url.Values{
"limit": []string{"0"},
}),
// Retry as the worker backfilling and persisting the event isn't necessarily
// the same as the worker serving `/context`
client.WithRetryUntil(remoteCharlie.SyncUntilTimeout, func(res *http.Response) bool {
return res.StatusCode == 200
}))
)
contextResResBody := client.ParseJSON(t, contextRes)
// Remember: Tokens are positions between events.
//
// start end
// | |
// [A] <-- ▼ [B] ▼ <--- [remoteCharlie join]
//
// "start" is the token that represents the position just *before* eventB
paginationToken := client.GetJSONFieldStr(t, contextResResBody, "start")

// Paginate backwards seamlessly from the `/context` request (start, point
// before eventB)
messagesRes := remoteCharlie.MustDo(t, "GET", []string{"_matrix", "client", "r0", "rooms", roomID, "messages"},
client.WithContentType("application/json"),
client.WithQueries(url.Values{
"dir": []string{"b"},
"limit": []string{"100"},
"from": []string{paginationToken},
}),
)

// Make sure A is visible
must.MatchResponse(t, messagesRes, match.HTTPResponse{
JSON: []match.JSON{
match.JSONCheckOff("chunk", []interface{}{eventA.EventID}, match.CheckOffMapper(func(r gjson.Result) interface{} {
return r.Get("event_id").Str
}), match.CheckOffAllowUnwanted()),
},
})
})

t.Run("can paginate backwards after getting remote event from timestamp to event endpoint (end)", func(t *testing.T) {
t.Parallel()
roomID, eventA, eventB := createTestRoom(t, alice)
remoteCharlie.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})
// After Charlie's homeserver finds the event, it "should try to backfill this
// event" (per the spec,
// https://spec.matrix.org/v1.17/server-server-api/#get_matrixfederationv1timestamp_to_eventroomid)
mustCheckEventisReturnedForTime(t, remoteCharlie, roomID, eventB.AfterTimestamp, "b", eventB.EventID)

// And then "clients can call /rooms/{roomId}/context/{eventId} to obtain a
// pagination token to retrieve the events around the returned event." (per the
// spec, https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1roomsroomidtimestamp_to_event).
//
// Get a pagination token that represents the position just *after* eventB
contextRes := remoteCharlie.MustDo(t, "GET", []string{"_matrix", "client", "r0", "rooms", roomID, "context", eventB.EventID},
client.WithContentType("application/json"), client.WithQueries(url.Values{
"limit": []string{"0"},
}),
// Retry as the worker backfilling and persisting the event isn't necessarily
// the same as the worker serving `/context`
client.WithRetryUntil(remoteCharlie.SyncUntilTimeout, func(res *http.Response) bool {
return res.StatusCode == 200
}))
)
contextResResBody := client.ParseJSON(t, contextRes)
// Remember: Tokens are positions between events. Normally, you would use the
Expand All @@ -227,16 +296,12 @@ func TestJumpToDateEndpoint(t *testing.T) {
// start end
// | |
// [A] <-- ▼ [B] ▼ <--- [remoteCharlie join]
//
// "end" is the token that represents the position just *after* eventB
paginationToken := client.GetJSONFieldStr(t, contextResResBody, "end")

// Hit `/messages` until `eventA` has been backfilled and replicated across
// workers (the worker persisting events isn't necessarily the same as the worker
// serving `/messages`)
fetchUntilMessagesResponseHas(t, remoteCharlie, roomID, func(ev gjson.Result) bool {
return ev.Get("event_id").Str == eventA.EventID
})

// Paginate backwards from the point after eventB
// Paginate backwards seamlessly from the `/context` request (end, point after
// eventB)
messagesRes := remoteCharlie.MustDo(t, "GET", []string{"_matrix", "client", "r0", "rooms", roomID, "messages"},
client.WithContentType("application/json"),
client.WithQueries(url.Values{
Expand Down Expand Up @@ -357,42 +422,6 @@ func mustCheckEventisReturnedForTime(t *testing.T, c *client.CSAPI, roomID strin
}
}

func fetchUntilMessagesResponseHas(t *testing.T, c *client.CSAPI, roomID string, check func(gjson.Result) bool) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove fetchUntilMessagesResponseHas as it's now unused

t.Helper()
start := time.Now()
checkCounter := 0
for {
if time.Since(start) > c.SyncUntilTimeout {
t.Fatalf("fetchUntilMessagesResponseHas timed out. Called check function %d times", checkCounter)
}

messagesRes := c.MustDo(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "messages"}, client.WithContentType("application/json"), client.WithQueries(url.Values{
"dir": []string{"b"},
"limit": []string{"100"},
}))
messsageResBody := client.ParseJSON(t, messagesRes)
wantKey := "chunk"
keyRes := gjson.GetBytes(messsageResBody, wantKey)
if !keyRes.Exists() {
t.Fatalf("missing key '%s'", wantKey)
}
if !keyRes.IsArray() {
t.Fatalf("key '%s' is not an array (was %s)", wantKey, keyRes.Type)
}

events := keyRes.Array()
for _, ev := range events {
if check(ev) {
return
}
}

checkCounter++
// Add a slight delay so we don't hammmer the messages endpoint
time.Sleep(500 * time.Millisecond)
}
}

func getDebugMessageListFromMessagesResponse(t *testing.T, c *client.CSAPI, roomID string, expectedEventId string, actualEventId string, givenTimestamp int64) string {
t.Helper()

Expand Down
Loading