Skip to content

Commit 1a3fb3a

Browse files
Merge pull request #37 from famedly/syk/test-member-removal-edge-cases
chore: update sync behaivor to handle removed user correctly
2 parents fb81ef0 + 3f2ea13 commit 1a3fb3a

3 files changed

Lines changed: 281 additions & 77 deletions

File tree

famedly_control_synapse/sync.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,31 @@ async def _process_sync(self) -> None:
120120
if response.data:
121121
group_rooms = await self.repository.get_rooms_by_group()
122122

123+
groups_with_removes = {
124+
group_id
125+
for group_id, diffs in response.data.items()
126+
if any(d.action == MembershipAction.REM for d in diffs)
127+
}
128+
group_members_cache: dict[str, list[str]] = {}
129+
if groups_with_removes:
130+
# Collect every group that shares a room with a group whose diffs
131+
# contain removes. These are the only groups whose membership we need
132+
# to filter non-needed removes (if a user is part of 2 groups and only
133+
# 1 group is removed from a room the user shouldn't be removed from the
134+
# room).
135+
other_group_ids = set()
136+
for group_id in groups_with_removes:
137+
rooms_with_this_group = group_rooms.get(group_id, [])
138+
for room_id, admin_user_id in rooms_with_this_group:
139+
for g, room_list in group_rooms.items():
140+
if g != group_id and (room_id, admin_user_id) in room_list:
141+
other_group_ids.add(g)
142+
143+
group_members_cache = {
144+
gid: await self.client.get_group_members(gid)
145+
for gid in other_group_ids
146+
}
147+
123148
for group_id, diffs in response.data.items():
124149
rooms = group_rooms.get(group_id, [])
125150
if not rooms:
@@ -130,7 +155,17 @@ async def _process_sync(self) -> None:
130155
continue
131156

132157
for room_id, admin_user_id in rooms:
133-
if not await self._apply_diffs(room_id, admin_user_id, diffs):
158+
other_groups_of_the_room = [
159+
g
160+
for g, room_list in group_rooms.items()
161+
if g != group_id and (room_id, admin_user_id) in room_list
162+
]
163+
effective_diffs = self._filter_removes(
164+
diffs, other_groups_of_the_room, group_members_cache
165+
)
166+
if not await self._apply_diffs(
167+
room_id, admin_user_id, effective_diffs
168+
):
134169
sync_succeeded = False
135170

136171
if not sync_succeeded:
@@ -147,6 +182,32 @@ async def _process_sync(self) -> None:
147182
self._sync_token_user_id, self._sync_token
148183
)
149184

185+
def _filter_removes(
186+
self,
187+
diffs: list[DiffRecord],
188+
other_groups: list[str],
189+
group_members_cache: dict[str, list[str]],
190+
) -> list[DiffRecord]:
191+
"""Filter out REM diffs for users still present in another group for the same room."""
192+
if not other_groups:
193+
return diffs
194+
195+
if not any(d.action == MembershipAction.REM for d in diffs):
196+
return diffs
197+
198+
members_in_other_groups = {
199+
member
200+
for gid in other_groups
201+
for member in group_members_cache.get(gid, [])
202+
}
203+
204+
return [
205+
d
206+
for d in diffs
207+
if d.action != MembershipAction.REM
208+
or d.external_user_id not in members_in_other_groups
209+
]
210+
150211
async def _apply_diffs(
151212
self, room_id: str, admin_user_id: str, diffs: list[DiffRecord]
152213
) -> bool:

tests/rest/test_room.py

