From e5a5a8739b91f51ce63b844ad5ae4f99bc11db41 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Aug 2025 19:32:19 -0600 Subject: [PATCH 01/11] Try to modify MSC4311 test to meet new proposal --- tests/v12_test.go | 65 +++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 70c86f53..a31b6d22 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1357,39 +1357,54 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { return eventIDs } -func TestMSC4311FullCreateEventOnStrippedState(t *testing.T) { +func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet - deployment := complement.Deploy(t, 2) + deployment := complement.Deploy(t, 1) defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + + // Alice will invite Bob. Bob's server should receive full PDUs in `invite_room_state`. alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) - local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) - remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) + bob := srv.UserID("bob") + roomID := alice.MustCreateRoom(t, map[string]interface{}{ "room_version": roomVersion12, "preset": "public_chat", }) - for _, target := range []*client.CSAPI{local, remote} { - t.Logf("checking %s", target.UserID) - alice.MustInviteRoom(t, roomID, target.UserID) - resp, _ := target.MustSync(t, client.SyncReq{}) - inviteState := resp.Get( - fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)), - ) - must.NotEqual(t, len(inviteState.Array()), 0, "no events in invite_state") - // find the create event - found := false - for _, ev := range inviteState.Array() { - if ev.Get("type").Str == spec.MRoomCreate { - found = true - // we should have extra fields - must.MatchGJSON(t, ev, - match.JSONKeyPresent("origin_server_ts"), - ) + srv.Mux().HandleFunc("/_matrix/federation/v2/invite/{roomID}/{eventID}", srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { + if pathParams["roomID"] != roomID { + t.Errorf("Received /invite_room_state for the wrong room: %s", roomID) + return util.JSONResponse{ + Code: 400, + JSON: "wrong room", } } - if !found { - ct.Errorf(t, "failed to find create event in invite_state") + inviteRoomState := gjson.ParseBytes(fr.Content()).Get("invite_room_state").Array() + for _, ev := range inviteRoomState { + // should have extra fields + must.MatchGJSON(t, ev, match.JSONKeyPresent("origin_server_ts")) } - } - + invite := []byte(gjson.ParseBytes(fr.Content()).Get("event").Raw) + signed, err := gomatrixserverlib.SignJSON(string(srv.ServerName()), srv.KeyID, srv.Priv, invite) + if err != nil { + t.Fatalf("failed to sign invite: %s", err) + } + return util.JSONResponse{ + Code: 200, + JSON: struct { + Event any `json:"event"` + }{ + Event: signed, + }, + } + })) + alice.MustInviteRoom(t, roomID, bob) } From 35dcccfb16487a7856c6563b691aca483afbe890 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 1 May 2026 18:10:47 -0500 Subject: [PATCH 02/11] More robust test --- tests/v12_test.go | 104 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index fcd8fd2d..3108a52a 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -18,6 +18,7 @@ import ( "github.com/matrix-org/complement/match" "github.com/matrix-org/complement/must" "github.com/matrix-org/complement/runtime" + "github.com/matrix-org/complement/should" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/fclient" "github.com/matrix-org/gomatrixserverlib/spec" @@ -1338,41 +1339,61 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { return eventIDs } +// Alice will invite Bob. Bob's server should receive full PDUs in `invite_room_state`. func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet deployment := complement.Deploy(t, 1) defer deployment.Destroy(t) - srv := federation.NewServer(t, deployment, - federation.HandleKeyRequests(), - federation.HandleMakeSendJoinRequests(), - federation.HandleTransactionRequests(nil, nil), - federation.HandleEventRequests(), - ) - srv.UnexpectedRequestsAreErrors = false - cancel := srv.Listen() - defer cancel() - // Alice will invite Bob. Bob's server should receive full PDUs in `invite_room_state`. + // Alice creates a room alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) - bob := srv.UserID("bob") - roomID := alice.MustCreateRoom(t, map[string]interface{}{ "room_version": roomVersion12, "preset": "public_chat", }) + + // Create an engineered homeserver that will listen for the invite and assert + inviteWaiter := helpers.NewWaiter() + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + // FIXME: Ideally, we'd use `federation.HandleInviteRequests(...)` but it doesn't + // allow us to access the `invite_room_state` yet and requires a bit more refactoring, + // see https://github.com/matrix-org/complement/pull/796#discussion_r2278442857 srv.Mux().HandleFunc("/_matrix/federation/v2/invite/{roomID}/{eventID}", srv.ValidFederationRequest(t, func(fr *fclient.FederationRequest, pathParams map[string]string) util.JSONResponse { - if pathParams["roomID"] != roomID { - t.Errorf("Received /invite_room_state for the wrong room: %s", roomID) + t.Logf("Received invite over federation %s", + string(fr.Content()), + ) + + // Invites for an unexpected rooms is an error + roomIDFromURL := pathParams["roomID"] + if roomIDFromURL != roomID { + t.Errorf("Received invite for unexpected room: %s (expected %s)", roomIDFromURL, roomID) return util.JSONResponse{ Code: 400, - JSON: "wrong room", + JSON: "unexpected wrong room", } } - inviteRoomState := gjson.ParseBytes(fr.Content()).Get("invite_room_state").Array() - for _, ev := range inviteRoomState { - // should have extra fields - must.MatchGJSON(t, ev, match.JSONKeyPresent("origin_server_ts")) - } + + // Check to make sure the `invite_room_state` includes full PDUs (the main MSC4311 + // behavior we're trying to test) + inviteRequest := gjson.ParseBytes(fr.Content()) + must.MatchGJSON(t, inviteRequest, + JSONArraySome("invite_room_state", func(event gjson.Result) error { + // MSC4311 also mandates that `m.room.create` event is required + return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) + }), + match.JSONArrayEach("invite_room_state", func(event gjson.Result) error { + // Each event should have extra fields `origin_server_ts` that indicate we're + // seeing a full PDU and not just a "stripped state event" + return should.MatchGJSON(event, match.JSONKeyPresent("origin_server_ts")) + }), + ) + inviteWaiter.Finish() + invite := []byte(gjson.ParseBytes(fr.Content()).Get("event").Raw) signed, err := gomatrixserverlib.SignJSON(string(srv.ServerName()), srv.KeyID, srv.Priv, invite) if err != nil { @@ -1387,5 +1408,48 @@ func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { }, } })) + // Synapse seems to send `/_matrix/federation/v1/query/profile` requests to us for + // some reason. + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + + // Alice invites bob + bob := srv.UserID("bob") alice.MustInviteRoom(t, roomID, bob) + + // Wait for the invite to go over federation and be validated + inviteWaiter.Wait(t, 5*time.Second) +} + + +// JSONArraySome returns a matcher which will check that `wantKey` is an array then +// loops over each item calling `fn`. If `fn` returns nil, the matcher is satisifed, +// iterating stops and we return. +// +// Will fail if the array is empty and the check never runs +func JSONArraySome(wantKey string, fn func(gjson.Result) error) match.JSON { + return func(body gjson.Result) error { + if wantKey != "" { + body = body.Get(wantKey) + } + + if !body.Exists() { + return fmt.Errorf("JSONArraySome: missing key '%s'", wantKey) + } + if !body.IsArray() { + return fmt.Errorf("JSONArraySome: key '%s' is not an array", wantKey) + } + var satisifed bool = false + body.ForEach(func(_, val gjson.Result) bool { + err := fn(val) + satisifed = err != nil + // Stop iterating when we find a non-error + return !satisifed + }) + if !satisifed { + return fmt.Errorf("JSONArraySome('%s'): unable to find item that satisfies check", wantKey) + } + return nil + } } From 96b8d498b334902fe0d3d7a2670f1642ace8677a Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 1 May 2026 19:04:32 -0500 Subject: [PATCH 03/11] Add TODO about testing `knock_room_state` --- tests/v12_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 3108a52a..163abb72 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1339,7 +1339,8 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { return eventIDs } -// Alice will invite Bob. Bob's server should receive full PDUs in `invite_room_state`. +// Alice will invite Bob. Bob's server should receive full PDUs in `invite_room_state` +// according to MSC4311 func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet deployment := complement.Deploy(t, 1) @@ -1422,6 +1423,8 @@ func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { inviteWaiter.Wait(t, 5*time.Second) } +// TODO: Test `knock_room_state` according to MSC4311 + // JSONArraySome returns a matcher which will check that `wantKey` is an array then // loops over each item calling `fn`. If `fn` returns nil, the matcher is satisifed, From 795a9519d205e15e56d94deca299ea7ede897fe9 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 1 May 2026 20:12:00 -0500 Subject: [PATCH 04/11] Fix raw bytes being encoded as `event` instead of actual JSON --- tests/v12_test.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 163abb72..b5e9f8cc 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1395,17 +1395,30 @@ func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { ) inviteWaiter.Finish() - invite := []byte(gjson.ParseBytes(fr.Content()).Get("event").Raw) - signed, err := gomatrixserverlib.SignJSON(string(srv.ServerName()), srv.KeyID, srv.Priv, invite) + // Craft a response that we can return + rawRoomVersion := inviteRequest.Get("room_version").Raw + rawInviteEventJson := inviteRequest.Get("event").Raw + + var roomVersion gomatrixserverlib.RoomVersion + if err := json.Unmarshal([]byte(rawRoomVersion), &roomVersion); err != nil { + t.Fatalf("failed to parse room version: %s", err) + } + verImpl, err := gomatrixserverlib.GetRoomVersion(roomVersion) if err != nil { - t.Fatalf("failed to sign invite: %s", err) + t.Fatalf("failed to get room version: %s", err) } + inviteEvent, err := verImpl.NewEventFromUntrustedJSON([]byte(rawInviteEventJson)) + if err != nil { + t.Fatalf("failed to parse invite event: %s", err) + } + signedInvite := inviteEvent.Sign(string(srv.ServerName()), srv.KeyID, srv.Priv) + return util.JSONResponse{ Code: 200, JSON: struct { Event any `json:"event"` }{ - Event: signed, + Event: signedInvite, }, } })) From 47474a0425e1a24644a4960dddccfea80b16a2fb Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 4 May 2026 14:18:21 -0500 Subject: [PATCH 05/11] Use more specific type --- tests/v12_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index b5e9f8cc..5881ac3e 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1416,7 +1416,7 @@ func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { return util.JSONResponse{ Code: 200, JSON: struct { - Event any `json:"event"` + Event gomatrixserverlib.PDU `json:"event"` }{ Event: signedInvite, }, From 25ced8ba28d1fe0a8f82f5f088bd5d6df71ad122 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 4 May 2026 15:33:00 -0500 Subject: [PATCH 06/11] WIP: Add `TestMSC4311CreateEventInStrippedStateClientApi` --- tests/v12_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 5881ac3e..08ed9445 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1339,8 +1339,52 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { return eventIDs } -// Alice will invite Bob. Bob's server should receive full PDUs in `invite_room_state` -// according to MSC4311 +// MSC4311 mandates that `m.room.create` is a required event in +// `invite_state`/`knock_state` (stripped state) in `/sync responses. And in under the +// `unsigned` `invite_room_state`/`knock_room_state` on `m.room.member` events. We're +// testing the client API which should still use stripped state event format. +func TestMSC4311CreateEventInStrippedStateClientApi(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet + deployment := complement.Deploy(t, 2) + defer deployment.Destroy(t) + + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice"}) + local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) + remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) + + // Alice creates a room + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + + for _, target := range []*client.CSAPI{local, remote} { + t.Logf("checking %s", target.UserID) + alice.MustInviteRoom(t, roomID, target.UserID) + + // Make a `/sync` request so we can check `invite_state` + syncRes, _ := target.MustSync(t, client.SyncReq{}) + syncInviteStateJSONFieldKey := fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)) + must.MatchGJSON(t, syncRes, + JSONArraySome(syncInviteStateJSONFieldKey, func(event gjson.Result) error { + // MSC4311 mandates that `m.room.create` event is required in `invite_state` + return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) + }), + match.JSONArrayEach(syncInviteStateJSONFieldKey, func(event gjson.Result) error { + // Each event should be using the "stripped state event" format; and *not* have + // extra fields like `origin_server_ts` as those indicate that we're seeing a + // full PDU and not just a "stripped state event". + return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) + }), + ) + + } +} + + +// Alice will invite Bob. Bob's server should receive full PDUs in +// `invite_room_state`/`knock_room_state` (stripped state) over the federation API's +// according to MSC4311. func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet deployment := complement.Deploy(t, 1) From 32566575b892431a37e62ca23bfd0d43d1adb072 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 4 May 2026 17:53:30 -0500 Subject: [PATCH 07/11] Use more robust `MustSyncUntil` --- tests/v12_test.go | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 08ed9445..fef91a71 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1343,6 +1343,8 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { // `invite_state`/`knock_state` (stripped state) in `/sync responses. And in under the // `unsigned` `invite_room_state`/`knock_room_state` on `m.room.member` events. We're // testing the client API which should still use stripped state event format. +// +// TODO: Test `knock_state` and `knock_room_state` func TestMSC4311CreateEventInStrippedStateClientApi(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet deployment := complement.Deploy(t, 2) @@ -1363,21 +1365,34 @@ func TestMSC4311CreateEventInStrippedStateClientApi(t *testing.T) { alice.MustInviteRoom(t, roomID, target.UserID) // Make a `/sync` request so we can check `invite_state` - syncRes, _ := target.MustSync(t, client.SyncReq{}) - syncInviteStateJSONFieldKey := fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)) - must.MatchGJSON(t, syncRes, - JSONArraySome(syncInviteStateJSONFieldKey, func(event gjson.Result) error { - // MSC4311 mandates that `m.room.create` event is required in `invite_state` - return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) - }), - match.JSONArrayEach(syncInviteStateJSONFieldKey, func(event gjson.Result) error { - // Each event should be using the "stripped state event" format; and *not* have - // extra fields like `origin_server_ts` as those indicate that we're seeing a - // full PDU and not just a "stripped state event". - return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) - }), - ) + target.MustSyncUntil(t, client.SyncReq{}, func(clientUserID string, topLevelSyncJSON gjson.Result) error { + // Sync until the target sees the invite + if err := client.SyncInvitedTo(target.UserID, roomID)(clientUserID, topLevelSyncJSON); err != nil { + return err + } + // Then assert that we see the proper `invite_state` + syncInviteStateJSONFieldKey := fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)) + err := should.MatchGJSON(topLevelSyncJSON, + JSONArraySome(syncInviteStateJSONFieldKey, func(event gjson.Result) error { + // MSC4311 mandates that `m.room.create` event is required in `invite_state` + return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) + }), + match.JSONArrayEach(syncInviteStateJSONFieldKey, func(event gjson.Result) error { + // Each event should be using the "stripped state event" format; and *not* have + // extra fields like `origin_server_ts` as those indicate that we're seeing a + // full PDU and not just a "stripped state event". + return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) + }), + ) + if err != nil { + return err + } + + return nil + }) + + // TODO: Check the `m.room.member` `invite_room_state` } } @@ -1385,6 +1400,8 @@ func TestMSC4311CreateEventInStrippedStateClientApi(t *testing.T) { // Alice will invite Bob. Bob's server should receive full PDUs in // `invite_room_state`/`knock_room_state` (stripped state) over the federation API's // according to MSC4311. +// +// TODO: Test `knock_room_state` func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet deployment := complement.Deploy(t, 1) From cabb542368f2a989e43cf0b0020552f7c024d835 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 4 May 2026 18:13:50 -0500 Subject: [PATCH 08/11] WIP: Add test for `invite_room_state` in client API But it doesn't seem possible to see. For example, Synapse strips it out when trying to view events, https://github.com/element-hq/synapse/blob/6100f6e4f7fb0c72f1ae2802683ebc811c0e3a77/synapse/events/utils.py#L590-L596 --- tests/v12_test.go | 108 +++++++++++++++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 35 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index fef91a71..cf5f4222 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1345,7 +1345,7 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { // testing the client API which should still use stripped state event format. // // TODO: Test `knock_state` and `knock_room_state` -func TestMSC4311CreateEventInStrippedStateClientApi(t *testing.T) { +func TestMSC4311StrippedStateClientApi(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet deployment := complement.Deploy(t, 2) defer deployment.Destroy(t) @@ -1354,46 +1354,84 @@ func TestMSC4311CreateEventInStrippedStateClientApi(t *testing.T) { local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) - // Alice creates a room - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) + t.Run("parallel", func(t *testing.T) { + t.Run("`invite_state` on `/sync`", func(t *testing.T) { + t.Parallel() - for _, target := range []*client.CSAPI{local, remote} { - t.Logf("checking %s", target.UserID) - alice.MustInviteRoom(t, roomID, target.UserID) + // Alice creates a room + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) - // Make a `/sync` request so we can check `invite_state` - target.MustSyncUntil(t, client.SyncReq{}, func(clientUserID string, topLevelSyncJSON gjson.Result) error { - // Sync until the target sees the invite - if err := client.SyncInvitedTo(target.UserID, roomID)(clientUserID, topLevelSyncJSON); err != nil { - return err - } + for _, target := range []*client.CSAPI{local, remote} { + t.Logf("checking %s", target.UserID) + alice.MustInviteRoom(t, roomID, target.UserID) - // Then assert that we see the proper `invite_state` - syncInviteStateJSONFieldKey := fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)) - err := should.MatchGJSON(topLevelSyncJSON, - JSONArraySome(syncInviteStateJSONFieldKey, func(event gjson.Result) error { - // MSC4311 mandates that `m.room.create` event is required in `invite_state` - return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) - }), - match.JSONArrayEach(syncInviteStateJSONFieldKey, func(event gjson.Result) error { - // Each event should be using the "stripped state event" format; and *not* have - // extra fields like `origin_server_ts` as those indicate that we're seeing a - // full PDU and not just a "stripped state event". - return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) - }), - ) - if err != nil { - return err - } + // Make a `/sync` request so we can check `invite_state` + target.MustSyncUntil(t, client.SyncReq{}, func(clientUserID string, topLevelSyncJSON gjson.Result) error { + // Sync until the target sees the invite + if err := client.SyncInvitedTo(target.UserID, roomID)(clientUserID, topLevelSyncJSON); err != nil { + return err + } + + // Then assert that we see the proper `invite_state` + syncInviteStateJSONFieldKey := fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)) + err := should.MatchGJSON(topLevelSyncJSON, + JSONArraySome(syncInviteStateJSONFieldKey, func(event gjson.Result) error { + // MSC4311 mandates that `m.room.create` event is required in `invite_state` + return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) + }), + match.JSONArrayEach(syncInviteStateJSONFieldKey, func(event gjson.Result) error { + // Each event should be using the "stripped state event" format; and *not* have + // extra fields like `origin_server_ts` as those indicate that we're seeing a + // full PDU and not just a "stripped state event". + return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) + }), + ) + if err != nil { + return err + } - return nil + return nil + }) + + } }) - // TODO: Check the `m.room.member` `invite_room_state` - } + t.Run("`invite_room_state` on `m.room.member`", func(t *testing.T) { + t.Parallel() + + // Alice creates a room + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + }) + + for _, target := range []*client.CSAPI{local, remote} { + t.Logf("checking %s", target.UserID) + alice.MustInviteRoom(t, roomID, target.UserID) + + // Wait until the invite shows up + target.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(target.UserID, roomID)) + + // Check the `m.room.membership` event + memberInviteContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomMembership, target.UserID) + must.MatchGJSON(t, memberInviteContent, + JSONArraySome("unsigned.invite_room_state", func(event gjson.Result) error { + // MSC4311 mandates that `m.room.create` event is required in `invite_room_state` + return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) + }), + match.JSONArrayEach("unsigned.invite_room_state", func(event gjson.Result) error { + // Each event should be using the "stripped state event" format; and *not* have + // extra fields like `origin_server_ts` as those indicate that we're seeing a + // full PDU and not just a "stripped state event". + return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) + }), + ) + } + }) + }) } From 7171d67356f9eaf4690f3e1c90a55a9e75650815 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 4 May 2026 18:19:15 -0500 Subject: [PATCH 09/11] Remove `invite_room_state` client test --- tests/v12_test.go | 104 +++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 67 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index cf5f4222..61d177b3 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1340,9 +1340,14 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { } // MSC4311 mandates that `m.room.create` is a required event in -// `invite_state`/`knock_state` (stripped state) in `/sync responses. And in under the -// `unsigned` `invite_room_state`/`knock_room_state` on `m.room.member` events. We're -// testing the client API which should still use stripped state event format. +// `invite_state`/`knock_state` (stripped state) in `/sync responses. +// +// MSC4311 also mentions `unsigned` -> `invite_room_state`/`knock_room_state` on +// `m.room.member` events but it doesn't seem possible to view this information from the +// client API's. For example, Synapse doesn't have any API's where it sets +// [`include_stripped_room_state=True`](https://github.com/element-hq/synapse/blob/6100f6e4f7fb0c72f1ae2802683ebc811c0e3a77/synapse/events/utils.py#L590-L596) +// when viewing full events. The spec is unclear here so we will hold off on a test for +// this (or adjusting Synapse). // // TODO: Test `knock_state` and `knock_room_state` func TestMSC4311StrippedStateClientApi(t *testing.T) { @@ -1354,83 +1359,48 @@ func TestMSC4311StrippedStateClientApi(t *testing.T) { local := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "local"}) remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) - t.Run("parallel", func(t *testing.T) { - t.Run("`invite_state` on `/sync`", func(t *testing.T) { - t.Parallel() - - // Alice creates a room - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) - - for _, target := range []*client.CSAPI{local, remote} { - t.Logf("checking %s", target.UserID) - alice.MustInviteRoom(t, roomID, target.UserID) - - // Make a `/sync` request so we can check `invite_state` - target.MustSyncUntil(t, client.SyncReq{}, func(clientUserID string, topLevelSyncJSON gjson.Result) error { - // Sync until the target sees the invite - if err := client.SyncInvitedTo(target.UserID, roomID)(clientUserID, topLevelSyncJSON); err != nil { - return err - } + t.Run("`invite_state` on `/sync`", func(t *testing.T) { + t.Parallel() - // Then assert that we see the proper `invite_state` - syncInviteStateJSONFieldKey := fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)) - err := should.MatchGJSON(topLevelSyncJSON, - JSONArraySome(syncInviteStateJSONFieldKey, func(event gjson.Result) error { - // MSC4311 mandates that `m.room.create` event is required in `invite_state` - return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) - }), - match.JSONArrayEach(syncInviteStateJSONFieldKey, func(event gjson.Result) error { - // Each event should be using the "stripped state event" format; and *not* have - // extra fields like `origin_server_ts` as those indicate that we're seeing a - // full PDU and not just a "stripped state event". - return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) - }), - ) - if err != nil { - return err - } - - return nil - }) - - } + // Alice creates a room + roomID := alice.MustCreateRoom(t, map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", }) - t.Run("`invite_room_state` on `m.room.member`", func(t *testing.T) { - t.Parallel() - - // Alice creates a room - roomID := alice.MustCreateRoom(t, map[string]interface{}{ - "room_version": roomVersion12, - "preset": "public_chat", - }) - - for _, target := range []*client.CSAPI{local, remote} { - t.Logf("checking %s", target.UserID) - alice.MustInviteRoom(t, roomID, target.UserID) + for _, target := range []*client.CSAPI{local, remote} { + t.Logf("checking %s", target.UserID) + alice.MustInviteRoom(t, roomID, target.UserID) - // Wait until the invite shows up - target.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(target.UserID, roomID)) + // Make a `/sync` request so we can check `invite_state` + target.MustSyncUntil(t, client.SyncReq{}, func(clientUserID string, topLevelSyncJSON gjson.Result) error { + // Sync until the target sees the invite + if err := client.SyncInvitedTo(target.UserID, roomID)(clientUserID, topLevelSyncJSON); err != nil { + return err + } - // Check the `m.room.membership` event - memberInviteContent := alice.MustGetStateEventContent(t, roomID, spec.MRoomMembership, target.UserID) - must.MatchGJSON(t, memberInviteContent, - JSONArraySome("unsigned.invite_room_state", func(event gjson.Result) error { - // MSC4311 mandates that `m.room.create` event is required in `invite_room_state` + // Then assert that we see the proper `invite_state` + syncInviteStateJSONFieldKey := fmt.Sprintf("rooms.invite.%s.invite_state.events", client.GjsonEscape(roomID)) + err := should.MatchGJSON(topLevelSyncJSON, + JSONArraySome(syncInviteStateJSONFieldKey, func(event gjson.Result) error { + // MSC4311 mandates that `m.room.create` event is required in `invite_state` return should.MatchGJSON(event, match.JSONKeyEqual("type", "m.room.create")) }), - match.JSONArrayEach("unsigned.invite_room_state", func(event gjson.Result) error { + match.JSONArrayEach(syncInviteStateJSONFieldKey, func(event gjson.Result) error { // Each event should be using the "stripped state event" format; and *not* have // extra fields like `origin_server_ts` as those indicate that we're seeing a // full PDU and not just a "stripped state event". return should.MatchGJSON(event, match.JSONKeyMissing("origin_server_ts")) }), ) - } - }) + if err != nil { + return err + } + + return nil + }) + + } }) } From 18d6da2e59a069df0e3ed54c96dd6774bb667c71 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 4 May 2026 18:47:30 -0500 Subject: [PATCH 10/11] Remove stray parallel --- tests/v12_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 61d177b3..9cc049bb 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1360,8 +1360,6 @@ func TestMSC4311StrippedStateClientApi(t *testing.T) { remote := deployment.Register(t, "hs2", helpers.RegistrationOpts{LocalpartSuffix: "remote"}) t.Run("`invite_state` on `/sync`", func(t *testing.T) { - t.Parallel() - // Alice creates a room roomID := alice.MustCreateRoom(t, map[string]interface{}{ "room_version": roomVersion12, From 5d61d018cfb210e6bbfec2a6403d9130cfaa1e58 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 4 May 2026 18:47:54 -0500 Subject: [PATCH 11/11] Better casing --- tests/v12_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 9cc049bb..74459bd9 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -1350,7 +1350,7 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string { // this (or adjusting Synapse). // // TODO: Test `knock_state` and `knock_room_state` -func TestMSC4311StrippedStateClientApi(t *testing.T) { +func TestMSC4311StrippedStateClientAPI(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet deployment := complement.Deploy(t, 2) defer deployment.Destroy(t)