Skip to content
Snippets Groups Projects
test_user_directory.py 54 KiB
Newer Older
  • Learn to ignore specific revisions
  •         # Add a user to the room.
            builder = self.event_builder_factory.for_room_version(
                room_version,
                {
                    "type": "m.room.member",
                    "sender": user_id,
                    "state_key": user_id,
                    "room_id": room_id,
                    "content": {"membership": "join"},
                },
            )
    
    
                self.event_creation_handler.create_new_client_event(builder)
            )
    
    
            context = self.get_success(unpersisted_context.persist(event))
    
            persistence = self.hs.get_storage_controllers().persistence
            assert persistence is not None
            self.get_success(persistence.persist_event(event, context))
    
        def test_local_user_leaving_room_remains_in_user_directory(self) -> None:
            """We've chosen to simplify the user directory's implementation by
            always including local users. Ensure this invariant is maintained when
            a local user
            - leaves a room, and
            - leaves the last room they're in which is visible to this server.
    
            This is user-visible if the "search_all_users" config option is on: the
            local user who left a room would no longer be searchable if this test fails!
            """
            alice = self.register_user("alice", "pass")
            alice_token = self.login(alice, "pass")
            bob = self.register_user("bob", "pass")
            bob_token = self.login(bob, "pass")
    
            # Alice makes two public rooms, which Bob joins.
            room1 = self.helper.create_room_as(alice, is_public=True, tok=alice_token)
            room2 = self.helper.create_room_as(alice, is_public=True, tok=alice_token)
            self.helper.join(room1, bob, tok=bob_token)
            self.helper.join(room2, bob, tok=bob_token)
    
            # The user directory tables are updated.
            users, in_public, in_private = self.get_success(
                self.user_dir_helper.get_tables()
            )
            self.assertEqual(users, {alice, bob})
            self.assertEqual(
                in_public, {(alice, room1), (alice, room2), (bob, room1), (bob, room2)}
            )
            self.assertEqual(in_private, set())
    
            # Alice leaves one room. She should still be in the directory.
            self.helper.leave(room1, alice, tok=alice_token)
            users, in_public, in_private = self.get_success(
                self.user_dir_helper.get_tables()
            )
            self.assertEqual(users, {alice, bob})
            self.assertEqual(in_public, {(alice, room2), (bob, room1), (bob, room2)})
            self.assertEqual(in_private, set())
    
            # Alice leaves the other. She should still be in the directory.
            self.helper.leave(room2, alice, tok=alice_token)
            self.wait_for_background_updates()
            users, in_public, in_private = self.get_success(
                self.user_dir_helper.get_tables()
            )
            self.assertEqual(users, {alice, bob})
            self.assertEqual(in_public, {(bob, room1), (bob, room2)})
            self.assertEqual(in_private, set())
    
    
        def test_ignore_display_names_with_null_codepoints(self) -> None:
            MXC_DUMMY = "mxc://dummy"
    
            # Alice creates a public room.
            alice = self.register_user("alice", "pass")
    
            # Alice has a user directory entry to start with.
            self.assertIn(
                alice,
                self.get_success(self.user_dir_helper.get_profiles_in_user_directory()),
            )
    
            # Alice changes her name to include a null codepoint.
            self.get_success(
                self.hs.get_user_directory_handler().handle_local_profile_change(
                    alice,
                    ProfileInfo(
                        display_name="abcd\u0000efgh",
                        avatar_url=MXC_DUMMY,
                    ),
                )
            )
            # Alice's profile should be updated with the new avatar, but no display name.
            self.assertEqual(
                self.get_success(self.user_dir_helper.get_profiles_in_user_directory()),
                {alice: ProfileInfo(display_name=None, avatar_url=MXC_DUMMY)},
            )
    
    
        def test_search_punctuation(self) -> None:
            """Test that you can search for a user that includes punctuation"""
    
            searching_user = self.register_user("searcher", "password")
            searching_user_tok = self.login("searcher", "password")
    
            room_id = self.helper.create_room_as(
                searching_user,
                room_version=RoomVersions.V1.identifier,
                tok=searching_user_tok,
            )
    
            # We want to test searching for users of the form e.g. "user-1", with
            # various punctuation. We also test both where the prefix is numeric and
            # alphanumeric, as e.g. postgres tokenises "user-1" as "user" and "-1".
            i = 1
            for char in ["-", ".", "_"]:
                for use_numeric in [False, True]:
                    if use_numeric:
                        prefix1 = f"{i}"
                        prefix2 = f"{i+1}"
                    else:
                        prefix1 = f"a{i}"
                        prefix2 = f"a{i+1}"
    
                    local_user_1 = self.register_user(f"user{char}{prefix1}", "password")
                    local_user_2 = self.register_user(f"user{char}{prefix2}", "password")
    
                    self._add_user_to_room(room_id, RoomVersions.V1, local_user_1)
                    self._add_user_to_room(room_id, RoomVersions.V1, local_user_2)
    
                    results = self.get_success(
                        self.handler.search_users(searching_user, local_user_1, 20)
                    )["results"]
                    received_user_id_ordering = [result["user_id"] for result in results]
                    self.assertSequenceEqual(received_user_id_ordering[:1], [local_user_1])
    
                    i += 2
    
    
    Erik Johnston's avatar
    Erik Johnston committed
    
    class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
        servlets = [
            user_directory.register_servlets,
            room.register_servlets,
            login.register_servlets,
    
            synapse.rest.admin.register_servlets_for_client_rest_resource,
    
    Erik Johnston's avatar
    Erik Johnston committed
        ]
    
    
        def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
    
    Erik Johnston's avatar
    Erik Johnston committed
            config = self.default_config()
    
            # Re-enables updating the user directory, as that function is needed below. It
            # will be force disabled later
            config["update_user_directory_from_worker"] = None
    
    Erik Johnston's avatar
    Erik Johnston committed
            hs = self.setup_test_homeserver(config=config)
    
            self.config = hs.config
    
            return hs
    
    
        def test_disabling_room_list(self) -> None:
    
            self.config.userdirectory.user_directory_search_enabled = True
    
    Erik Johnston's avatar
    Erik Johnston committed
    
    
            # Create two users and put them in the same room.
            u1 = self.register_user("user1", "pass")
            u1_token = self.login(u1, "pass")
    
    Erik Johnston's avatar
    Erik Johnston committed
            u2 = self.register_user("user2", "pass")
    
            u2_token = self.login(u2, "pass")
    
            room = self.helper.create_room_as(u1, tok=u1_token)
            self.helper.join(room, user=u2, tok=u2_token)
    
    Erik Johnston's avatar
    Erik Johnston committed
    
    
            # Each should see the other when searching the user directory.
    
                "POST",
                b"user_directory/search",
                b'{"search_term":"user2"}',
                access_token=u1_token,
    
    Erik Johnston's avatar
    Erik Johnston committed
            )
    
            self.assertEqual(200, channel.code, channel.result)
    
    Erik Johnston's avatar
    Erik Johnston committed
            self.assertTrue(len(channel.json_body["results"]) > 0)
    
            # Disable user directory and check search returns nothing
    
            self.config.userdirectory.user_directory_search_enabled = False
    
                "POST",
                b"user_directory/search",
                b'{"search_term":"user2"}',
                access_token=u1_token,
    
    Erik Johnston's avatar
    Erik Johnston committed
            )
    
            self.assertEqual(200, channel.code, channel.result)
    
    Erik Johnston's avatar
    Erik Johnston committed
            self.assertTrue(len(channel.json_body["results"]) == 0)
    
    
    
    class UserDirectoryRemoteProfileTestCase(unittest.HomeserverTestCase):
        servlets = [
            login.register_servlets,
            synapse.rest.admin.register_servlets,
            register.register_servlets,
            room.register_servlets,
        ]
    
        def default_config(self) -> JsonDict:
            config = super().default_config()
            # Re-enables updating the user directory, as that functionality is needed below.
            config["update_user_directory_from_worker"] = None
            return config
    
        def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
            self.store = hs.get_datastores().main
            self.alice = self.register_user("alice", "alice123")
            self.alice_tok = self.login("alice", "alice123")
            self.user_dir_helper = GetUserDirectoryTables(self.store)
            self.user_dir_handler = hs.get_user_directory_handler()
            self.profile_handler = hs.get_profile_handler()
    
            # Cancel the startup call: in the steady-state case we can't rely on it anyway.
            assert self.user_dir_handler._refresh_remote_profiles_call_later is not None
            self.user_dir_handler._refresh_remote_profiles_call_later.cancel()
    
        def test_public_rooms_have_profiles_collected(self) -> None:
            """
            In a public room, member state events are treated as reflecting the user's
            real profile and they are accepted.
            (The main motivation for accepting this is to prevent having to query
            *every* single profile change over federation.)
            """
            room_id = self.helper.create_room_as(
                self.alice, is_public=True, tok=self.alice_tok
            )
            self.get_success(
                event_injection.inject_member_event(
                    self.hs,
                    room_id,
                    "@bruce:remote",
                    "join",
                    "@bruce:remote",
                    extra_content={
                        "displayname": "Bruce!",
                        "avatar_url": "mxc://remote/123",
                    },
                )
            )
            # Sending this event makes the streams move forward after the injection...
            self.helper.send(room_id, "Test", tok=self.alice_tok)
            self.pump(0.1)
    
            profiles = self.get_success(
                self.user_dir_helper.get_profiles_in_user_directory()
            )
            self.assertEqual(
                profiles.get("@bruce:remote"),
                ProfileInfo(display_name="Bruce!", avatar_url="mxc://remote/123"),
            )
    
        def test_private_rooms_do_not_have_profiles_collected(self) -> None:
            """
            In a private room, member state events are not pulled out and used to populate
            the user directory.
            """
            room_id = self.helper.create_room_as(
                self.alice, is_public=False, tok=self.alice_tok
            )
            self.get_success(
                event_injection.inject_member_event(
                    self.hs,
                    room_id,
                    "@bruce:remote",
                    "join",
                    "@bruce:remote",
                    extra_content={
                        "displayname": "super-duper bruce",
                        "avatar_url": "mxc://remote/456",
                    },
                )
            )
            # Sending this event makes the streams move forward after the injection...
            self.helper.send(room_id, "Test", tok=self.alice_tok)
            self.pump(0.1)
    
            profiles = self.get_success(
                self.user_dir_helper.get_profiles_in_user_directory()
            )
            self.assertNotIn("@bruce:remote", profiles)
    
        def test_private_rooms_have_profiles_requested(self) -> None:
            """
            When a name changes in a private room, the homeserver instead requests
            the user's global profile over federation.
            """
    
            async def get_remote_profile(
                user_id: str, ignore_backoff: bool = True
            ) -> JsonDict:
                if user_id == "@bruce:remote":
                    return {
                        "displayname": "Sir Bruce Bruceson",
                        "avatar_url": "mxc://remote/789",
                    }
                else:
                    raise ValueError(f"unable to fetch {user_id}")
    
            with patch.object(self.profile_handler, "get_profile", get_remote_profile):
                # Continue from the earlier test...
                self.test_private_rooms_do_not_have_profiles_collected()
    
                # Advance by a minute
                self.reactor.advance(61.0)
    
            profiles = self.get_success(
                self.user_dir_helper.get_profiles_in_user_directory()
            )
            self.assertEqual(
                profiles.get("@bruce:remote"),
                ProfileInfo(
                    display_name="Sir Bruce Bruceson", avatar_url="mxc://remote/789"
                ),
            )
    
        def test_profile_requests_are_retried(self) -> None:
            """
            When we fail to fetch the user's profile over federation,
            we try again later.
            """
            has_failed_once = False
    
            async def get_remote_profile(
                user_id: str, ignore_backoff: bool = True
            ) -> JsonDict:
                nonlocal has_failed_once
                if user_id == "@bruce:remote":
                    if not has_failed_once:
                        has_failed_once = True
                        raise SynapseError(502, "temporary network problem")
    
                    return {
                        "displayname": "Sir Bruce Bruceson",
                        "avatar_url": "mxc://remote/789",
                    }
                else:
                    raise ValueError(f"unable to fetch {user_id}")
    
            with patch.object(self.profile_handler, "get_profile", get_remote_profile):
                # Continue from the earlier test...
                self.test_private_rooms_do_not_have_profiles_collected()
    
                # Advance by a minute
                self.reactor.advance(61.0)
    
                # The request has already failed once
                self.assertTrue(has_failed_once)
    
                # The profile has yet to be updated.
                profiles = self.get_success(
                    self.user_dir_helper.get_profiles_in_user_directory()
                )
                self.assertNotIn(
                    "@bruce:remote",
                    profiles,
                )
    
                # Advance by five minutes, after the backoff has finished
                self.reactor.advance(301.0)
    
                # The profile should have been updated now
                profiles = self.get_success(
                    self.user_dir_helper.get_profiles_in_user_directory()
                )
                self.assertEqual(
                    profiles.get("@bruce:remote"),
                    ProfileInfo(
                        display_name="Sir Bruce Bruceson", avatar_url="mxc://remote/789"
                    ),
                )