diff --git a/.gitignore b/.gitignore index c56b22a2..9a18f381 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ complement # For direnv users /.envrc .direnv/ + +# devenv users +.devenv/ diff --git a/client/client.go b/client/client.go index 261924de..a6d6ceab 100644 --- a/client/client.go +++ b/client/client.go @@ -15,6 +15,7 @@ import ( "net/url" "strconv" "strings" + "sync" "sync/atomic" "time" @@ -48,6 +49,19 @@ type retryUntilParams struct { // See functions starting with `With...` in this package for more info. type RequestOpt func(req *http.Request) +type CSAPIOpts struct { + UserID string + AccessToken string + DeviceID string + Password string // if provided + BaseURL string + Client *http.Client + // how long are we willing to wait for MustSyncUntil.... calls + SyncUntilTimeout time.Duration + // True to enable verbose logging + Debug bool +} + type CSAPI struct { UserID string AccessToken string @@ -60,7 +74,22 @@ type CSAPI struct { // True to enable verbose logging Debug bool - txnID int64 + txnID int64 + createRoomMutex *sync.Mutex +} + +func NewCSAPI(opts CSAPIOpts) *CSAPI { + return &CSAPI{ + UserID: opts.UserID, + AccessToken: opts.AccessToken, + DeviceID: opts.DeviceID, + Password: opts.Password, + BaseURL: opts.BaseURL, + Client: opts.Client, + SyncUntilTimeout: opts.SyncUntilTimeout, + Debug: opts.Debug, + createRoomMutex: &sync.Mutex{}, + } } // CreateMedia creates an MXC URI for asynchronous media uploads. @@ -172,6 +201,10 @@ func (c *CSAPI) MustCreateRoom(t ct.TestLike, reqBody map[string]interface{}) st // CreateRoom creates a room with an optional HTTP request body. func (c *CSAPI) CreateRoom(t ct.TestLike, body map[string]interface{}) *http.Response { t.Helper() + // Ensure we don't call /createRoom from the same user in parallel, else we might try to make + // 2 rooms in the same millisecond (same `origin_server_ts`), causing v12 rooms to get the same room ID thus failing the test. + c.createRoomMutex.Lock() + defer c.createRoomMutex.Unlock() return c.Do(t, "POST", []string{"_matrix", "client", "v3", "createRoom"}, WithJSONBody(t, body)) } diff --git a/internal/docker/deployment.go b/internal/docker/deployment.go index 029b3f56..a3f3313d 100644 --- a/internal/docker/deployment.go +++ b/internal/docker/deployment.go @@ -105,13 +105,13 @@ func (d *Deployment) Register(t ct.TestLike, hsName string, opts helpers.Registr ct.Fatalf(t, "Deployment.Register - HS name '%s' not found", hsName) return nil } - client := &client.CSAPI{ + client := client.NewCSAPI(client.CSAPIOpts{ BaseURL: dep.BaseURL, Client: client.NewLoggedClient(t, hsName, nil), SyncUntilTimeout: 5 * time.Second, Debug: d.Deployer.debugLogging, Password: opts.Password, - } + }) // Appending a slice is not thread-safe. Protect the write with a mutex. dep.CSAPIClientsMutex.Lock() dep.CSAPIClients = append(dep.CSAPIClients, client) @@ -155,13 +155,13 @@ func (d *Deployment) Login(t ct.TestLike, hsName string, existing *client.CSAPI, if err != nil { ct.Fatalf(t, "Deployment.Login: existing CSAPI client has invalid user ID '%s', cannot login as this user: %s", existing.UserID, err) } - c := &client.CSAPI{ + c := client.NewCSAPI(client.CSAPIOpts{ BaseURL: dep.BaseURL, Client: client.NewLoggedClient(t, hsName, nil), SyncUntilTimeout: 5 * time.Second, Debug: d.Deployer.debugLogging, Password: existing.Password, - } + }) if opts.Password != "" { c.Password = opts.Password } @@ -197,12 +197,12 @@ func (d *Deployment) UnauthenticatedClient(t ct.TestLike, hsName string) *client ct.Fatalf(t, "Deployment.Client - HS name '%s' not found", hsName) return nil } - client := &client.CSAPI{ + client := client.NewCSAPI(client.CSAPIOpts{ BaseURL: dep.BaseURL, Client: client.NewLoggedClient(t, hsName, nil), SyncUntilTimeout: 5 * time.Second, Debug: d.Deployer.debugLogging, - } + }) // Appending a slice is not thread-safe. Protect the write with a mutex. dep.CSAPIClientsMutex.Lock() dep.CSAPIClients = append(dep.CSAPIClients, client) @@ -230,7 +230,7 @@ func (d *Deployment) AppServiceUser(t ct.TestLike, hsName, appServiceUserID stri if deviceID == "" && appServiceUserID != "" { t.Logf("WARNING: Deployment.Client - HS name '%s' - user ID '%s' - deviceID not found", hsName, appServiceUserID) } - client := &client.CSAPI{ + client := client.NewCSAPI(client.CSAPIOpts{ UserID: appServiceUserID, AccessToken: token, DeviceID: deviceID, @@ -238,7 +238,7 @@ func (d *Deployment) AppServiceUser(t ct.TestLike, hsName, appServiceUserID stri Client: client.NewLoggedClient(t, hsName, nil), SyncUntilTimeout: 5 * time.Second, Debug: d.Deployer.debugLogging, - } + }) // Appending a slice is not thread-safe. Protect the write with a mutex. dep.CSAPIClientsMutex.Lock() dep.CSAPIClients = append(dep.CSAPIClients, client) diff --git a/tests/csapi/apidoc_room_receipts_test.go b/tests/csapi/apidoc_room_receipts_test.go index 30e7fa84..32758020 100644 --- a/tests/csapi/apidoc_room_receipts_test.go +++ b/tests/csapi/apidoc_room_receipts_test.go @@ -17,15 +17,19 @@ func createRoomForReadReceipts(t *testing.T, c *client.CSAPI) (string, string) { c.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(c.UserID, roomID)) - eventID := c.SendEventSynced(t, roomID, b.Event{ + eventID := sendMessageIntoRoom(t, c, roomID) + + return roomID, eventID +} + +func sendMessageIntoRoom(t *testing.T, c *client.CSAPI, roomID string) string { + return c.SendEventSynced(t, roomID, b.Event{ Type: "m.room.message", Content: map[string]interface{}{ "msgtype": "m.text", "body": "Hello world!", }, }) - - return roomID, eventID } func syncHasReadReceipt(roomID, userID, eventID string) client.SyncCheckOpt { @@ -45,7 +49,41 @@ func TestRoomReceipts(t *testing.T) { alice.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", eventID}, client.WithJSONBody(t, struct{}{})) // Make sure the read receipt shows up in sync. - alice.MustSyncUntil(t, client.SyncReq{}, syncHasReadReceipt(roomID, alice.UserID, eventID)) + sinceToken := alice.MustSyncUntil(t, client.SyncReq{}, syncHasReadReceipt(roomID, alice.UserID, eventID)) + + // Receipt events include a `room_id` field over federation, but they should + // not do so down `/sync` to clients. Ensure homeservers strip that field out. + t.Run("Receipts DO NOT include a `room_id` field", func(t *testing.T) { + // Send another event to read. + eventID2 := sendMessageIntoRoom(t, alice, roomID) + + // Send a read receipt for the event. + alice.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", eventID2}, client.WithJSONBody(t, struct{}{})) + + alice.MustSyncUntil( + t, + client.SyncReq{Since: sinceToken}, + client.SyncEphemeralHas(roomID, func(r gjson.Result) bool { + // Check that this is a m.receipt ephemeral event. + if r.Get("type").Str != "m.receipt" { + return false + } + + // Check that the receipt type is "m.read". + if !r.Get(`content.*.m\.read`).Exists() { + t.Fatalf("Receipt was not of type 'm.read'") + } + + // Ensure that the `room_id` field does NOT exist. + if r.Get("room_id").Exists() { + t.Fatalf("Read receipt should not contain 'room_id' field when syncing but saw: %s", r.Raw) + } + + // Exit the /sync loop. + return true; + }), + ) + }) } // sytest: POST /rooms/:room_id/read_markers can create read marker diff --git a/tests/csapi/device_lists_test.go b/tests/csapi/device_lists_test.go index 17340ccb..7012efa7 100644 --- a/tests/csapi/device_lists_test.go +++ b/tests/csapi/device_lists_test.go @@ -49,6 +49,7 @@ func TestDeviceListUpdates(t *testing.T) { ed25519KeyID: ed25519Key, curve25519KeyID: curve25519Key, }, + "signatures": map[string]interface{}{}, }, }), ) diff --git a/tests/csapi/room_typing_test.go b/tests/csapi/room_typing_test.go index 84253cca..7769dd06 100644 --- a/tests/csapi/room_typing_test.go +++ b/tests/csapi/room_typing_test.go @@ -6,6 +6,7 @@ import ( "github.com/matrix-org/complement" "github.com/matrix-org/complement/client" "github.com/matrix-org/complement/helpers" + "github.com/tidwall/gjson" ) // sytest: PUT /rooms/:room_id/typing/:user_id sets typing notification @@ -33,6 +34,30 @@ func TestTyping(t *testing.T) { alice.SendTyping(t, roomID, false, 0) bob.MustSyncUntil(t, client.SyncReq{Since: token}, client.SyncUsersTyping(roomID, []string{})) }) + + // Typing events include a `room_id` field over federation, but they should + // not do so down `/sync` to clients. Ensure homeservers strip that field out. + t.Run("Typing events DO NOT include a `room_id` field", func(t *testing.T) { + alice.SendTyping(t, roomID, true, 0) + + bob.MustSyncUntil( + t, + client.SyncReq{Since: token}, + client.SyncEphemeralHas(roomID, func(r gjson.Result) bool { + if r.Get("type").Str != "m.typing" { + return false + } + + // Ensure that the `room_id` field does NOT exist. + if r.Get("room_id").Exists() { + t.Fatalf("Typing event should not contain `room_id` field when syncing but saw: %s", r.Raw) + } + + // Exit the /sync loop. + return true; + }), + ) + }) } // sytest: Typing notifications don't leak diff --git a/tests/federation_acl_test.go b/tests/federation_acl_test.go index c6112cd5..c7f4dae5 100644 --- a/tests/federation_acl_test.go +++ b/tests/federation_acl_test.go @@ -59,7 +59,9 @@ func TestACLs(t *testing.T) { Content: map[string]interface{}{ "allow": []string{"*"}, "allow_ip_literals": true, - "deny": []string{"hs2"}, + "deny": []string{ + string(deployment.GetFullyQualifiedHomeserverName(t, "hs2")), + }, }, }) // wait for the ACL to show up on hs2 @@ -111,7 +113,9 @@ func TestACLs(t *testing.T) { content := user.MustGetStateEventContent(t, roomID, "m.room.server_acl", "") must.MatchGJSON(t, content, match.JSONKeyEqual("allow", []string{"*"}), - match.JSONKeyEqual("deny", []string{"hs2"}), + match.JSONKeyEqual("deny", []string{ + string(deployment.GetFullyQualifiedHomeserverName(t, "hs2")), + }), match.JSONKeyEqual("allow_ip_literals", true), ) } diff --git a/tests/federation_unreject_rejected_test.go b/tests/federation_unreject_rejected_test.go index e05e58e3..ef7f016b 100644 --- a/tests/federation_unreject_rejected_test.go +++ b/tests/federation_unreject_rejected_test.go @@ -1,13 +1,16 @@ package tests import ( + "context" "encoding/json" "testing" + "time" "github.com/matrix-org/complement" "github.com/matrix-org/complement/client" "github.com/matrix-org/complement/federation" "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/spec" ) @@ -65,7 +68,16 @@ func TestUnrejectRejectedEvents(t *testing.T) { // Send event B into the room. Event A at this point is unknown // to the homeserver and we're not going to respond to the events // request for it, so it should get rejected. - srv.MustSendTransaction(t, deployment, deployment.GetFullyQualifiedHomeserverName(t, "hs1"), []json.RawMessage{eventB.JSON()}, nil) + fedClient := srv.FederationClient(deployment) + fedClient.SendTransaction(context.Background(), gomatrixserverlib.Transaction{ + TransactionID: "complement1", + Origin: srv.ServerName(), + Destination: deployment.GetFullyQualifiedHomeserverName(t, "hs1"), + OriginServerTS: spec.AsTimestamp(time.Now()), + PDUs: []json.RawMessage{ + eventB.JSON(), + }, + }) // Now we're going to send Event A into the room, which should give // the server the prerequisite event to pass Event B later. This one