Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MSC3030 Jump to date API endpoint #178

Merged
merged 23 commits into from
Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1889d0a
Add tests to ensure MSC3030 functionality
MadLittleMods Jul 28, 2021
55385ec
Add color coding debug string when the test fails
MadLittleMods Jul 28, 2021
2a360a0
Remove unused GetEvent and GetJSONFieldStringMap functions
MadLittleMods Jul 28, 2021
352b8e9
Merge branch 'master' into madlittlemods/msc3030-jump-to-date
MadLittleMods Nov 10, 2021
5990efe
Fix timestamp insertion in debug string
MadLittleMods Nov 12, 2021
f8b88a9
Show debug message list in scrollback order how you would see it in a…
MadLittleMods Nov 12, 2021
42af51a
Update to use direction paremeter
MadLittleMods Nov 12, 2021
5233383
Add tests for finding nothing before and after the event timeline
MadLittleMods Nov 12, 2021
4348803
Add some comments
MadLittleMods Nov 12, 2021
83f993c
Make tests parallel and add federation tests
MadLittleMods Nov 16, 2021
7d3a691
Add experimental feature flag and use unstable endpoint
MadLittleMods Nov 17, 2021
97a287f
Add potential history visibility test
MadLittleMods Nov 30, 2021
d96622b
Remove superfluous history visibility test
MadLittleMods Nov 30, 2021
e8787c1
Add actual/expected text labels so it's usable with ascii colors
MadLittleMods Dec 3, 2021
f71f88f
Remove commented out code
MadLittleMods Dec 3, 2021
94957f4
Add test to make sure we're not leaking events from private rooms
MadLittleMods Dec 3, 2021
39cb82c
Refactor language to want/got
MadLittleMods Dec 18, 2021
2486df7
Merge remote-tracking branch 'origin/main' into madlittlemods/msc3030…
MadLittleMods Feb 18, 2022
5678986
Merge branch 'main' into madlittlemods/msc3030-jump-to-date
MadLittleMods Mar 1, 2022
148387e
Always set preset since Synapse/Dendrite disagree on what the default is
MadLittleMods Mar 3, 2022
ccb51a9
Fix message A vs B typo
MadLittleMods Mar 3, 2022
3b914c2
Merge branch 'main' into madlittlemods/msc3030-jump-to-date
MadLittleMods Mar 3, 2022
a383a08
Also add test to make sure we don't leak from public room either
MadLittleMods Mar 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dockerfiles/synapse/homeserver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,5 @@ experimental_features:
spaces_enabled: true
# Enable history backfilling support
msc2716_enabled: true
# Enable jump to date endpoint
msc3030_enabled: true
2 changes: 2 additions & 0 deletions dockerfiles/synapse/workers-shared.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ experimental_features:
msc2716_enabled: true
# Enable spaces support
spaces_enabled: true
# Enable jump to date endpoint
msc3030_enabled: true
270 changes: 270 additions & 0 deletions tests/msc3030_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// +build msc3030

// This file contains tests for a jump to date API endpoint,
// currently experimental feature defined by MSC3030, which you can read here:
// https://github.com/matrix-org/matrix-doc/pull/3030

package tests

import (
"fmt"
"net/url"
"strconv"
"testing"
"time"

"github.com/matrix-org/complement/internal/b"
"github.com/matrix-org/complement/internal/client"
"github.com/tidwall/gjson"
)

