Skip to content

Fix flawed membership checks and align membership checking patterns#813

Merged
MadLittleMods merged 12 commits intomainfrom
madlittlemods/better-wait-for-membershipin-sync
Nov 10, 2025
Merged

Fix flawed membership checks and align membership checking patterns#813
MadLittleMods merged 12 commits intomainfrom
madlittlemods/better-wait-for-membershipin-sync

Conversation

@MadLittleMods
Copy link
Copy Markdown
Collaborator

@MadLittleMods MadLittleMods commented Oct 27, 2025

Spawning from #808 per my suggestion on #808 (comment),

We have other spots that have flawed membership checks that need to be fixed. For example, when our goal is to wait for the user's membership to be leave, we should keep checking until it is leave. Currently, there are some spots that wait until any membership exists for the user and then asserts leave on that which is flawed because that user may have previous membership events that may be picked up first instead of waiting for the leave.

This PR fixes those flawed checks and aligns our membership checks so we don't cargo cult this bad pattern elsewhere.

  • Generalize our client.SyncXXX helpers to use syncMembershipIn utility
    • More robust
    • Standardize extra checks on event (previously, only available with client.SyncJoinedTo)
  • Introduce client.SyncBannedFrom so we can differentiate ban/leave

Dev notes

Pull Request Checklist

Signed-off-by: Eric Eastwood erice@element.io

Comment on lines -300 to -311
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
},
))
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Previous flawed check.

Our goal is to wait until bob is joined. Previously, this waited for any bob membership (which could have been the invite instead of the join which we're waiting for) and then asserted that it must be a join.

Comment on lines -466 to -478
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
},
))
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Previous flawed check.

Our goal is to wait until charlie is joined. Previously, this waited for any charlie membership (which could have been the leave instead of the join which we're waiting for) and then asserted that it must be a join.

Comment thread client/sync.go
Comment on lines +299 to +315
// 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)
}
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread tests/restricted_rooms_test.go
return true
},
))
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've double-checked the translation is correct for all of these updates:

  • The client syncing
  • The user we're checking
  • The membership being checked

Comment on lines -82 to -91
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
},
))
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Any of these client.SyncTimelineHas(...) checks where we check until we see a certain event type and then use an assert for the membership are bad form.

While, it works well in some tests like this specific one, that's only because the user doesn't have any other previous membership in the room to be confused with. If there was any other previous membership for this user, this is flawed (see #813 (comment) as an example).

Instead of leaving these around to be copy-pasted and cargo-culted around, I've updated all of them to use the more proper assertion check.

Comment on lines -125 to -132
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
},
))
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For reference, this kind of check was just fine. We return false until we find a leave membership for the user. While fine, I've updated it to use the more proper utility we have for this.

(notice the difference to the above bad form assertion)

@MadLittleMods MadLittleMods marked this pull request as ready for review October 27, 2025 20:47
@MadLittleMods MadLittleMods requested review from a team as code owners October 27, 2025 20:47
Copy link
Copy Markdown
Member

@anoadragon453 anoadragon453 left a comment

Choose a reason for hiding this comment

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

Thanks for taking this on! I wonder if it fixes any flaky tests in Synapse.

Comment thread client/sync.go
Comment on lines +335 to +338
// 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.
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.

Any reason not to enable state_after for all /sync calls in Complement?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Could work although it's only available in new Matrix versions (added in v1.16) and I don't think we can assume all homeservers support that version yet.

Something to revisit in the future ⏩

Comment thread client/sync.go Outdated
Comment on lines +353 to +354
if clientUserID != userID ||
// Otherwise, if the client is the user whose membership we are checking,
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.

This comment and if statement don't align.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think it does align although more subtle than the other similar comments as it's using negative logic.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In any case, I've updated the logic layout and comments to make this more clear:

complement/client/sync.go

Lines 347 to 357 in e1ac188

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

@MadLittleMods MadLittleMods merged commit 1de6412 into main Nov 10, 2025
4 checks passed
@MadLittleMods MadLittleMods deleted the madlittlemods/better-wait-for-membershipin-sync branch November 10, 2025 20:41
@MadLittleMods MadLittleMods removed the request for review from anoadragon453 November 10, 2025 20:41
@MadLittleMods
Copy link
Copy Markdown
Collaborator Author

Thanks for the review @anoadragon453 🦘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants