complement/tests/csapi/apidoc_search_test.go
kegsay 503bf6e126
Refactor the match.CheckOff API to use functional options (#711)
This means you don't need to tack on `, nil` all the time, and can
optionally specify when you want to allow unwanted items, obviating
the need for `JSONCheckOffAllowUnwanted`. This composes better and
supports adding additional functionality e.g allowing duplicate items.
2024-02-16 17:26:35 +00:00

366 lines
12 KiB
Go

package csapi_tests
import (
"fmt"
"net/http"
"net/url"
"testing"
"github.com/tidwall/gjson"
"github.com/matrix-org/complement"
"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/client"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/match"
"github.com/matrix-org/complement/must"
)
// Note: In contrast to Sytest, we define a filter.rooms on each search request, this is to mimic
// creating a new user and new room per test. This also allows us to run in parallel.
func TestSearch(t *testing.T) {
deployment := complement.Deploy(t, 1)
defer deployment.Destroy(t)
alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{})
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.MustCreateRoom(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.MustDo(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.MustCreateRoom(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.MustDo(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.MustCreateRoom(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.MustDo(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.MustDo(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.MustDo(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.MustCreateRoom(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",
},
})
t.Logf("Sent message before upgrade with event ID %s", eventBeforeUpgrade)
upgradeBody := client.WithJSONBody(t, map[string]string{
"new_version": "9",
})
upgradeResp := alice.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, upgradeBody)
body := must.ParseJSON(t, upgradeResp.Body)
newRoomID := must.GetJSONFieldStr(t, body, "replacement_room")
t.Logf("Replaced room %s with %s", roomID, newRoomID)
eventAfterUpgrade := alice.SendEventSynced(t, newRoomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"body": "Message after upgrade",
"msgtype": "m.text",
},
})
t.Logf("Sent message after upgrade with event ID %s", eventAfterUpgrade)
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.MustDo(t, "POST", []string{"_matrix", "client", "v3", "search"}, searchRequest)
sce := "search_categories.room_events"
expectedEvents := map[string]string{
eventBeforeUpgrade: "Message before upgrade",
eventAfterUpgrade: "Message after upgrade",
}
must.MatchResponse(t, resp, match.HTTPResponse{
StatusCode: http.StatusOK,
JSON: []match.JSON{
match.JSONKeyEqual(sce+".count", float64(2)),
match.JSONKeyArrayOfSize(sce+".results", 2),
// the results can be in either order: check that both are there and that the content is as expected
match.JSONCheckOff(sce+".results", []interface{}{eventBeforeUpgrade, eventAfterUpgrade}, match.CheckOffMapper(func(res gjson.Result) interface{} {
return res.Get("result.event_id").Str
}), match.CheckOffForEach(func(eventID interface{}, result gjson.Result) error {
matchers := []match.JSON{
match.JSONKeyEqual("result.type", "m.room.message"),
match.JSONKeyEqual("result.content.body", expectedEvents[eventID.(string)]),
}
for _, jm := range matchers {
if err := jm(result); err != nil {
return err
}
}
return nil
})),
},
})
})
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.MustCreateRoom(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
redactionEventID := alice.MustSendRedaction(t, roomID, map[string]interface{}{"reason": "testing"}, redactedEventID)
// 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.MustDo(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
}