func TestJumpToDateEndpoint(t *testing.T) {
deployment := Deploy(t, b.BlueprintFederationTwoLocalOneRemote)
defer deployment.Destroy(t)

// Create the normal user which will send messages in the room
userID := "@alice:hs1"
alice := deployment.Client(t, "hs1", userID)

// Create the federated user which will fetch the messages from a remote homeserver
remoteUserID := "@charlie:hs2"
remoteCharlie := deployment.Client(t, "hs2", remoteUserID)

t.Run("parallel", func(t *testing.T) {
t.Run("should find event after given timestmap", func(t *testing.T) {
t.Parallel()
roomID, eventA, _ := createTestRoom(t, alice)
mustCheckEventisReturnedForTime(t, alice, roomID, eventA.BeforeTimestamp, "f", eventA.EventID)
})

t.Run("should find event before given timestmap", func(t *testing.T) {
t.Parallel()
roomID, _, eventB := createTestRoom(t, alice)
mustCheckEventisReturnedForTime(t, alice, roomID, eventB.AfterTimestamp, "b", eventB.EventID)
})

t.Run("should find nothing before the earliest timestmap", func(t *testing.T) {
t.Parallel()
timeBeforeRoomCreation := time.Now()
roomID, _, _ := createTestRoom(t, alice)
mustCheckEventisReturnedForTime(t, alice, roomID, timeBeforeRoomCreation, "b", "")
})

t.Run("should find nothing after the latest timestmap", func(t *testing.T) {
t.Parallel()
roomID, _, eventB := createTestRoom(t, alice)
mustCheckEventisReturnedForTime(t, alice, roomID, eventB.AfterTimestamp, "f", "")
})

// Just a sanity check that we're not leaking anything from the `/timestamp_to_event` endpoint
t.Run("should not be able to query a private room you are not a member of", func(t *testing.T) {
t.Parallel()
timeBeforeRoomCreation := time.Now()

// Alice will create the private room
roomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "private_chat",
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
})

// We will use Bob to query the room they're not a member of
nonMemberUser := deployment.Client(t, "hs1", "@bob:hs1")

// Make the `/timestamp_to_event` request from Bob's perspective (non room member)
timestamp := makeTimestampFromTime(timeBeforeRoomCreation)
timestampString := strconv.FormatInt(timestamp, 10)
timestampToEventRes := nonMemberUser.DoFunc(t, "GET", []string{"_matrix", "client", "unstable", "org.matrix.msc3030", "rooms", roomID, "timestamp_to_event"}, client.WithContentType("application/json"), client.WithQueries(url.Values{
"ts": []string{timestampString},
"dir": []string{"f"},
}))

// A random user is not allowed to query for events in a private room
// they're not a member of (forbidden).
if timestampToEventRes.StatusCode != 403 {
t.Fatalf("/timestamp_to_event returned %d HTTP status code but expected %d", timestampToEventRes.StatusCode, 403)
}
})

t.Run("federation", func(t *testing.T) {
t.Run("looking forwards, should be able to find event that was sent before we joined", func(t *testing.T) {
t.Parallel()
roomID, eventA, _ := createTestRoom(t, alice)
remoteCharlie.JoinRoom(t, roomID, []string{"hs1"})
mustCheckEventisReturnedForTime(t, remoteCharlie, roomID, eventA.BeforeTimestamp, "f", eventA.EventID)
})

t.Run("looking backwards, should be able to find event that was sent before we joined", func(t *testing.T) {
t.Parallel()
roomID, _, eventB := createTestRoom(t, alice)
remoteCharlie.JoinRoom(t, roomID, []string{"hs1"})
mustCheckEventisReturnedForTime(t, remoteCharlie, roomID, eventB.AfterTimestamp, "b", eventB.EventID)
})
})
})
}

type eventTime struct {
EventID string
BeforeTimestamp time.Time
AfterTimestamp time.Time
}

func createTestRoom(t *testing.T, c *client.CSAPI) (roomID string, eventA, eventB *eventTime) {
t.Helper()

roomID = c.CreateRoom(t, map[string]interface{}{})
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved

timeBeforeEventA := time.Now()
eventAID := c.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Message A",
},
})
timeAfterEventA := time.Now()

eventBID := c.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "Message A",
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
},
})
timeAfterEventB := time.Now()

eventA = &eventTime{EventID: eventAID, BeforeTimestamp: timeBeforeEventA, AfterTimestamp: timeAfterEventA}
eventB = &eventTime{EventID: eventBID, BeforeTimestamp: timeAfterEventA, AfterTimestamp: timeAfterEventB}

return roomID, eventA, eventB
}

