diff --git a/README.md b/README.md index 63c31e7e..6a0b2cf5 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ update-ca-certificates ## Sytest parity -As of 10 February 2023: +As of 29 October 2025: ``` $ go build ./cmd/sytest-coverage $ ./sytest-coverage -v @@ -507,7 +507,13 @@ $ ./sytest-coverage -v ✓ Can get rooms/{roomId}/members 30rooms/60version_upgrade 0/19 tests -30rooms/70publicroomslist 0/5 tests +30rooms/70publicroomslist 2/5 tests + × Asking for a remote rooms list, but supplying the local server's name, returns the local rooms list + × Can get remote public room list + × Can paginate public room list + ✓ Can search public room list + ✓ Name/topic keys are correct + 31sync/01filter 2/2 tests ✓ Can create filter ✓ Can download filter @@ -707,5 +713,5 @@ $ ./sytest-coverage -v 90jira/SYN-516 0/1 tests 90jira/SYN-627 0/1 tests -TOTAL: 220/610 tests converted +TOTAL: 222/610 tests converted ``` diff --git a/client/client.go b/client/client.go index a6d6ceab..c7fb34b6 100644 --- a/client/client.go +++ b/client/client.go @@ -26,6 +26,7 @@ import ( "github.com/matrix-org/complement/b" "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/internal" ) type ctxKey string @@ -767,6 +768,9 @@ func (c *CSAPI) MustDo(t ct.TestLike, method string, paths []string, opts ...Req // match.JSONKeyEqual("errcode", "M_INVALID_USERNAME"), // }, // }) +// +// The caller does not need to worry about closing the returned `http.Response.Body` as +// this is handled automatically. func (c *CSAPI) Do(t ct.TestLike, method string, paths []string, opts ...RequestOpt) *http.Response { t.Helper() escapedPaths := make([]string, len(paths)) @@ -815,6 +819,30 @@ func (c *CSAPI) Do(t ct.TestLike, method string, paths []string, opts ...Request if err != nil { ct.Fatalf(t, "CSAPI.Do response returned error: %s", err) } + // `defer` is function scoped but it's okay that we only clean up all requests at + // the end. To also be clear, `defer` arguments are evaluated at the time of the + // `defer` statement so we are only closing the original response body here. Our new + // response body will be untouched. + defer internal.CloseIO( + res.Body, + fmt.Sprintf( + "CSAPI.Do: response body from %s %s", + res.Request.Method, + res.Request.URL.String(), + ), + ) + + // Make a copy of the response body so that downstream callers can read it multiple + // times if needed and don't need to worry about closing it. + var resBody []byte + if res.Body != nil { + resBody, err = io.ReadAll(res.Body) + if err != nil { + ct.Fatalf(t, "CSAPI.Do failed to read response body for RetryUntil check: %s", err) + } + res.Body = io.NopCloser(bytes.NewBuffer(resBody)) + } + // debug log the response if c.Debug && res != nil { var dump []byte @@ -824,19 +852,12 @@ func (c *CSAPI) Do(t ct.TestLike, method string, paths []string, opts ...Request } t.Logf("%s", string(dump)) } + if retryUntil == nil || retryUntil.timeout == 0 { return res // don't retry } - // check the condition, make a copy of the response body first in case the check consumes it - var resBody []byte - if res.Body != nil { - resBody, err = io.ReadAll(res.Body) - if err != nil { - ct.Fatalf(t, "CSAPI.Do failed to read response body for RetryUntil check: %s", err) - } - res.Body = io.NopCloser(bytes.NewBuffer(resBody)) - } + // check the condition if retryUntil.untilFn(res) { // remake the response and return res.Body = io.NopCloser(bytes.NewBuffer(resBody)) diff --git a/client/sync.go b/client/sync.go index 1916ce22..7939d944 100644 --- a/client/sync.go +++ b/client/sync.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "reflect" + "slices" "sort" "strings" "time" @@ -269,95 +270,138 @@ func SyncPresenceHas(fromUser string, expectedPresence *string, checks ...func(g } } -// Checks that `userID` gets invited to `roomID`. +// syncMembershipIn checks that `userID` has `membership` in `roomID`, with optional +// extra checks on the found membership event. // -// This checks different parts of the /sync response depending on the client making the request. -// If the client is also the person being invited to the room then the 'invite' block will be inspected. -// If the client is different to the person being invited then the 'join' block will be inspected. -func SyncInvitedTo(userID, roomID string) SyncCheckOpt { - return func(clientUserID string, topLevelSyncJSON gjson.Result) error { - // two forms which depend on what the client user is: - // - passively viewing an invite for a room you're joined to (timeline events) - // - actively being invited to a room. - if clientUserID == userID { - // active - err := checkArrayElements( - topLevelSyncJSON, "rooms.invite."+GjsonEscape(roomID)+".invite_state.events", - func(ev gjson.Result) bool { - return ev.Get("type").Str == "m.room.member" && ev.Get("state_key").Str == userID && ev.Get("content.membership").Str == "invite" - }, - ) - if err != nil { - return fmt.Errorf("SyncInvitedTo(%s): %s", roomID, err) - } - return nil - } - // passive - return SyncTimelineHas(roomID, func(ev gjson.Result) bool { - return ev.Get("type").Str == "m.room.member" && ev.Get("state_key").Str == userID && ev.Get("content.membership").Str == "invite" - })(clientUserID, topLevelSyncJSON) - } -} - -// Check that `userID` gets joined to `roomID` by inspecting the join timeline for a membership event. +// This can be also used to passively observe another user's membership changes in a +// room although we assume that the observing client is joined to the room. // -// Additional checks can be passed to narrow down the check, all must pass. -func SyncJoinedTo(userID, roomID string, checks ...func(gjson.Result) bool) SyncCheckOpt { - checkJoined := func(ev gjson.Result) bool { - if ev.Get("type").Str == "m.room.member" && ev.Get("state_key").Str == userID && ev.Get("content.membership").Str == "join" { +// Note: This will not work properly with leave/ban membership for initial syncs, see +// https://github.com/matrix-org/matrix-doc/issues/3537 +func syncMembershipIn(userID, roomID, membership string, checks ...func(gjson.Result) bool) SyncCheckOpt { + checkMembership := func(ev gjson.Result) bool { + if ev.Get("type").Str == "m.room.member" && ev.Get("state_key").Str == userID && ev.Get("content.membership").Str == membership { for _, check := range checks { if !check(ev) { // short-circuit, bail early return false } } - // passed both basic join check and all other checks + // passed both basic membership check and all other checks return true } return false } return func(clientUserID string, topLevelSyncJSON gjson.Result) error { - // Check both the timeline and the state events for the join event - // since on initial sync, the state events may only be in - // .state.events. + // Check both the timeline and the state events for the membership event since on + // initial sync, the state events may only be in state. Additionally, state only + // covers the "updates for the room up to the start of the timeline." + + // We assume the passively observing client user is joined to the room + roomTypeKey := "join" + // Otherwise, if the client is the user whose membership we are checking, we need to + // pick the correct room type JSON key based on the membership being checked. + if clientUserID == userID { + if membership == "join" { + roomTypeKey = "join" + } else if membership == "leave" || membership == "ban" { + roomTypeKey = "leave" + } else if membership == "invite" { + roomTypeKey = "invite" + } else if membership == "knock" { + roomTypeKey = "knock" + } else { + return fmt.Errorf("syncMembershipIn(%s, %s): unknown membership: %s", roomID, membership, membership) + } + } + + // We assume the passively observing client user is joined to the room (`rooms.join..state`) + stateKey := "state" + // Otherwise, if the client is the user whose membership we are checking, + // we need to pick the correct JSON key based on the membership being checked. + if clientUserID == userID { + if membership == "join" || membership == "leave" || membership == "ban" { + stateKey = "state" + } else if membership == "invite" { + stateKey = "invite_state" + } else if membership == "knock" { + stateKey = "knock_state" + } else { + return fmt.Errorf("syncMembershipIn(%s, %s): unknown membership: %s", roomID, membership, membership) + } + } + + // Check the state first as it's a better source of truth than the `timeline`. + // + // FIXME: Ideally, we'd use something like `state_after` to get the actual current + // state in the room instead of us assuming that no state resets/conflicts happen + // when we apply state from the `timeline` on top of the `state`. But `state_after` + // is gated behind a sync request parameter which we can't control here. firstErr := checkArrayElements( - topLevelSyncJSON, "rooms.join."+GjsonEscape(roomID)+".timeline.events", checkJoined, + topLevelSyncJSON, "rooms."+roomTypeKey+"."+GjsonEscape(roomID)+"."+stateKey+".events", checkMembership, ) if firstErr == nil { return nil } - secondErr := checkArrayElements( - topLevelSyncJSON, "rooms.join."+GjsonEscape(roomID)+".state.events", checkJoined, - ) - if secondErr == nil { - return nil + // Check the timeline + // + // This is also important to differentiate between leave/ban because those both + // appear in the `leave` `roomTypeKey` and we need to specifically check the + // timeline for the membership event to differentiate them. + var secondErr error + // The `timeline` is only available for join/leave/ban memberships. + if slices.Contains([]string{"join", "leave", "ban"}, membership) || + // We assume the passively observing client user is joined to the room (therefore + // has `timeline`). + clientUserID != userID { + secondErr = checkArrayElements( + topLevelSyncJSON, "rooms."+roomTypeKey+"."+GjsonEscape(roomID)+".timeline.events", checkMembership, + ) + if secondErr == nil { + return nil + } } - return fmt.Errorf("SyncJoinedTo(%s): %s & %s", roomID, firstErr, secondErr) + + return fmt.Errorf("syncMembershipIn(%s, %s): %s & %s - %s", roomID, membership, firstErr, secondErr, topLevelSyncJSON) } } -// Check that `userID` is leaving `roomID` by inspecting the timeline for a membership event, or witnessing `roomID` in `rooms.leave` +// Checks that `userID` gets invited to `roomID` +// +// Additional checks can be passed to narrow down the check, all must pass. +func SyncInvitedTo(userID, roomID string, checks ...func(gjson.Result) bool) SyncCheckOpt { + return syncMembershipIn(userID, roomID, "invite", checks...) +} + +// Checks that `userID` has knocked on `roomID` +// +// Additional checks can be passed to narrow down the check, all must pass. +func SyncKnockedOn(userID, roomID string, checks ...func(gjson.Result) bool) SyncCheckOpt { + return syncMembershipIn(userID, roomID, "knock", checks...) +} + +// Check that `userID` gets joined to `roomID` +// +// Additional checks can be passed to narrow down the check, all must pass. +func SyncJoinedTo(userID, roomID string, checks ...func(gjson.Result) bool) SyncCheckOpt { + return syncMembershipIn(userID, roomID, "join", checks...) +} + +// Check that `userID` has left the `roomID` // Note: This will not work properly with initial syncs, see https://github.com/matrix-org/matrix-doc/issues/3537 -func SyncLeftFrom(userID, roomID string) SyncCheckOpt { - return func(clientUserID string, topLevelSyncJSON gjson.Result) error { - // two forms which depend on what the client user is: - // - passively viewing a membership for a room you're joined in - // - actively leaving the room - if clientUserID == userID { - // active - events := topLevelSyncJSON.Get("rooms.leave." + GjsonEscape(roomID)) - if !events.Exists() { - return fmt.Errorf("no leave section for room %s", roomID) - } else { - return nil - } - } - // passive - return SyncTimelineHas(roomID, func(ev gjson.Result) bool { - return ev.Get("type").Str == "m.room.member" && ev.Get("state_key").Str == userID && ev.Get("content.membership").Str == "leave" - })(clientUserID, topLevelSyncJSON) - } +// +// Additional checks can be passed to narrow down the check, all must pass. +func SyncLeftFrom(userID, roomID string, checks ...func(gjson.Result) bool) SyncCheckOpt { + return syncMembershipIn(userID, roomID, "leave", checks...) +} + +// Check that `userID` is banned from the `roomID` +// Note: This will not work properly with initial syncs, see https://github.com/matrix-org/matrix-doc/issues/3537 +// +// Additional checks can be passed to narrow down the check, all must pass. +func SyncBannedFrom(userID, roomID string, checks ...func(gjson.Result) bool) SyncCheckOpt { + return syncMembershipIn(userID, roomID, "ban", checks...) } // Calls the `check` function for each global account data event, and returns with success if the diff --git a/cmd/account-snapshot/internal/sync.go b/cmd/account-snapshot/internal/sync.go index 616f5f07..278ceb7e 100644 --- a/cmd/account-snapshot/internal/sync.go +++ b/cmd/account-snapshot/internal/sync.go @@ -12,6 +12,8 @@ import ( "os" "strconv" "strings" + + "github.com/matrix-org/complement/internal" ) // LoadSyncData loads sync data from disk or by doing a /sync request @@ -75,7 +77,14 @@ func doRequest(httpCli *http.Client, req *http.Request, token string) ([]byte, e if err != nil { return nil, fmt.Errorf("failed to perform request: %w", err) } - defer res.Body.Close() + defer internal.CloseIO( + res.Body, + fmt.Sprintf( + "doRequest: response body from %s %s", + res.Request.Method, + res.Request.URL.String(), + ), + ) if res.StatusCode != 200 { return nil, fmt.Errorf("response returned %s", res.Status) } diff --git a/federation/server.go b/federation/server.go index d8825cc1..ba7478de 100644 --- a/federation/server.go +++ b/federation/server.go @@ -3,6 +3,7 @@ package federation import ( + "bytes" "context" "crypto/ed25519" "crypto/rand" @@ -12,6 +13,7 @@ import ( "encoding/json" "encoding/pem" "fmt" + "io" "io/ioutil" "math/big" "net" @@ -32,6 +34,7 @@ import ( "github.com/matrix-org/complement/config" "github.com/matrix-org/complement/ct" + "github.com/matrix-org/complement/internal" ) // Subset of Deployment used in federation @@ -278,6 +281,9 @@ func (s *Server) SendFederationRequest( // DoFederationRequest signs and sends an arbitrary federation request from this server, and returns the response. // // The requests will be routed according to the deployment map in `deployment`. +// +// The caller does not need to worry about closing the returned `http.Response.Body` as +// this is handled automatically. func (s *Server) DoFederationRequest( ctx context.Context, t ct.TestLike, @@ -297,12 +303,25 @@ func (s *Server) DoFederationRequest( var resp *http.Response resp, err = httpClient.DoHTTPRequest(ctx, httpReq) + defer internal.CloseIO(resp.Body, "DoFederationRequest: federation response body") if httpError, ok := err.(gomatrix.HTTPError); ok { t.Logf("[SSAPI] %s %s%s => error(%d): %s (%s)", req.Method(), req.Destination(), req.RequestURI(), httpError.Code, err, time.Since(start)) } else if err == nil { t.Logf("[SSAPI] %s %s%s => %d (%s)", req.Method(), req.Destination(), req.RequestURI(), resp.StatusCode, time.Since(start)) } + + // Make a copy of the response body so that downstream callers can read it multiple + // times if needed and don't need to worry about closing it. + var respBody []byte + if resp.Body != nil { + respBody, err = io.ReadAll(resp.Body) + if err != nil { + ct.Fatalf(t, "CSAPI.Do failed to read response body for RetryUntil check: %s", err) + } + resp.Body = io.NopCloser(bytes.NewBuffer(respBody)) + } + return resp, err } diff --git a/federation/server_test.go b/federation/server_test.go index 3db645fb..33561937 100644 --- a/federation/server_test.go +++ b/federation/server_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/matrix-org/complement/config" + "github.com/matrix-org/complement/internal" ) type fedDeploy struct { @@ -63,10 +64,10 @@ func TestComplementServerIsSigned(t *testing.T) { return // wanted failure, got failure } } + defer internal.CloseIO(resp.Body, "server response body") if !tc.wantSuccess { t.Fatalf("request succeeded when we expected it to fail") } - defer resp.Body.Close() if resp.StatusCode != 404 { t.Errorf("expected 404, got %d", resp.StatusCode) diff --git a/internal/docker/deployer.go b/internal/docker/deployer.go index 6ce9dc33..0a8a511a 100644 --- a/internal/docker/deployer.go +++ b/internal/docker/deployer.go @@ -31,6 +31,7 @@ import ( "time" "github.com/docker/docker/client" + "github.com/matrix-org/complement/internal" complementRuntime "github.com/matrix-org/complement/runtime" "github.com/docker/docker/api/types/container" @@ -668,6 +669,7 @@ func waitForContainer(ctx context.Context, docker *client.Client, hsDep *Homeser time.Sleep(50 * time.Millisecond) continue } + defer internal.CloseIO(res.Body, "waitForContainer: version response body") if res.StatusCode != 200 { lastErr = fmt.Errorf("GET %s => HTTP %s", versionsURL, res.Status) time.Sleep(50 * time.Millisecond) diff --git a/internal/instruction/runner.go b/internal/instruction/runner.go index 16db45f5..d2f14214 100644 --- a/internal/instruction/runner.go +++ b/internal/instruction/runner.go @@ -18,6 +18,7 @@ import ( "github.com/tidwall/gjson" "github.com/matrix-org/complement/b" + "github.com/matrix-org/complement/internal" ) // An instruction for the runner to run. @@ -212,6 +213,15 @@ func (r *Runner) runInstructionSet(contextStr string, hsURL string, instrs []ins return err } } + defer internal.CloseIO( + res.Body, + fmt.Sprintf( + "runInstructionSet: response body from %s %s", + res.Request.Method, + res.Request.URL.String(), + ), + ) + // parse the response if we have one (if bestEffort=true then we don't return an error above) if res != nil && res.Body != nil { if i < 100 || i%200 == 0 { diff --git a/internal/io.go b/internal/io.go new file mode 100644 index 00000000..fac5f02d --- /dev/null +++ b/internal/io.go @@ -0,0 +1,43 @@ +package internal + +import ( + "io" + "log" +) + +// CloseIO is a little helper to close an io.Closer and log any error encountered. +// +// Based off of https://blevesearch.com/news/Deferred-Cleanup,-Checking-Errors,-and-Potential-Problems/ +// +// Probably, most relevant for closing HTTP response bodies as they MUST be closed, even +// if you don’t read it. https://manishrjain.com/must-close-golang-http-response +// +// Usage: +// ```go +// res, err := client.Do(req) +// defer internal.CloseIO(res.Body, "request body") +// ``` +// +// Alternative to this bulky pattern: +// +// ```go +// res, err := client.Do(req) +// defer func(c io.Closer) { +// if c != nil { +// err := c.Close() +// if err != nil { +// log.Fatalf("error closing request body stream %v", err) +// } +// } +// }(res.Body) +// ``` +func CloseIO(c io.Closer, contextString string) { + if c != nil { + err := c.Close() + if err != nil { + // In most cases, not much we can do besides logging as we already received and + // handled whatever resource this io.Closer was wrapping. + log.Fatalf("error closing io.Closer (%s): %v", contextString, err) + } + } +} diff --git a/tests/csapi/apidoc_room_members_test.go b/tests/csapi/apidoc_room_members_test.go index 41c15ef8..adc2e094 100644 --- a/tests/csapi/apidoc_room_members_test.go +++ b/tests/csapi/apidoc_room_members_test.go @@ -79,16 +79,7 @@ func TestRoomMembers(t *testing.T) { }, }) - bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas( - roomID, - func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != bob.UserID { - return false - } - must.Equal(t, ev.Get("content").Get("membership").Str, "join", "Bob failed to join the room") - return true - }, - )) + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) }) // sytest: Test that we can be reinvited to a room we created t.Run("Test that we can be reinvited to a room we created", func(t *testing.T) { @@ -122,14 +113,7 @@ func TestRoomMembers(t *testing.T) { alice.MustLeaveRoom(t, roomID) // Wait until alice has left the room - bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas( - roomID, - func(ev gjson.Result) bool { - return ev.Get("type").Str == "m.room.member" && - ev.Get("content.membership").Str == "leave" && - ev.Get("state_key").Str == alice.UserID - }, - )) + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(alice.UserID, roomID)) bob.MustInviteRoom(t, roomID, alice.UserID) since := alice.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(alice.UserID, roomID)) @@ -203,12 +187,7 @@ func TestRoomMembers(t *testing.T) { }) res := alice.Do(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "ban"}, banBody) must.MatchResponse(t, res, match.HTTPResponse{StatusCode: 200}) - alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != bob.UserID { - return false - } - return ev.Get("content.membership").Str == "ban" - })) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncBannedFrom(bob.UserID, roomID)) // verify bob is banned content := alice.MustGetStateEventContent(t, roomID, "m.room.member", bob.UserID) must.MatchGJSON(t, content, match.JSONKeyEqual("membership", "ban")) diff --git a/tests/csapi/public_rooms_test.go b/tests/csapi/public_rooms_test.go index 5aa7b406..c48b68e5 100644 --- a/tests/csapi/public_rooms_test.go +++ b/tests/csapi/public_rooms_test.go @@ -1,9 +1,9 @@ package csapi_tests import ( + "fmt" "net/http" "testing" - "time" "github.com/tidwall/gjson" @@ -12,64 +12,230 @@ import ( "github.com/matrix-org/complement/helpers" "github.com/matrix-org/complement/match" "github.com/matrix-org/complement/must" + "github.com/matrix-org/complement/should" ) func TestPublicRooms(t *testing.T) { deployment := complement.Deploy(t, 1) defer deployment.Destroy(t) + hs1ServerName := deployment.GetFullyQualifiedHomeserverName(t, "hs1") - t.Run("parallel", func(t *testing.T) { - // sytest: Can search public room list - t.Run("Can search public room list", func(t *testing.T) { - t.Parallel() - authedClient := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + // sytest: Can search public room list + t.Run("Can search public room list", func(t *testing.T) { + authedClient := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) - roomID := authedClient.MustCreateRoom(t, map[string]any{ - "visibility": "public", - "name": "Test Name", - "topic": "Test Topic Wombles", - }) + roomID := authedClient.MustCreateRoom(t, map[string]any{ + "visibility": "public", + "name": "Test Name", + "topic": "Test Topic Wombles", + }) + + // Remove the room from the public rooms list to avoid polluting other tests. + defer authedClient.MustDo( + t, + "PUT", + []string{"_matrix", "client", "v3", "directory", "list", "room", roomID}, + client.WithJSONBody(t, map[string]interface{}{ + "visibility": "private", + }), + ) + + authedClient.MustDo( + t, + "POST", + []string{"_matrix", "client", "v3", "publicRooms"}, + client.WithJSONBody(t, map[string]any{ + "filter": map[string]any{ + "generic_search_term": "wombles", + }, + }), + client.WithRetryUntil(authedClient.SyncUntilTimeout, func(res *http.Response) bool { + results := parsePublicRoomsResponse(t, res) + + if len(results) != 1 { + t.Logf("expected a single search result, got %d", len(results)) + return false + } + + foundRoomID := results[0].Get("room_id").Str + if foundRoomID != roomID { + t.Logf("expected room_id %s in search results, got %s", roomID, foundRoomID) + return false + } + + return true + }), + ) + }) + + // sytest: Name/topic keys are correct + t.Run("Name/topic keys are correct", func(t *testing.T) { + authedClient := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + + // Define room configurations + roomConfigs := []struct { + alias string + name string + topic string + }{ + {"publicroomalias_no_name", "", ""}, + {"publicroomalias_with_name", "name_1", ""}, + {"publicroomalias_with_topic", "", "topic_1"}, + {"publicroomalias_with_name_topic", "name_2", "topic_2"}, + {"publicroom_with_unicode_chars_name", "un nom français", ""}, + {"publicroom_with_unicode_chars_topic", "", "un topic à la française"}, + {"publicroom_with_unicode_chars_name_topic", "un nom français", "un topic à la française"}, + } + + for _, roomConfig := range roomConfigs { + t.Run(fmt.Sprintf("Creating room with alias %s", roomConfig.alias), func(t *testing.T) { + expectedCanonicalAlias := fmt.Sprintf("#%s:%s", roomConfig.alias, hs1ServerName) + + // Create the room + roomOptions := map[string]interface{}{ + // Add the room to the public rooms list. + "visibility": "public", + "room_alias_name": roomConfig.alias, + } + + if roomConfig.name != "" { + roomOptions["name"] = roomConfig.name + } + if roomConfig.topic != "" { + roomOptions["topic"] = roomConfig.topic + } + + roomID := authedClient.MustCreateRoom(t, roomOptions) + t.Logf("Created room %s with alias %s", roomID, roomConfig.alias) + + // Remove the room from the public rooms list to avoid polluting other tests. + defer authedClient.MustDo( + t, + "PUT", + []string{"_matrix", "client", "v3", "directory", "list", "room", roomID}, + client.WithJSONBody(t, map[string]interface{}{ + "visibility": "private", + }), + ) + + // Poll /publicRooms until the room appears with the correct data + + // Keep track of any rooms that we didn't expect to see. + unexpectedRooms := make([]string, 0) + + var discoveredRoomData gjson.Result + authedClient.MustDo(t, "GET", []string{"_matrix", "client", "v3", "publicRooms"}, + client.WithRetryUntil(authedClient.SyncUntilTimeout, func(res *http.Response) bool { + results := parsePublicRoomsResponse(t, res) - authedClient.MustDo( - t, - "POST", - []string{"_matrix", "client", "v3", "publicRooms"}, - client.WithJSONBody(t, map[string]any{ - "filter": map[string]any{ - "generic_search_term": "wombles", - }, - }), - client.WithRetryUntil(15*time.Second, func(res *http.Response) bool { - body := must.ParseJSON(t, res.Body) - - must.MatchGJSON( - t, - body, - match.JSONKeyPresent("chunk"), - match.JSONKeyTypeEqual("chunk", gjson.JSON), - ) - - chunk := body.Get("chunk") - if !chunk.IsArray() { - t.Logf("chunk is not an array") - return false + // Check each room in the public rooms list + for _, roomData := range results { + discoveredRoomID := roomData.Get("room_id").Str + if discoveredRoomID != roomID { + // Not our room, skip. + unexpectedRooms = append(unexpectedRooms, discoveredRoomID) + continue + } + + // We found our room. Stop calling /publicRooms and validate the response. + discoveredRoomData = roomData + } + + if !discoveredRoomData.Exists() { + t.Logf("Room %s not found in public rooms list, trying again...", roomID) + return false + } + + return true + }), + ) + + if len(unexpectedRooms) > 0 { + t.Logf("Warning: Found unexpected rooms in public rooms list: %v", unexpectedRooms) + } + + // Verify required keys are present in the room data. + err := should.MatchGJSON( + discoveredRoomData, + match.JSONKeyPresent("world_readable"), + match.JSONKeyPresent("guest_can_join"), + match.JSONKeyPresent("num_joined_members"), + ) + if err != nil { + // The room is missing required keys, and + // it's unlikely to get them after + // calling the method again. Let's bail out. + t.Fatalf("Room %s data missing required keys: %s, data: %v", roomID, err.Error(), discoveredRoomData) + } + + // Keep track of all validation errors, rather than bailing out on the first one. + validationErrors := make([]error, 0) + + // Verify canonical alias + canonicalAlias := discoveredRoomData.Get("canonical_alias").Str + if canonicalAlias != expectedCanonicalAlias { + err = fmt.Errorf("Room %s has canonical alias '%s', expected '%s'", roomID, canonicalAlias, expectedCanonicalAlias) + validationErrors = append(validationErrors, err) + } + + // Verify member count + numMembers := discoveredRoomData.Get("num_joined_members").Int() + if numMembers != 1 { + err = fmt.Errorf("Room %s has %d members, expected 1", roomID, numMembers) + validationErrors = append(validationErrors, err) + } + + // Verify name field, if there is one to verify + name := discoveredRoomData.Get("name").Str + if roomConfig.name != "" { + if name != roomConfig.name { + err = fmt.Errorf("Room %s has name '%s', expected '%s'", roomID, name, roomConfig.name) + validationErrors = append(validationErrors, err) } + } else { + if name != "" { + err = fmt.Errorf("Room %s has unexpected name '%s', expected no name", roomID, name) + validationErrors = append(validationErrors, err) + } + } - results := chunk.Array() - if len(results) != 1 { - t.Logf("expected a single search result, got %d", len(results)) - return false + // Verify topic field, if there is one to verify + topic := discoveredRoomData.Get("topic").Str + if roomConfig.topic != "" { + if topic != roomConfig.topic { + err = fmt.Errorf("Room %s has topic '%s', expected '%s'", roomID, topic, roomConfig.topic) + validationErrors = append(validationErrors, err) + } + } else { + if topic != "" { + err = fmt.Errorf("Room %s has unexpected topic '%s', expected no topic", roomID, topic) + validationErrors = append(validationErrors, err) } + } - foundRoomID := results[0].Get("room_id").Str - if foundRoomID != roomID { - t.Logf("expected room_id %s in search results, got %s", roomID, foundRoomID) - return false + if len(validationErrors) > 0 { + for _, e := range validationErrors { + t.Errorf("Validation error for room %s: %s", roomID, e.Error()) } - return true - }), - ) - }) + t.Fail() + } + }) + } }) } + +func parsePublicRoomsResponse(t *testing.T, res *http.Response) []gjson.Result { + t.Helper() + body := must.ParseJSON(t, res.Body) + + chunk := body.Get("chunk") + if !chunk.Exists() { + t.Fatalf("`chunk` field on public rooms response does not exist, got body: %v", body) + } + if !chunk.IsArray() { + t.Fatalf("`chunk` field on public rooms response is not an array, got: %v", chunk) + } + + return chunk.Array() +} diff --git a/tests/csapi/rooms_state_test.go b/tests/csapi/rooms_state_test.go index 05a482e9..2158cae1 100644 --- a/tests/csapi/rooms_state_test.go +++ b/tests/csapi/rooms_state_test.go @@ -13,6 +13,7 @@ import ( "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" ) @@ -46,13 +47,10 @@ func TestRoomCreationReportsEventsToMyself(t *testing.T) { t.Run("Room creation reports m.room.member to myself", func(t *testing.T) { t.Parallel() - alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" { - return false - } - must.Equal(t, ev.Get("sender").Str, alice.UserID, "wrong sender") - must.Equal(t, ev.Get("state_key").Str, alice.UserID, "wrong state_key") - must.Equal(t, ev.Get("content").Get("membership").Str, "join", "wrong content.membership") + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID, func(ev gjson.Result) bool { + must.MatchGJSON(t, ev, + match.JSONKeyEqual("sender", alice.UserID), + ) return true })) }) diff --git a/tests/csapi/sync_test.go b/tests/csapi/sync_test.go index 2399cfab..3dec9ea5 100644 --- a/tests/csapi/sync_test.go +++ b/tests/csapi/sync_test.go @@ -3,6 +3,7 @@ package csapi_tests import ( "encoding/json" "fmt" + "strconv" "testing" "time" @@ -491,7 +492,7 @@ func TestSyncTimelineGap(t *testing.T) { Type: "m.room.message", Sender: charlie, Content: map[string]interface{}{ - "body": "Remote message", + "body": "Remote message " + strconv.Itoa(i), "msgtype": "m.text", }, }) diff --git a/tests/federation_keys_test.go b/tests/federation_keys_test.go index 7907b191..316927a5 100644 --- a/tests/federation_keys_test.go +++ b/tests/federation_keys_test.go @@ -13,6 +13,7 @@ import ( "github.com/tidwall/sjson" "github.com/matrix-org/complement" + "github.com/matrix-org/complement/internal" "github.com/matrix-org/complement/match" "github.com/matrix-org/complement/must" ) @@ -36,6 +37,7 @@ func TestInboundFederationKeys(t *testing.T) { res, err := fedClient.Get("https://hs1/_matrix/key/v2/server") must.NotError(t, "failed to GET /keys", err) + defer internal.CloseIO(res.Body, "server key response body") var keys = map[string]ed25519.PublicKey{} var oldKeys = map[string]ed25519.PublicKey{} diff --git a/tests/federation_room_ban_test.go b/tests/federation_room_ban_test.go index 9a3ff479..29d3d53c 100644 --- a/tests/federation_room_ban_test.go +++ b/tests/federation_room_ban_test.go @@ -30,7 +30,7 @@ func TestUnbanViaInvite(t *testing.T) { bob.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "ban"}, client.WithJSONBody(t, map[string]interface{}{ "user_id": alice.UserID, })) - alice.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(alice.UserID, roomID)) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncBannedFrom(alice.UserID, roomID)) // Unban Alice bob.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "unban"}, client.WithJSONBody(t, map[string]interface{}{ diff --git a/tests/federation_room_event_auth_test.go b/tests/federation_room_event_auth_test.go index 245f73ef..bb08d073 100644 --- a/tests/federation_room_event_auth_test.go +++ b/tests/federation_room_event_auth_test.go @@ -181,7 +181,7 @@ func TestInboundFederationRejectsEventsWithRejectedAuthEvents(t *testing.T) { sentEvent2 := srv.MustCreateEvent(t, room, federation.Event{ Type: "m.room.message", Sender: charlie, - Content: map[string]interface{}{"body": "sentEvent1"}, + Content: map[string]interface{}{"body": "sentEvent2"}, AuthEvents: room.EventIDsOrReferences(sentEventAuthEvents), }) room.AddEvent(sentEvent2) diff --git a/tests/federation_rooms_invite_test.go b/tests/federation_rooms_invite_test.go index c5200d8d..d5386014 100644 --- a/tests/federation_rooms_invite_test.go +++ b/tests/federation_rooms_invite_test.go @@ -227,22 +227,15 @@ func TestFederationRoomsInvite(t *testing.T) { "is_direct": true, }) bob.MustJoinRoom(t, roomID, []spec.ServerName{}) - bob.MustSyncUntil(t, client.SyncReq{}, - client.SyncTimelineHas(roomID, func(result gjson.Result) bool { - // We expect a membership event .. - if result.Get("type").Str != spec.MRoomMember { - return false - } - // .. for Bob - if result.Get("state_key").Str != bob.UserID { - return false - } - // Check that we've got tbe expected is_idrect flag - return result.Get("unsigned.prev_content.membership").Str == "invite" && - result.Get("unsigned.prev_content.is_direct").Bool() == true && - result.Get("unsigned.prev_sender").Str == alice.UserID - }), - ) + + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID, func(ev gjson.Result) bool { + must.MatchGJSON(t, ev, + match.JSONKeyEqual("unsigned.prev_content.membership", "invite"), + match.JSONKeyEqual("unsigned.prev_content.is_direct", true), + match.JSONKeyEqual("unsigned.prev_sender", alice.UserID), + ) + return true + })) }) }) } diff --git a/tests/knocking_test.go b/tests/knocking_test.go index 7c1bc98b..7eb625cf 100644 --- a/tests/knocking_test.go +++ b/tests/knocking_test.go @@ -163,12 +163,10 @@ func knockingBetweenTwoUsersTest( } } - inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" || ev.Get("sender").Str != knockingUser.UserID { - return false - } - must.Equal(t, ev.Get("content").Get("reason").Str, testKnockReason, "incorrect reason for knock") - must.Equal(t, ev.Get("content").Get("membership").Str, "knock", "incorrect membership for knocking user") + inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncKnockedOn(knockingUser.UserID, roomID, func(ev gjson.Result) bool { + must.MatchGJSON(t, ev, + match.JSONKeyEqual("content.reason", testKnockReason), + ) return true })) }) @@ -224,11 +222,7 @@ func knockingBetweenTwoUsersTest( ) // Wait until the leave membership event has come down sync - inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool { - return ev.Get("type").Str != "m.room.member" || - ev.Get("state_key").Str != knockingUser.UserID || - ev.Get("content").Get("membership").Str != "leave" - })) + inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(knockingUser.UserID, roomID)) // Knock again mustKnockOnRoomSynced(t, knockingUser, roomID, "Pleeease let me in?", []spec.ServerName{ @@ -258,11 +252,7 @@ func knockingBetweenTwoUsersTest( inRoomUser.MustInviteRoom(t, roomID, knockingUser.UserID) // Wait until the invite membership event has come down sync - inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool { - return ev.Get("type").Str != "m.room.member" || - ev.Get("state_key").Str != knockingUser.UserID || - ev.Get("content").Get("membership").Str != "invite" - })) + inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(knockingUser.UserID, roomID)) }) t.Run("A user cannot knock on a room they are already invited to", func(t *testing.T) { @@ -299,11 +289,7 @@ func knockingBetweenTwoUsersTest( ) // Wait until the ban membership event has come down sync - inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool { - return ev.Get("type").Str != "m.room.member" || - ev.Get("state_key").Str != knockingUser.UserID || - ev.Get("content").Get("membership").Str != "ban" - })) + inRoomUser.MustSyncUntil(t, client.SyncReq{}, client.SyncBannedFrom(knockingUser.UserID, roomID)) knockOnRoomWithStatus(t, knockingUser, roomID, "I didn't mean it!", []spec.ServerName{ deployment.GetFullyQualifiedHomeserverName(t, "hs1"), @@ -311,28 +297,6 @@ func knockingBetweenTwoUsersTest( }) } -func syncKnockedOn(userID, roomID string) client.SyncCheckOpt { - return func(clientUserID string, topLevelSyncJSON gjson.Result) error { - // two forms which depend on what the client user is: - // - passively viewing a membership for a room you're joined in - // - actively leaving the room - if clientUserID == userID { - events := topLevelSyncJSON.Get("rooms.knock." + client.GjsonEscape(roomID) + ".knock_state.events") - if events.Exists() && events.IsArray() { - // We don't currently define any required state event types to be sent. - // If we've reached this point, then an entry for this room was found - return nil - } - return fmt.Errorf("no knock section for room %s", roomID) - } - - // passive - return client.SyncTimelineHas(roomID, func(ev gjson.Result) bool { - return ev.Get("type").Str == "m.room.member" && ev.Get("state_key").Str == userID && ev.Get("content.membership").Str == "knock" - })(clientUserID, topLevelSyncJSON) - } -} - // mustKnockOnRoomSynced will knock on a given room on the behalf of a user, and block until the knock has persisted. // serverNames should be populated if knocking on a room that the user's homeserver isn't currently a part of. // Fails the test if the knock response does not return a 200 status code. @@ -344,7 +308,7 @@ func mustKnockOnRoomSynced(t *testing.T, c *client.CSAPI, roomID, reason string, knockOnRoomWithStatus(t, c, roomID, reason, serverNames, 200) // The knock should have succeeded. Block until we see the knock appear down sync - c.MustSyncUntil(t, client.SyncReq{}, syncKnockedOn(c.UserID, roomID)) + c.MustSyncUntil(t, client.SyncReq{}, client.SyncKnockedOn(c.UserID, roomID)) } // knockOnRoomWithStatus will knock on a given room on the behalf of a user. diff --git a/tests/msc3902/federation_room_join_partial_state_test.go b/tests/msc3902/federation_room_join_partial_state_test.go index 48b86814..9e11b163 100644 --- a/tests/msc3902/federation_room_join_partial_state_test.go +++ b/tests/msc3902/federation_room_join_partial_state_test.go @@ -3963,9 +3963,7 @@ func TestPartialStateJoin(t *testing.T) { aliceNextBatch = alice.MustSyncUntil( t, client.SyncReq{Since: aliceNextBatch, Filter: buildLazyLoadingSyncFilter(nil)}, - // TODO: introduce a SyncBannedFrom which checks the membership of the - // leave event - client.SyncLeftFrom(alice.UserID, serverRoom.RoomID), + client.SyncBannedFrom(alice.UserID, serverRoom.RoomID), ) t.Log("Alice tries to rejoin...") diff --git a/tests/msc4140/delayed_event_test.go b/tests/msc4140/delayed_event_test.go index 759e36bf..8def039e 100644 --- a/tests/msc4140/delayed_event_test.go +++ b/tests/msc4140/delayed_event_test.go @@ -20,6 +20,14 @@ import ( const hsName = "hs1" const eventType = "com.example.test" +type DelayedEventAction string + +const ( + DelayedEventActionCancel = "cancel" + DelayedEventActionRestart = "restart" + DelayedEventActionSend = "send" +) + // TODO: Test pagination of `GET /_matrix/client/v1/delayed_events` once // it is implemented in a homeserver. @@ -29,6 +37,7 @@ func TestDelayedEvents(t *testing.T) { user := deployment.Register(t, hsName, helpers.RegistrationOpts{}) user2 := deployment.Register(t, hsName, helpers.RegistrationOpts{}) + unauthedClient := deployment.UnauthenticatedClient(t, hsName) roomID := user.MustCreateRoom(t, map[string]interface{}{ "preset": "public_chat", @@ -44,6 +53,13 @@ func TestDelayedEvents(t *testing.T) { matchDelayedEvents(t, user, 0) }) + t.Run("delayed event lookups are authenticated", func(t *testing.T) { + res := unauthedClient.Do(t, "GET", getPathForDelayedEvents()) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: 401, + }) + }) + t.Run("delayed message events are sent on timeout", func(t *testing.T) { var res *http.Response var countExpected uint64 @@ -165,66 +181,42 @@ func TestDelayedEvents(t *testing.T) { }) }) - t.Run("cannot update a delayed event without a delay ID", func(t *testing.T) { - res := user.Do(t, "POST", append(getPathForUpdateDelayedEvents(), "")) - must.MatchResponse(t, res, match.HTTPResponse{ - StatusCode: 404, - }) - }) - - t.Run("cannot update a delayed event without a request body", func(t *testing.T) { - res := user.Do(t, "POST", append(getPathForUpdateDelayedEvents(), "abc")) - must.MatchResponse(t, res, match.HTTPResponse{ - StatusCode: 400, - JSON: []match.JSON{ - match.JSONKeyEqual("errcode", "M_NOT_JSON"), - }, - }) - }) - t.Run("cannot update a delayed event without an action", func(t *testing.T) { - res := user.Do( + res := unauthedClient.Do( t, "POST", - append(getPathForUpdateDelayedEvents(), "abc"), + append(getPathForDelayedEvents(), "abc"), client.WithJSONBody(t, map[string]interface{}{}), ) - must.MatchResponse(t, res, match.HTTPResponse{ - StatusCode: 400, - JSON: []match.JSON{ - match.JSONKeyEqual("errcode", "M_MISSING_PARAM"), - }, - }) + // TODO: specify failure as 404 when/if Synapse removes the action-in-body version of this endpoint + must.MatchFailure(t, res) }) t.Run("cannot update a delayed event with an invalid action", func(t *testing.T) { - res := user.Do( + res := unauthedClient.Do( t, "POST", - append(getPathForUpdateDelayedEvents(), "abc"), - client.WithJSONBody(t, map[string]interface{}{ - "action": "oops", - }), + append(getPathForDelayedEvents(), "abc", "oops"), + client.WithJSONBody(t, map[string]interface{}{}), ) must.MatchResponse(t, res, match.HTTPResponse{ - StatusCode: 400, - JSON: []match.JSON{ - match.JSONKeyEqual("errcode", "M_INVALID_PARAM"), - }, + StatusCode: 404, }) }) t.Run("parallel", func(t *testing.T) { - for _, action := range []string{"cancel", "restart", "send"} { + for _, action := range []DelayedEventAction{ + DelayedEventActionCancel, + DelayedEventActionRestart, + DelayedEventActionSend, + } { t.Run(fmt.Sprintf("cannot %s a delayed event without a matching delay ID", action), func(t *testing.T) { t.Parallel() - res := user.Do( + res := unauthedClient.Do( t, "POST", - append(getPathForUpdateDelayedEvents(), "abc"), - client.WithJSONBody(t, map[string]interface{}{ - "action": action, - }), + getPathForUpdateDelayedEvent("abc", action), + client.WithJSONBody(t, map[string]interface{}{}), ) must.MatchResponse(t, res, match.HTTPResponse{ StatusCode: 404, @@ -258,13 +250,11 @@ func TestDelayedEvents(t *testing.T) { StatusCode: 404, }) - user.MustDo( + unauthedClient.MustDo( t, "POST", - append(getPathForUpdateDelayedEvents(), delayID), - client.WithJSONBody(t, map[string]interface{}{ - "action": "cancel", - }), + getPathForUpdateDelayedEvent(delayID, DelayedEventActionCancel), + client.WithJSONBody(t, map[string]interface{}{}), ) matchDelayedEvents(t, user, 0) @@ -302,13 +292,11 @@ func TestDelayedEvents(t *testing.T) { StatusCode: 404, }) - user.MustDo( + unauthedClient.MustDo( t, "POST", - append(getPathForUpdateDelayedEvents(), delayID), - client.WithJSONBody(t, map[string]interface{}{ - "action": "send", - }), + getPathForUpdateDelayedEvent(delayID, DelayedEventActionSend), + client.WithJSONBody(t, map[string]interface{}{}), ) matchDelayedEvents(t, user, 0) res = user.Do(t, "GET", getPathForState(roomID, eventType, stateKey)) @@ -346,13 +334,11 @@ func TestDelayedEvents(t *testing.T) { StatusCode: 404, }) - user.MustDo( + unauthedClient.MustDo( t, "POST", - append(getPathForUpdateDelayedEvents(), delayID), - client.WithJSONBody(t, map[string]interface{}{ - "action": "restart", - }), + getPathForUpdateDelayedEvent(delayID, DelayedEventActionRestart), + client.WithJSONBody(t, map[string]interface{}{}), ) time.Sleep(1 * time.Second) @@ -489,10 +475,14 @@ func TestDelayedEvents(t *testing.T) { }) } -func getPathForUpdateDelayedEvents() []string { +func getPathForDelayedEvents() []string { return []string{"_matrix", "client", "unstable", "org.matrix.msc4140", "delayed_events"} } +func getPathForUpdateDelayedEvent(delayId string, action DelayedEventAction) []string { + return append(getPathForDelayedEvents(), delayId, string(action)) +} + func getPathForSend(roomID string, eventType string, txnId string) []string { return []string{"_matrix", "client", "v3", "rooms", roomID, "send", eventType, txnId} } @@ -509,7 +499,7 @@ func getDelayQueryParam(delayStr string) client.RequestOpt { func getDelayedEvents(t *testing.T, user *client.CSAPI) *http.Response { t.Helper() - return user.MustDo(t, "GET", getPathForUpdateDelayedEvents()) + return user.MustDo(t, "GET", getPathForDelayedEvents()) } // Checks if the number of delayed events match the given number. This will @@ -518,7 +508,7 @@ func matchDelayedEvents(t *testing.T, user *client.CSAPI, wantNumber int) { t.Helper() // We need to retry this as replication can sometimes lag. - user.MustDo(t, "GET", getPathForUpdateDelayedEvents(), + user.MustDo(t, "GET", getPathForDelayedEvents(), client.WithRetryUntil( 500*time.Millisecond, func(res *http.Response) bool { @@ -548,10 +538,8 @@ func cleanupDelayedEvents(t *testing.T, user *client.CSAPI) { user.MustDo( t, "POST", - append(getPathForUpdateDelayedEvents(), delayID), - client.WithJSONBody(t, map[string]interface{}{ - "action": "cancel", - }), + getPathForUpdateDelayedEvent(delayID, DelayedEventActionCancel), + client.WithJSONBody(t, map[string]interface{}{}), ) } diff --git a/tests/restricted_rooms_test.go b/tests/restricted_rooms_test.go index 1fd28588..8bb8406d 100644 --- a/tests/restricted_rooms_test.go +++ b/tests/restricted_rooms_test.go @@ -121,14 +121,7 @@ func checkRestrictedRoom(t *testing.T, deployment complement.Deployment, alice * // Wait until Alice sees Bob leave the allowed room. This ensures that Alice's HS // has processed the leave before Bob tries rejoining, so that it rejects his // attempt to join the room. - alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas( - allowed_room, func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" || ev.Get("sender").Str != bob.UserID { - return false - } - - return ev.Get("content").Get("membership").Str == "leave" - })) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(bob.UserID, allowed_room)) res := bob.JoinRoom(t, room, []spec.ServerName{ deployment.GetFullyQualifiedHomeserverName(t, "hs1"), @@ -297,18 +290,7 @@ func doTestRestrictedRoomsRemoteJoinLocalUser(t *testing.T, roomVersion string, // Ensure that the join comes down sync on hs2. Note that we want to ensure hs2 // accepted the event. - charlie.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas( - room, - func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != bob.UserID { - return false - } - must.Equal(t, ev.Get("sender").Str, bob.UserID, "Bob should have joined by himself") - must.Equal(t, ev.Get("content").Get("membership").Str, "join", "Bob failed to join the room") - - return true - }, - )) + charlie.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, room)) // Raise the power level so that users on hs1 can invite people and then leave // the room. @@ -407,18 +389,12 @@ func doTestRestrictedRoomsRemoteJoinFailOver(t *testing.T, roomVersion string, j }) // Double check that the join was authorised via hs1. - bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas( - room, - func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID { - return false - } - must.Equal(t, ev.Get("content").Get("membership").Str, "join", "Charlie failed to join the room") - must.Equal(t, ev.Get("content").Get("join_authorised_via_users_server").Str, alice.UserID, "Join authorised via incorrect server") - - return true - }, - )) + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(charlie.UserID, room, func(ev gjson.Result) bool { + must.MatchGJSON(t, ev, + match.JSONKeyEqual("content.join_authorised_via_users_server", alice.UserID), + ) + return true + })) // Bump the power-level of bob. t.Logf("%s allows %s to send invites.", alice.UserID, bob.UserID) @@ -463,17 +439,10 @@ func doTestRestrictedRoomsRemoteJoinFailOver(t *testing.T, roomVersion string, j }) // Double check that the join was authorised via hs1. - bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas( - room, - func(ev gjson.Result) bool { - if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID { - return false - } - must.MatchGJSON(t, ev, - match.JSONKeyEqual("content.membership", "join"), - match.JSONKeyEqual("content.join_authorised_via_users_server", alice.UserID), - ) - return true - }, - )) + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(charlie.UserID, room, func(ev gjson.Result) bool { + must.MatchGJSON(t, ev, + match.JSONKeyEqual("content.join_authorised_via_users_server", alice.UserID), + ) + return true + })) }