Skip to content
Merged
Changes from all commits
Commits
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
355 changes: 355 additions & 0 deletions tests/csapi/apidoc_search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
package csapi_tests

import (
"fmt"
"net/http"
"net/url"
"testing"

"github.com/matrix-org/util"
"github.com/tidwall/gjson"

"github.com/matrix-org/complement/internal/b"
"github.com/matrix-org/complement/internal/client"
"github.com/matrix-org/complement/internal/match"
"github.com/matrix-org/complement/internal/must"
"github.com/matrix-org/complement/runtime"
)

// Note: In contrast to Sytest, we define a filter.rooms on each search request, this allows us to run in parallel.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the point that sytest assumes each test was run in isolation, so that there were no other rooms to search for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Exactly, Sytest is creating separate a user and room for each test, so would only receive the results from those rooms.
Since we're only using Alice and a new room for each test, we need to filter on the room.

func TestSearch(t *testing.T) {
runtime.SkipIf(t, runtime.Dendrite) // https://github.com/matrix-org/dendrite/pull/2675
deployment := Deploy(t, b.BlueprintAlice)
defer deployment.Destroy(t)

alice := deployment.Client(t, "hs1", "@alice:hs1")

t.Run("parallel", func(t *testing.T) {
// sytest: Can search for an event by body
t.Run("Can search for an event by body", func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "private_chat",
})
eventID := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "hello, world",
},
})

searchRequest := client.WithJSONBody(t, map[string]interface{}{
"search_categories": map[string]interface{}{
"room_events": map[string]interface{}{
"keys": []string{"content.body"},
"search_term": "hello",
"filter": map[string]interface{}{
"rooms": []string{roomID},
},
},
},
})

resp := alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest)
sce := "search_categories.room_events"
result0 := sce + ".results.0.result"
must.MatchResponse(t, resp, match.HTTPResponse{
StatusCode: http.StatusOK,
JSON: []match.JSON{
match.JSONKeyPresent(sce + ".count"),
match.JSONKeyPresent(sce + ".results"),
match.JSONKeyEqual(sce+".count", float64(1)),
match.JSONKeyEqual(result0+".event_id", eventID),
match.JSONKeyEqual(result0+".room_id", roomID),
match.JSONKeyPresent(result0 + ".content"),
match.JSONKeyPresent(result0 + ".type"),
match.JSONKeyEqual(result0+".content.body", "hello, world"),
},
})
})

// sytest: Can get context around search results
t.Run("Can get context around search results", func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "private_chat",
})

for i := 1; i <= 7; i++ {
alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": fmt.Sprintf("Message number %d", i),
"msgtype": "m.text",
},
})
}

searchRequest := client.WithJSONBody(t, map[string]interface{}{
"search_categories": map[string]interface{}{
"room_events": map[string]interface{}{
"keys": []string{"content.body"},
"search_term": "Message 4",
"order_by": "recent",
"filter": map[string]interface{}{
"limit": 1,
"rooms": []string{roomID},
},
"event_context": map[string]interface{}{
"before_limit": 2,
"after_limit": 2,
},
},
},
})

resp := alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest)
sce := "search_categories.room_events"
result0 := sce + ".results.0.result"
resBefore := sce + ".results.0.context.events_before"
resAfter := sce + ".results.0.context.events_after"
must.MatchResponse(t, resp, match.HTTPResponse{
StatusCode: http.StatusOK,
JSON: []match.JSON{
match.JSONKeyPresent(sce + ".count"),
match.JSONKeyPresent(sce + ".results"),
match.JSONKeyPresent(sce + ".next_batch"),
match.JSONKeyEqual(sce+".count", float64(1)),
match.JSONKeyEqual(result0+".room_id", roomID),
match.JSONKeyPresent(result0 + ".content"),
match.JSONKeyPresent(result0 + ".type"),
match.JSONKeyEqual(result0+".content.body", "Message number 4"),
match.JSONKeyEqual(resBefore+".0.content.body", "Message number 3"),
match.JSONKeyEqual(resBefore+".1.content.body", "Message number 2"),
match.JSONKeyEqual(resAfter+".0.content.body", "Message number 5"),
match.JSONKeyEqual(resAfter+".1.content.body", "Message number 6"),
},
})
})

// sytest: Can back-paginate search results
t.Run("Can back-paginate search results", func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "private_chat",
})

eventIDs := make([]string, 20)
for i := 0; i <= 19; i++ {
eventID := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": fmt.Sprintf("Message number %d", i),
"msgtype": "m.text",
},
})
eventIDs[i] = eventID
}

searchRequest := client.WithJSONBody(t, map[string]interface{}{
"search_categories": map[string]interface{}{
"room_events": map[string]interface{}{
"keys": []string{"content.body"},
"search_term": "Message",
"order_by": "recent",
"filter": map[string]interface{}{
"limit": 10,
"rooms": []string{roomID},
},
},
},
})

resp := alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest)

// First search result
nextBatch := checkBackpaginateResult(t, resp, 20, eventIDs[19], eventIDs[10])

if nextBatch == "" {
t.Fatalf("no next batch set!")
}

values := url.Values{}
values.Set("next_batch", nextBatch)
params := client.WithQueries(values)
resp = alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest, params)
// Second search result
nextBatch = checkBackpaginateResult(t, resp, 20, eventIDs[9], eventIDs[0])

// At this point we expect next_batch to be empty
values.Set("next_batch", nextBatch)
params = client.WithQueries(values)
resp = alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest, params)
// third search result
sce := "search_categories.room_events"
result0 := sce + ".results.0.result"
must.MatchResponse(t, resp, match.HTTPResponse{
StatusCode: http.StatusOK,
JSON: []match.JSON{
match.JSONKeyPresent(sce + ".count"),
match.JSONKeyPresent(sce + ".results"),
match.JSONKeyEqual(sce+".count", float64(20)),
match.JSONKeyMissing(result0),
match.JSONKeyMissing(sce + ".next_batch"),
},
})
})

// sytest: Search works across an upgraded room and its predecessor
t.Run("Search works across an upgraded room and its predecessor", func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "private_chat",
"version": "8",
})

eventBeforeUpgrade := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": "Message before upgrade",
"msgtype": "m.text",
},
})

upgradeBody := client.WithJSONBody(t, map[string]string{
"new_version": "9",
})
upgradeResp := alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, upgradeBody)
body := must.ParseJSON(t, upgradeResp.Body)
newRoomID := must.GetJSONFieldStr(t, body, "replacement_room")

eventAfterUpgrade := alice.SendEventSynced(t, newRoomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": "Message after upgrade",
"msgtype": "m.text",
},
})

searchRequest := client.WithJSONBody(t, map[string]interface{}{
"search_categories": map[string]interface{}{
"room_events": map[string]interface{}{
"keys": []string{"content.body"},
"search_term": "upgrade",
"filter": map[string]interface{}{
"rooms": []string{roomID, newRoomID},
},
},
},
})

resp := alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest)
sce := "search_categories.room_events"
result0 := sce + ".results.0.result"
result1 := sce + ".results.1.result"
must.MatchResponse(t, resp, match.HTTPResponse{
StatusCode: http.StatusOK,
JSON: []match.JSON{
match.JSONKeyPresent(sce + ".count"),
match.JSONKeyPresent(sce + ".results"),
match.JSONKeyEqual(sce+".count", float64(2)),
match.JSONKeyPresent(result0 + ".content"),
match.JSONKeyPresent(result0 + ".type"),
match.JSONKeyEqual(result0+".event_id", eventBeforeUpgrade),
match.JSONKeyEqual(result1+".event_id", eventAfterUpgrade),
match.JSONKeyEqual(result0+".content.body", "Message before upgrade"),
match.JSONKeyEqual(result1+".content.body", "Message after upgrade"),
},
})
})

for _, ordering := range []string{"rank", "recent"} {
// sytest: Search results with $ordering_type ordering do not include redacted events
t.Run(fmt.Sprintf("Search results with %s ordering do not include redacted events", ordering), func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{
"preset": "private_chat",
})

redactedEventID := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": "This message is going to be redacted",
"msgtype": "m.text",
},
})

wantContentBody := "This message is not going to be redacted"
visibleEventID := alice.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": wantContentBody,
"msgtype": "m.text",
},
})

// redact the event
redactBody := client.WithJSONBody(t, map[string]interface{}{"reason": "testing"})
txnID := util.RandomString(8) // random string, as time.Now().Unix() might create the same txnID
resp := alice.MustDoFunc(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "redact", redactedEventID, txnID}, redactBody)
j := must.ParseJSON(t, resp.Body)
redactionEventID := must.GetJSONFieldStr(t, j, "event_id")
// wait for the redaction to come down sync
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, redactionEventID))

searchRequest := client.WithJSONBody(t, map[string]interface{}{
"search_categories": map[string]interface{}{
"room_events": map[string]interface{}{
"keys": []string{"content.body"},
"order_by": ordering,
"search_term": "redacted",
"filter": map[string]interface{}{
"rooms": []string{roomID},
},
},
},
})

resp = alice.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest)
sce := "search_categories.room_events"
result0 := sce + ".results.0.result"
must.MatchResponse(t, resp, match.HTTPResponse{
StatusCode: http.StatusOK,
JSON: []match.JSON{
match.JSONKeyPresent(sce + ".count"),
match.JSONKeyPresent(sce + ".results"),
match.JSONKeyArrayOfSize(sce+".results", 1),
match.JSONKeyPresent(result0 + ".content"),
match.JSONKeyPresent(result0 + ".type"),
match.JSONKeyEqual(result0+".event_id", visibleEventID),
match.JSONKeyEqual(result0+".content.body", wantContentBody),
},
})
})
}

})
}

func checkBackpaginateResult(t *testing.T, resp *http.Response, wantCount float64, lastEventID, firstEventID string) (nextBatch string) {
sce := "search_categories.room_events"
result0 := sce + ".results.0.result"
result9 := sce + ".results.9.result"
must.MatchResponse(t, resp, match.HTTPResponse{
StatusCode: http.StatusOK,
JSON: []match.JSON{
match.JSONKeyPresent(sce + ".count"),
match.JSONKeyPresent(sce + ".results"),
match.JSONMapEach(sce, func(k, v gjson.Result) error {
if k.Str == "next_batch" {
nextBatch = v.Str
return nil
}
return nil
}),
match.JSONKeyPresent(sce + ".next_batch"),
match.JSONKeyEqual(sce+".count", wantCount),
match.JSONKeyPresent(result0 + ".content"),
match.JSONKeyPresent(result0 + ".type"),
match.JSONKeyEqual(result0+".event_id", lastEventID),
match.JSONKeyEqual(result9+".event_id", firstEventID),
},
})
return
}