// Fetch event from /timestamp_to_event and ensure it matches the expectedEventId
func mustCheckEventisReturnedForTime(t *testing.T, c *client.CSAPI, roomID string, givenTime time.Time, direction string, expectedEventId string) {
t.Helper()

givenTimestamp := makeTimestampFromTime(givenTime)
timestampString := strconv.FormatInt(givenTimestamp, 10)
timestampToEventRes := c.DoFunc(t, "GET", []string{"_matrix", "client", "unstable", "org.matrix.msc3030", "rooms", roomID, "timestamp_to_event"}, client.WithContentType("application/json"), client.WithQueries(url.Values{
"ts": []string{timestampString},
"dir": []string{direction},
}))
timestampToEventResBody := client.ParseJSON(t, timestampToEventRes)

// Only allow a 200 response meaning we found an event or a 404 meaning we didn't.
// Other status codes will throw and assumed to be application errors.
actualEventId := ""
if timestampToEventRes.StatusCode == 200 {
actualEventId = client.GetJSONFieldStr(t, timestampToEventResBody, "event_id")
} else if timestampToEventRes.StatusCode != 404 {
t.Fatalf("mustCheckEventisReturnedForTime: /timestamp_to_event request failed with status=%d", timestampToEventRes.StatusCode)
}

if actualEventId != expectedEventId {
debugMessageList := getDebugMessageListFromMessagesResponse(t, c, roomID, expectedEventId, actualEventId, givenTimestamp)
t.Fatalf(
"Expected to see %s given %s but received %s\n%s",
decorateStringWithAnsiColor(expectedEventId, AnsiColorGreen),
decorateStringWithAnsiColor(timestampString, AnsiColorYellow),
decorateStringWithAnsiColor(actualEventId, AnsiColorRed),
debugMessageList,
)
kegsay marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

messagesRes := c.MustDoFunc(t, "GET", []string{"_matrix", "client", "r0", "rooms", roomID, "messages"}, client.WithContentType("application/json"), client.WithQueries(url.Values{
// The events returned will be from the newest -> oldest since we're going backwards
"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)
}

// Make the events go from oldest-in-time -> newest-in-time
events := reverseGjsonArray(keyRes.Array())
if len(events) == 0 {
t.Fatalf(
"getDebugMessageListFromMessagesResponse found no messages in the room(%s).",
roomID,
)
}

// We need some padding for some lines to make them all align with the label.
// Pad this out so it equals whatever the longest label is.
paddingString := " "

resultantString := fmt.Sprintf("%s-- oldest events --\n", paddingString)

givenTimestampAlreadyInserted := false
givenTimestampMarker := decorateStringWithAnsiColor(fmt.Sprintf("%s-- givenTimestamp=%s --\n", paddingString, strconv.FormatInt(givenTimestamp, 10)), AnsiColorYellow)

// We're iterating over the events from oldest-in-time -> newest-in-time
for _, ev := range events {
// As we go, keep checking whether the givenTimestamp is
// older(before-in-time) than the current event and insert a timestamp
// marker as soon as we find the spot
if givenTimestamp < ev.Get("origin_server_ts").Int() && !givenTimestampAlreadyInserted {
resultantString += givenTimestampMarker
givenTimestampAlreadyInserted = true
}

eventID := ev.Get("event_id").String()
eventIDString := eventID
labelString := paddingString
if eventID == expectedEventId {
eventIDString = decorateStringWithAnsiColor(eventID, AnsiColorGreen)
labelString = "(expected) "
} else if eventID == actualEventId {
eventIDString = decorateStringWithAnsiColor(eventID, AnsiColorRed)
labelString = " (actual) "
}

resultantString += fmt.Sprintf(
"%s%s (%s) - %s\n",
labelString,
eventIDString,
strconv.FormatInt(ev.Get("origin_server_ts").Int(), 10),
ev.Get("type").String(),
)
}

// The givenTimestamp could be newer(after-in-time) than any of the other events
if givenTimestamp > events[len(events)-1].Get("origin_server_ts").Int() && !givenTimestampAlreadyInserted {
resultantString += givenTimestampMarker
givenTimestampAlreadyInserted = true
}

resultantString += fmt.Sprintf("%s-- newest events --\n", paddingString)

return resultantString
}

func makeTimestampFromTime(t time.Time) int64 {
return t.UnixNano() / int64(time.Millisecond)
}

const AnsiColorRed string = "31"
const AnsiColorGreen string = "32"
const AnsiColorYellow string = "33"

func decorateStringWithAnsiColor(inputString, decorationColor string) string {
return fmt.Sprintf("\033[%sm%s\033[0m", decorationColor, inputString)
}

func reverseGjsonArray(in []gjson.Result) []gjson.Result {
out := make([]gjson.Result, len(in))
for i := 0; i < len(in); i++ {
out[i] = in[len(in)-i-1]
}
return out
}