Skip to content
Open
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e5a5a87
Try to modify MSC4311 test to meet new proposal
turt2live Aug 15, 2025
d98aae0
Merge branch 'main' into travis/msc4311-v2
kegsay Oct 1, 2025
1faef0d
Merge branch 'main' into travis/msc4311-v2
MadLittleMods Apr 30, 2026
35dcccf
More robust test
MadLittleMods May 1, 2026
96b8d49
Add TODO about testing `knock_room_state`
MadLittleMods May 2, 2026
795a951
Fix raw bytes being encoded as `event` instead of actual JSON
MadLittleMods May 2, 2026
47474a0
Use more specific type
MadLittleMods May 4, 2026
25ced8b
WIP: Add `TestMSC4311CreateEventInStrippedStateClientApi`
MadLittleMods May 4, 2026
3256657
Use more robust `MustSyncUntil`
MadLittleMods May 4, 2026
cabb542
WIP: Add test for `invite_room_state` in client API
MadLittleMods May 4, 2026
7171d67
Remove `invite_room_state` client test
MadLittleMods May 4, 2026
18d6da2
Remove stray parallel
MadLittleMods May 4, 2026
5d61d01
Better casing
MadLittleMods May 4, 2026
9f29477
Merge branch 'main' into travis/msc4311-v2
MadLittleMods May 22, 2026
633c1a2
Add client `knock_state` test
MadLittleMods May 22, 2026
55df04e
Add `knock_room_state` test (`MustKnockRoom` not implemented)
MadLittleMods May 22, 2026
92cdd79
Implement `srv.MustKnockRoom`
MadLittleMods May 22, 2026
2523631
We're doing the TODO now
MadLittleMods May 22, 2026
b4ab892
Split up tests
MadLittleMods May 23, 2026
07c60fc
Add `TestMSC4311RejectInvalidStrippedStateFederation`
MadLittleMods May 26, 2026
4b044a1
Add note about this applying to other room versions
MadLittleMods May 27, 2026
9b3b3da
`satisfied` typo
MadLittleMods May 28, 2026
f3134ad
Fix `JSONArraySome` logic
MadLittleMods May 28, 2026
9f339a8
Merge branch 'main' into travis/msc4311-v2
MadLittleMods Jul 9, 2026
9500eb0
Fix `serverNames` arg docstring typo
MadLittleMods Jul 9, 2026
d9972aa
Fix markdown syntax typo in comment
MadLittleMods Jul 9, 2026
15d86d1
Fix `room` plural typo
MadLittleMods Jul 9, 2026
8e23350
Clarify why run later "to allow some time for the ecosystem to adapt …
MadLittleMods Jul 9, 2026
f011b88
Cross-link https://github.com/element-hq/synapse/issues/19943
MadLittleMods Jul 9, 2026
69c079c
Fix other new typos
MadLittleMods Jul 9, 2026
7d525ee
Revert non-new typo
MadLittleMods Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 102 additions & 23 deletions tests/v12_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1338,39 +1339,117 @@ func asEventIDs(pdus []gomatrixserverlib.PDU) []string {
return eventIDs
}

func TestMSC4311FullCreateEventOnStrippedState(t *testing.T) {
// Alice will invite Bob. Bob's server should receive full PDUs in `invite_room_state`.
func TestMSC4311FullEventsOnStrippedStateFederation(t *testing.T) {
Comment thread
MadLittleMods marked this conversation as resolved.
runtime.SkipIf(t, runtime.Dendrite) // does not implement it yet
deployment := complement.Deploy(t, 2)
deployment := complement.Deploy(t, 1)
defer deployment.Destroy(t)

// Alice creates a room
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"})
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)),

// 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to alternatively use:

	srv := federation.NewServer(t, deployment,
		federation.HandleInviteRequests(func(p gomatrixserverlib.PDU) {
			// checks here
		}),
		federation.HandleKeyRequests(),
		federation.HandleMakeSendJoinRequests(),
		federation.HandleTransactionRequests(nil, nil),
	)

@MadLittleMods MadLittleMods May 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went down this route but the problem is that federation.HandleInviteRequests(...) doesn't allow you to inspect the invite_room_state yet. It would have to be updated to pass in the full invite request to the callback.

And gomatrixserverlib needs to be updated to handle the new world where invite_room_state can be stripped state or full PDUs.

We could have an infallible InviteV2Request.StrippedInviteRoomState() that would give a view of stripped state regardless of whether we were passed stripped or full PDU's (we can derive stripped state from the full PDU's). And then a fallible InviteV2Request.FullInviteRoomState() that would return an error if the homeserver didn't pass us full PDUs.

We would also need to update IRoomVersion to add new fields like CreateEventRequiredInInviteRoomState and FullPDUInviteRoomState (full PDU's can happen in any room version) so we can conditionally apply this behavior. Or maybe the flags could be combined as MSC4311InviteRoomState

If any of the events are not a PDU, not for the room ID specified, or fail signature checks, or the m.room.create event is missing, the receiving server MAY respond to invites with a 400 M_MISSING_PARAM standard Matrix error (new to the endpoint). For invites to room version 12+ rooms, servers SHOULD rather than MAY respond to such requests with 400 M_MISSING_PARAM.

-- matrix-org/matrix-spec-proposals#4311

t.Logf("Received invite over federation %s",
string(fr.Content()),
)
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"),
)

// 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: "unexpected wrong room",
}
}
if !found {
ct.Errorf(t, "failed to find create event in invite_state")

// 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"))
}),
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
)
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 {
t.Fatalf("failed to sign invite: %s", err)
}
return util.JSONResponse{
Code: 200,
JSON: struct {
Event any `json:"event"`
}{
Event: signed,
},
}
}))
// 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 {
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
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
}
}
Loading