From 89b911ff14dc0a25be6ff95d3b652a02ec7bdbbd Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 22 Sep 2025 10:15:47 -0500 Subject: [PATCH 1/8] Use FQDN helper on `m.room.server_acl` events (#804) Follow-up to https://github.com/matrix-org/complement/pull/780 --- tests/federation_acl_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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), ) } From 091cbf5e86d1076cfaa738e8a61e7c0f5e2a8b3a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:16:57 +0100 Subject: [PATCH 2/8] Verify that ephemeral events down `/sync` don't have a `room_id` field (#807) --- tests/csapi/apidoc_room_receipts_test.go | 46 +++++++++++++++++++++--- tests/csapi/room_typing_test.go | 25 +++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) 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/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 From 10fee655b6185003d6e48898d4447ab2920f5a83 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:14:54 +0100 Subject: [PATCH 3/8] Lock /createRoom calls (#800) * Lock /createRoom calls To reduce the chance of multiple rooms being made in the same millisecond which makes v12 rooms unhappy as it causes the same room ID to be generated. * Update client/client.go Co-authored-by: Eric Eastwood --------- Co-authored-by: Eric Eastwood --- client/client.go | 35 ++++++++++++++++++++++++++++++++++- internal/docker/deployment.go | 16 ++++++++-------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/client/client.go b/client/client.go index 904eba12..b4d3e14e 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. @@ -137,6 +166,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) From 40d50058da03471ad288ee24737a938b8dfbbc9f Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:16:04 +0100 Subject: [PATCH 4/8] Add require `signatures` field to `/keys/upload` call (#805) --- tests/csapi/device_lists_test.go | 1 + 1 file changed, 1 insertion(+) 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{}{}, }, }), ) From e9a6b3b68b95931095f7f8f7332fb26198af2837 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:16:04 +0100 Subject: [PATCH 5/8] Add require `signatures` field to `/keys/upload` call (#805) --- tests/csapi/device_lists_test.go | 1 + 1 file changed, 1 insertion(+) 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{}{}, }, }), ) From 9951693b34eacdc3cc07328ac337b899cf34a9e1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:16:04 +0100 Subject: [PATCH 6/8] Add require `signatures` field to `/keys/upload` call (#805) --- tests/csapi/device_lists_test.go | 1 + 1 file changed, 1 insertion(+) 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{}{}, }, }), ) From 3752f0c6e4aaf550a87d5ef1863e6bbf350a0a71 Mon Sep 17 00:00:00 2001 From: Chrislearn Young Date: Fri, 10 Oct 2025 00:12:19 +0800 Subject: [PATCH 7/8] Ignore error for send B before A in TestUnrejectRejectedEvents test (#809) --- tests/federation_unreject_rejected_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 From 0de52854d3d771460dee9db3de57260de3bfe86e Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:00:54 +0100 Subject: [PATCH 8/8] Exclude .devenv directory from .gitignore (#810) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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/