Lines changed: 93 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,33 @@ def test_room_creation_fails_with_invalid_initial_state(
448448
new_callable=AsyncMock,
449449
)
450450
class TestAssignGroupsToManagedRoom(ModuleApiTestCase):
451+
def assert_join_for_users(self, room_id: str, users: list[str]) -> None:
452+
"""Helper function to assert the list of users are joined to the room."""
453+
for member in users:
454+
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
455+
channel = self.make_request(
456+
"GET", path, access_token=self.creator_access_token
457+
)
458+
assert (
459+
channel.code == HTTPStatus.OK
460+
), f"Expected 200 but got {channel.code} for member {member}"
461+
assert (
462+
channel.json_body["membership"] == "join"
463+
), f"Expected membership to be join but got {channel.json_body['membership']} for member {member}"
464+
465+
def assert_leave_for_users(self, room_id: str, users: list[str]) -> None:
466+
"""Helper function to assert the users have left the room."""
467+
for member in users:
468+
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
469+
channel = self.make_request(
470+
"GET", path, access_token=self.creator_access_token
471+
)
472+
assert (
473+
channel.code == HTTPStatus.OK
474+
), f"Expected 200 but got {channel.code} for member {member}"
475+
assert (
476+
channel.json_body["membership"] == "leave"
477+
), f"Expected membership to be leave but got {channel.json_body['membership']} for member {member}"
451478

452479
def test_update_single_group_to_single_group(self, mock_get_group_members) -> None:
453480
"""Tests that managed room which already have groups can be updated and the
@@ -493,27 +520,10 @@ def get_group_members(group_id): # in real case this should return external_ids
493520
assert channel.code == HTTPStatus.OK, channel.result
494521

495522
# Check if the new member of the group is joined to the room
496-
for member in new_group_members:
497-
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
498-
channel = self.make_request(
499-
"GET", path, access_token=self.creator_access_token
500-
)
501-
assert (
502-
channel.code == HTTPStatus.OK
503-
), f"Expected 200 but got {channel.code} for member {member}"
504-
assert (
505-
channel.json_body["membership"] == "join"
506-
), f"Expected membership to be join but got {channel.json_body['membership']} for member {member}"
523+
self.assert_join_for_users(room_id, new_group_members)
507524

508525
# Check if the old member who is not in the new group is removed from the room
509-
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{test_member_1}"
510-
channel = self.make_request("GET", path, access_token=self.creator_access_token)
511-
assert (
512-
channel.code == HTTPStatus.OK
513-
), f"Expected 200 but got {channel.code} for member {test_member_1}"
514-
assert (
515-
channel.json_body["membership"] == "leave"
516-
), f"Expected membership to be leave but got {channel.json_body['membership']} for member {test_member_1}"
526+
self.assert_leave_for_users(room_id, [test_member_1])
517527

518528
# Check the account data is updated
519529
room_account_data = self.get_success(
@@ -606,29 +616,8 @@ def get_members_by_group(
606616
member_should_not_be_in_room = [test_member_r]
607617

608618
# Check if the new member of the group is joined to the room
609-
for member in member_should_be_in_room:
610-
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
611-
channel = self.make_request(
612-
"GET", path, access_token=self.creator_access_token
613-
)
614-
assert (
615-
channel.code == HTTPStatus.OK
616-
), f"Expected 200 but got {channel.code} for member {member}"
617-
assert (
618-
channel.json_body["membership"] == "join"
619-
), f"Expected membership to be join but got {channel.json_body['membership']} for member {member}"
620-
621-
for member in member_should_not_be_in_room:
622-
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
623-
channel = self.make_request(
624-
"GET", path, access_token=self.creator_access_token
625-
)
626-
assert (
627-
channel.code == HTTPStatus.OK
628-
), f"Expected 200 but got {channel.code} for member {member}"
629-
assert (
630-
channel.json_body["membership"] == "leave"
631-
), f"Expected membership to be leave but got {channel.json_body['membership']} for member {member}"
619+
self.assert_join_for_users(room_id, list(member_should_be_in_room))
620+
self.assert_leave_for_users(room_id, member_should_not_be_in_room)
632621

633622
# Check the account data is updated
634623
room_account_data = self.get_success(
@@ -731,29 +720,8 @@ def get_members_by_group(
731720
member_should_not_be_in_room = [test_member_c] + test_group_3_members
732721

733722
# Check if the new member of the group is joined to the room
734-
for member in member_should_be_in_room:
735-
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
736-
channel = self.make_request(
737-
"GET", path, access_token=self.creator_access_token
738-
)
739-
assert (
740-
channel.code == HTTPStatus.OK
741-
), f"Expected 200 but got {channel.code} for member {member}"
742-
assert (
743-
channel.json_body["membership"] == "join"
744-
), f"Expected membership to be join but got {channel.json_body['membership']} for member {member}"
745-
746-
for member in member_should_not_be_in_room:
747-
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
748-
channel = self.make_request(
749-
"GET", path, access_token=self.creator_access_token
750-
)
751-
assert (
752-
channel.code == HTTPStatus.OK
753-
), f"Expected 200 but got {channel.code} for member {member}"
754-
assert (
755-
channel.json_body["membership"] == "leave"
756-
), f"Expected membership to be leave but got {channel.json_body['membership']} for member {member}"
723+
self.assert_join_for_users(room_id, member_should_be_in_room)
724+
self.assert_leave_for_users(room_id, member_should_not_be_in_room)
757725

758726
# Check the account data is updated
759727
room_account_data = self.get_success(
@@ -763,6 +731,63 @@ def get_members_by_group(
763731
MANAGED_ROOM_TYPE: {"groups": new_group_info}
764732
}, room_account_data
765733

734+
def test_user_from_removed_group_is_still_in_other_assigned_group(
735+
self, mock_get_group_members
736+
) -> None:
737+
"""
738+
Test the edge case when the member_r was both in group 1 and group 2, and
739+
group 2 was removed from the room.
740+
Old groups info: test_group_1, test_group_2
741+
New groups info: test_group_1
742+
"""
743+
test_group_1 = "test_group_1"
744+
test_member_a = self.register_user("test_member_a", "password")
745+
test_member_r = self.register_user("test_member_r", "password")
746+
747+
test_group_2 = "test_group_2"
748+
test_member_b = self.register_user("test_member_b", "password")
749+
750+
old_group_info = [test_group_1, test_group_2]
751+
new_group_info = [test_group_1]
752+
753+
def get_group_members(group_id):
754+
if group_id == test_group_1:
755+
return [test_member_a, test_member_r]
756+
elif group_id == test_group_2:
757+
return [test_member_b, test_member_r]
758+
return []
759+
760+
mock_get_group_members.side_effect = get_group_members
761+
762+
room_id = self._create_managed_room(name="Test Room", groups=old_group_info)
763+
self._test_get_membership(
764+
room_id,
765+
[test_member_a, test_member_b, test_member_r],
766+
expect_code=200,
767+
)
768+
room_account_data = self.get_success(
769+
self.store.get_account_data_for_room(self.creator, room_id)
770+
)
771+
assert room_account_data == {
772+
MANAGED_ROOM_TYPE: {"groups": old_group_info}
773+
}, room_account_data
774+
775+
# Now update the group information
776+
channel = self.make_request(
777+
method="POST",
778+
path=self.BASE_PATH + f"/{room_id}/groups",
779+
content={"groups": new_group_info},
780+
access_token=self.creator_access_token,
781+
shorthand=False,
782+
)
783+
assert channel.code == HTTPStatus.OK, channel.result
784+
785+
member_should_be_in_room = [test_member_a, test_member_r]
786+
member_should_not_be_in_room = [test_member_b]
787+
788+
self.assert_join_for_users(room_id, member_should_be_in_room)
789+
self.assert_leave_for_users(room_id, member_should_not_be_in_room)
790+
766791
def test_prevent_room_creator_membership_change(
767792
self, mock_get_group_members
768793
) -> None:
@@ -933,17 +958,9 @@ def update_room_membership_side_effect(
933958

934959
# Since both members failed to be removed, they should still be in the room
935960
# and the new member should be joined to the room
936-
for member in [test_member_1, test_member_2, test_member_3]:
937-
path = f"/_matrix/client/v3/rooms/{room_id}/state/m.room.member/{member}"
938-
channel = self.make_request(
939-
"GET", path, access_token=self.creator_access_token
940-
)
941-
assert (
942-
channel.code == HTTPStatus.OK
943-
), f"Expected 200 but got {channel.code} for member {member}"
944-
assert (
945-
channel.json_body["membership"] == "join"
946-
), f"Expected membership to be join but got {channel.json_body['membership']} for member {member}"
961+
self.assert_join_for_users(
962+
room_id, [test_member_1, test_member_2, test_member_3]
963+
)
947964

948965
# Check both metrics are incremented by 1 each
949966
new_value_unknown = famedly_control_user_sync_error.labels(

0 commit comments

Comments
 (0)