from datetime import timedelta from typing import Any from unittest import mock import orjson import time_machine from django.test import override_settings from django.utils.timezone import now as timezone_now from zerver.actions.message_delete import do_delete_messages from zerver.actions.message_edit import ( build_message_edit_request, check_update_message, do_update_message, maybe_send_resolve_topic_notifications, ) from zerver.actions.reactions import do_add_reaction from zerver.actions.realm_settings import ( do_change_realm_permission_group_setting, do_set_realm_property, ) from zerver.actions.streams import do_change_stream_group_based_setting, do_set_stream_property from zerver.actions.user_groups import check_add_user_group from zerver.actions.user_settings import do_change_user_setting from zerver.actions.user_topics import do_set_user_topic_visibility_policy from zerver.lib.message import truncate_topic from zerver.lib.test_classes import ZulipTestCase, get_topic_messages from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.topic import RESOLVED_TOPIC_PREFIX from zerver.lib.types import StreamMessageEditRequest, UserGroupMembersData from zerver.lib.user_topics import ( get_users_with_user_topic_visibility_policy, set_topic_visibility_policy, topic_has_visibility_policy, ) from zerver.lib.utils import assert_is_not_none from zerver.models import Message, UserMessage, UserProfile, UserTopic from zerver.models.constants import MAX_TOPIC_NAME_LENGTH from zerver.models.groups import NamedUserGroup, SystemGroups from zerver.models.realms import RealmTopicsPolicyEnum from zerver.models.streams import Stream, StreamTopicsPolicyEnum, get_stream from zerver.models.users import ResolvedTopicNoticeAutoReadPolicyEnum class MessageMoveTopicTest(ZulipTestCase): def check_topic(self, msg_id: int, topic_name: str) -> None: msg = Message.objects.get(id=msg_id) self.assertEqual(msg.topic_name(), topic_name) def assert_move_message( self, user: str, orig_stream: Stream, orig_topic_name: str = "test", stream_id: int | None = None, topic_name: str | None = None, expected_error: str | None = None, ) -> None: user_profile = self.example_user(user) self.subscribe(user_profile, orig_stream.name) message_id = self.send_stream_message( user_profile, orig_stream.name, topic_name=orig_topic_name ) params_dict: dict[str, str | int] = {} if stream_id is not None: params_dict["stream_id"] = stream_id if topic_name is not None: params_dict["topic"] = topic_name result = self.api_patch( user_profile, "/api/v1/messages/" + str(message_id), params_dict, ) if expected_error is not None: self.assert_json_error(result, expected_error) else: self.assert_json_success(result) def assert_has_visibility_policy( self, user_profile: UserProfile, topic_name: str, stream: Stream, visibility_policy: int, *, expected: bool = True, ) -> None: if expected: self.assertTrue( topic_has_visibility_policy(user_profile, stream.id, topic_name, visibility_policy) ) else: self.assertFalse( topic_has_visibility_policy(user_profile, stream.id, topic_name, visibility_policy) ) def test_private_message_edit_topic(self) -> None: hamlet = self.example_user("hamlet") self.login("hamlet") cordelia = self.example_user("cordelia") msg_id = self.send_personal_message(hamlet, cordelia) result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "Should not exist", }, ) self.assert_json_error(result, "Direct messages cannot have topics.") def test_propagate_invalid(self) -> None: self.login("hamlet") id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1") result = self.client_patch( "/json/messages/" + str(id1), { "topic": "edited", "propagate_mode": "invalid", }, ) self.assert_json_error(result, "Invalid propagate_mode") self.check_topic(id1, topic_name="topic1") result = self.client_patch( "/json/messages/" + str(id1), { "content": "edited", "propagate_mode": "change_all", }, ) self.assert_json_error(result, "Invalid propagate_mode without topic edit") self.check_topic(id1, topic_name="topic1") def test_edit_message_empty_topic_with_extra_space(self) -> None: self.login("hamlet") msg_id = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="before edit" ) result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": " ", }, ) self.assert_json_success(result) self.check_topic(msg_id, "") def test_topic_required_in_mandatory_topic_realm(self) -> None: admin_user = self.example_user("iago") hamlet = self.example_user("hamlet") realm = admin_user.realm self.login_user(admin_user) stream = self.make_stream("new_stream") stream_mandatory_topics = self.make_stream( "topics_required", topics_policy=StreamTopicsPolicyEnum.disable_empty_topic.value ) self.subscribe(admin_user, stream.name) self.subscribe(hamlet, stream.name) original_topic_name = "topic 1" message_id = self.send_stream_message( hamlet, stream.name, topic_name=original_topic_name, ) # Verify with topics_policy=disable_empty_topic: # * A topic can't be moved to an empty topic # * A topic can be moved to a non-empty topic do_set_realm_property( realm, "topics_policy", RealmTopicsPolicyEnum.disable_empty_topic, acting_user=admin_user, ) for topic_name in ["(no topic)", ""]: result = self.client_patch( f"/json/messages/{message_id}", { "topic": topic_name, }, ) self.assert_json_error( result, "Sending messages to the general chat is not allowed in this channel." ) self.check_topic(message_id, topic_name=original_topic_name) new_topic_name = "new valid topic" result = self.client_patch( f"/json/messages/{message_id}", { "topic": new_topic_name, }, ) self.assert_json_success(result) self.check_topic(message_id, new_topic_name) # Verify with topics_policy=allow_empty_topic: # * A topic can be moved to an empty topic # * A topic can be moved to a non-empty topic do_set_realm_property( realm, "topics_policy", RealmTopicsPolicyEnum.allow_empty_topic, acting_user=admin_user ) for topic_name in ["(no topic)", "", "non-empty topic"]: result = self.client_patch( f"/json/messages/{message_id}", { "topic": topic_name, }, ) self.assert_json_success(result) self.check_topic(message_id, topic_name) # Test that message cannot be moved to empty topic in stream with # `topics_policy=disable_empty_topic`. for topic_name in ["(no topic)", ""]: result = self.client_patch( f"/json/messages/{message_id}", {"topic": topic_name, "stream_id": stream_mandatory_topics.id}, ) self.assert_json_error( result, "Sending messages to the general chat is not allowed in this channel." ) self.check_topic(message_id, topic_name="non-empty topic") # Test that message cannot be moved to empty topic in stream with # `topics_policy=disable_empty_topic` when `topic_name` is `None`. for topic_name in ["(no topic)", ""]: message_id = self.send_stream_message( hamlet, stream.name, topic_name=topic_name, ) result = self.client_patch( f"/json/messages/{message_id}", {"stream_id": stream_mandatory_topics.id}, ) self.assert_json_error( result, "Sending messages to the general chat is not allowed in this channel." ) self.check_topic(message_id, topic_name="") def test_edit_message_invalid_topic(self) -> None: self.login("hamlet") msg_id = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="before edit" ) result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "editing\nfun", }, ) self.assert_json_error(result, "Invalid character in topic, at position 8!") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": f"{Message.DM_TOPIC}", }, ) self.assert_json_error(result, "Invalid character in topic, at position 1!") @mock.patch("zerver.actions.message_edit.send_event_on_commit") def test_edit_topic_public_history_stream(self, mock_send_event: mock.MagicMock) -> None: stream_name = "Macbeth" hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") self.make_stream(stream_name, history_public_to_subscribers=True) self.subscribe(hamlet, stream_name) self.login_user(hamlet) message_id = self.send_stream_message(hamlet, stream_name, "Where am I?") self.login_user(cordelia) self.subscribe(cordelia, stream_name) message = Message.objects.get(id=message_id) def do_update_message_topic_success( user_profile: UserProfile, message: Message, topic_name: str, users_to_be_notified: list[dict[str, Any]], ) -> None: message_edit_request = build_message_edit_request( message=message, user_profile=user_profile, propagate_mode="change_later", stream_id=None, topic_name=topic_name, content=None, ) do_update_message( user_profile=user_profile, target_message=message, message_edit_request=message_edit_request, send_notification_to_old_thread=False, send_notification_to_new_thread=False, rendering_result=None, prior_mention_user_ids=set(), mention_data=None, ) mock_send_event.assert_called_with(mock.ANY, mock.ANY, users_to_be_notified) # Returns the users that need to be notified when a message topic is changed def notify(user_id: int) -> dict[str, Any]: um = UserMessage.objects.get(message=message_id) if um.user_profile_id == user_id: return { "id": user_id, "flags": um.flags_list(), } else: return { "id": user_id, "flags": ["read"], } users_to_be_notified = list(map(notify, [hamlet.id, cordelia.id])) # Edit topic of a message sent before Cordelia subscribed the stream do_update_message_topic_success( cordelia, message, "Othello eats apple", users_to_be_notified ) # If Cordelia is long-term idle, she doesn't get a notification. cordelia.long_term_idle = True cordelia.save() users_to_be_notified = list(map(notify, [hamlet.id])) do_update_message_topic_success( cordelia, message, "Another topic idle", users_to_be_notified ) cordelia.long_term_idle = False cordelia.save() # Even if Hamlet unsubscribes the stream, he should be notified when the topic is changed # because he has a UserMessage row. self.unsubscribe(hamlet, stream_name) users_to_be_notified = list(map(notify, [hamlet.id, cordelia.id])) do_update_message_topic_success(cordelia, message, "Another topic", users_to_be_notified) # Hamlet subscribes to the stream again and Cordelia unsubscribes, then Hamlet changes # the message topic. Cordelia won't receive any updates when a message on that stream is # changed because she is not a subscriber and doesn't have a UserMessage row. self.subscribe(hamlet, stream_name) self.unsubscribe(cordelia, stream_name) self.login_user(hamlet) users_to_be_notified = list(map(notify, [hamlet.id])) do_update_message_topic_success(hamlet, message, "Change again", users_to_be_notified) @mock.patch("zerver.actions.user_topics.send_event_on_commit") def test_edit_muted_topic(self, mock_send_event_on_commit: mock.MagicMock) -> None: stream_name = "Stream 123" stream = self.make_stream(stream_name) hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") aaron = self.example_user("aaron") self.subscribe(hamlet, stream_name) self.login_user(hamlet) message_id = self.send_stream_message( hamlet, stream_name, topic_name="Topic1", content="Hello World" ) self.subscribe(cordelia, stream_name) self.login_user(cordelia) self.subscribe(aaron, stream_name) self.login_user(aaron) def assert_is_topic_muted( user_profile: UserProfile, stream_id: int, topic_name: str, *, muted: bool, ) -> None: if muted: self.assertTrue( topic_has_visibility_policy( user_profile, stream_id, topic_name, UserTopic.VisibilityPolicy.MUTED ) ) else: self.assertFalse( topic_has_visibility_policy( user_profile, stream_id, topic_name, UserTopic.VisibilityPolicy.MUTED ) ) already_muted_topic_name = "Already muted topic" muted_topics = [ [stream_name, "Topic1"], [stream_name, "Topic2"], [stream_name, already_muted_topic_name], ] set_topic_visibility_policy(hamlet, muted_topics, UserTopic.VisibilityPolicy.MUTED) set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED) # users that need to be notified by send_event in the case of change-topic-name operation. users_to_be_notified_via_muted_topics_event: list[int] = [] users_to_be_notified_via_user_topic_event: list[int] = [] for user_topic in get_users_with_user_topic_visibility_policy(stream.id, "Topic1"): # We are appending the same data twice because 'user_topic' event notifies # the user during delete and create operation. users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id) users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id) # 'muted_topics' event notifies the user of muted topics during create # operation only. users_to_be_notified_via_muted_topics_event.append(user_topic.user_profile_id) change_all_topic_name = "Topic 1 edited" # Verify how many total database queries are required. We # expect 6 queries (4/visibility_policy to update the muted # state + 1/user with a UserTopic row for the events data) # beyond what is typical were there not UserTopic records to # update. Ideally, we'd eliminate the per-user component. with self.assert_database_query_count(27): check_update_message( user_profile=hamlet, message_id=message_id, stream_id=None, topic_name=change_all_topic_name, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) # Extract the send_event call where event type is 'user_topic' or 'muted_topics. # Here we assert that the expected users are notified properly. users_notified_via_muted_topics_event: list[int] = [] users_notified_via_user_topic_event: list[int] = [] for call_args in mock_send_event_on_commit.call_args_list: (arg_realm, arg_event, arg_notified_users) = call_args[0] if arg_event["type"] == "user_topic": users_notified_via_user_topic_event.append(*arg_notified_users) elif arg_event["type"] == "muted_topics": users_notified_via_muted_topics_event.append(*arg_notified_users) self.assertEqual( sorted(users_notified_via_muted_topics_event), sorted(users_to_be_notified_via_muted_topics_event), ) self.assertEqual( sorted(users_notified_via_user_topic_event), sorted(users_to_be_notified_via_user_topic_event), ) assert_is_topic_muted(hamlet, stream.id, "Topic1", muted=False) assert_is_topic_muted(cordelia, stream.id, "Topic1", muted=False) assert_is_topic_muted(aaron, stream.id, "Topic1", muted=False) assert_is_topic_muted(hamlet, stream.id, "Topic2", muted=True) assert_is_topic_muted(cordelia, stream.id, "Topic2", muted=True) assert_is_topic_muted(aaron, stream.id, "Topic2", muted=False) assert_is_topic_muted(hamlet, stream.id, change_all_topic_name, muted=True) assert_is_topic_muted(cordelia, stream.id, change_all_topic_name, muted=True) assert_is_topic_muted(aaron, stream.id, change_all_topic_name, muted=False) change_later_topic_name = "Topic 1 edited again" check_update_message( user_profile=hamlet, message_id=message_id, stream_id=None, topic_name=change_later_topic_name, propagate_mode="change_later", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) assert_is_topic_muted(hamlet, stream.id, change_all_topic_name, muted=False) assert_is_topic_muted(hamlet, stream.id, change_later_topic_name, muted=True) # Make sure we safely handle the case of the new topic being already muted. check_update_message( user_profile=hamlet, message_id=message_id, stream_id=None, topic_name=already_muted_topic_name, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) assert_is_topic_muted(hamlet, stream.id, change_later_topic_name, muted=False) assert_is_topic_muted(hamlet, stream.id, already_muted_topic_name, muted=True) change_one_topic_name = "Topic 1 edited change_one" check_update_message( user_profile=hamlet, message_id=message_id, stream_id=None, topic_name=change_one_topic_name, propagate_mode="change_one", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) assert_is_topic_muted(hamlet, stream.id, change_one_topic_name, muted=True) assert_is_topic_muted(hamlet, stream.id, change_later_topic_name, muted=False) # Move topic between two public streams. desdemona = self.example_user("desdemona") message_id = self.send_stream_message( hamlet, stream_name, topic_name="New topic", content="Hello World" ) new_public_stream = self.make_stream("New public stream") self.subscribe(desdemona, new_public_stream.name) self.login_user(desdemona) muted_topics = [ [stream_name, "New topic"], ] set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED) set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED) with self.assert_database_query_count(29): check_update_message( user_profile=desdemona, message_id=message_id, stream_id=new_public_stream.id, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) assert_is_topic_muted(desdemona, stream.id, "New topic", muted=False) assert_is_topic_muted(cordelia, stream.id, "New topic", muted=False) assert_is_topic_muted(aaron, stream.id, "New topic", muted=False) assert_is_topic_muted(desdemona, new_public_stream.id, "New topic", muted=True) assert_is_topic_muted(cordelia, new_public_stream.id, "New topic", muted=True) assert_is_topic_muted(aaron, new_public_stream.id, "New topic", muted=False) # Move topic to a private stream. message_id = self.send_stream_message( hamlet, stream_name, topic_name="New topic", content="Hello World" ) new_private_stream = self.make_stream("New private stream", invite_only=True) self.subscribe(desdemona, new_private_stream.name) self.login_user(desdemona) muted_topics = [ [stream_name, "New topic"], ] set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED) set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED) with self.assert_database_query_count(35): check_update_message( user_profile=desdemona, message_id=message_id, stream_id=new_private_stream.id, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) # Cordelia is not subscribed to the private stream, so # Cordelia should have had the topic unmuted, while Desdemona # should have had her muted topic record moved. assert_is_topic_muted(desdemona, stream.id, "New topic", muted=False) assert_is_topic_muted(cordelia, stream.id, "New topic", muted=False) assert_is_topic_muted(aaron, stream.id, "New topic", muted=False) assert_is_topic_muted(desdemona, new_private_stream.id, "New topic", muted=True) assert_is_topic_muted(cordelia, new_private_stream.id, "New topic", muted=False) assert_is_topic_muted(aaron, new_private_stream.id, "New topic", muted=False) # Move topic between two public streams with change in topic name. desdemona = self.example_user("desdemona") message_id = self.send_stream_message( hamlet, stream_name, topic_name="New topic 2", content="Hello World" ) self.login_user(desdemona) muted_topics = [ [stream_name, "New topic 2"], ] set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED) set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED) with self.assert_database_query_count(31): check_update_message( user_profile=desdemona, message_id=message_id, stream_id=new_public_stream.id, topic_name="changed topic name", propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) assert_is_topic_muted(desdemona, stream.id, "New topic 2", muted=False) assert_is_topic_muted(cordelia, stream.id, "New topic 2", muted=False) assert_is_topic_muted(aaron, stream.id, "New topic 2", muted=False) assert_is_topic_muted(desdemona, new_public_stream.id, "changed topic name", muted=True) assert_is_topic_muted(cordelia, new_public_stream.id, "changed topic name", muted=True) assert_is_topic_muted(aaron, new_public_stream.id, "changed topic name", muted=False) # Moving only half the messages doesn't move UserTopic records. second_message_id = self.send_stream_message( hamlet, stream_name, topic_name="changed topic name", content="Second message" ) with self.assert_database_query_count(25): check_update_message( user_profile=desdemona, message_id=second_message_id, stream_id=new_public_stream.id, topic_name="final topic name", propagate_mode="change_later", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) assert_is_topic_muted(desdemona, new_public_stream.id, "changed topic name", muted=True) assert_is_topic_muted(cordelia, new_public_stream.id, "changed topic name", muted=True) assert_is_topic_muted(aaron, new_public_stream.id, "changed topic name", muted=False) assert_is_topic_muted(desdemona, new_public_stream.id, "final topic name", muted=False) assert_is_topic_muted(cordelia, new_public_stream.id, "final topic name", muted=False) assert_is_topic_muted(aaron, new_public_stream.id, "final topic name", muted=False) @mock.patch("zerver.actions.user_topics.send_event_on_commit") def test_edit_unmuted_topic(self, mock_send_event_on_commit: mock.MagicMock) -> None: stream_name = "Stream 123" stream = self.make_stream(stream_name) hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") aaron = self.example_user("aaron") othello = self.example_user("othello") self.subscribe(hamlet, stream_name) self.login_user(hamlet) message_id = self.send_stream_message( hamlet, stream_name, topic_name="Topic1", content="Hello World" ) self.subscribe(cordelia, stream_name) self.login_user(cordelia) self.subscribe(aaron, stream_name) self.login_user(aaron) self.subscribe(othello, stream_name) self.login_user(othello) # Initially, hamlet and othello set visibility_policy as UNMUTED for 'Topic1' and 'Topic2', # cordelia sets visibility_policy as MUTED for 'Topic1' and 'Topic2', while # aaron doesn't have a visibility_policy set for 'Topic1' or 'Topic2'. # # After moving messages from 'Topic1' to 'Topic 1 edited', the expected behaviour is: # hamlet and othello have UNMUTED 'Topic 1 edited' and no visibility_policy set for 'Topic1' # cordelia has MUTED 'Topic 1 edited' and no visibility_policy set for 'Topic1' # # There is no change in visibility_policy configurations for 'Topic2', i.e. # hamlet and othello have UNMUTED 'Topic2' + cordelia has MUTED 'Topic2' # aaron still doesn't have visibility_policy set for any topic. # # Note: We have used two users with UNMUTED 'Topic1' to verify that the query count # doesn't increase (in order to update UserTopic records) with an increase in users. # (We are using bulk database operations.) # 1 query/user is added in order to send muted_topics event.(which will be deprecated) topics = [ [stream_name, "Topic1"], [stream_name, "Topic2"], ] set_topic_visibility_policy(hamlet, topics, UserTopic.VisibilityPolicy.UNMUTED) set_topic_visibility_policy(cordelia, topics, UserTopic.VisibilityPolicy.MUTED) set_topic_visibility_policy(othello, topics, UserTopic.VisibilityPolicy.UNMUTED) # users that need to be notified by send_event in the case of change-topic-name operation. users_to_be_notified_via_muted_topics_event: list[int] = [] users_to_be_notified_via_user_topic_event: list[int] = [] for user_topic in get_users_with_user_topic_visibility_policy(stream.id, "Topic1"): # We are appending the same data twice because 'user_topic' event notifies # the user during delete and create operation. users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id) users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id) # 'muted_topics' event notifies the user of muted topics during create # operation only. users_to_be_notified_via_muted_topics_event.append(user_topic.user_profile_id) change_all_topic_name = "Topic 1 edited" with self.assert_database_query_count(32): check_update_message( user_profile=hamlet, message_id=message_id, stream_id=None, topic_name=change_all_topic_name, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) # Extract the send_event call where event type is 'user_topic' or 'muted_topics. # Here we assert that the expected users are notified properly. users_notified_via_muted_topics_event: list[int] = [] users_notified_via_user_topic_event: list[int] = [] for call_args in mock_send_event_on_commit.call_args_list: (arg_realm, arg_event, arg_notified_users) = call_args[0] if arg_event["type"] == "user_topic": users_notified_via_user_topic_event.append(*arg_notified_users) elif arg_event["type"] == "muted_topics": users_notified_via_muted_topics_event.append(*arg_notified_users) self.assertEqual( sorted(users_notified_via_muted_topics_event), sorted(users_to_be_notified_via_muted_topics_event), ) self.assertEqual( sorted(users_notified_via_user_topic_event), sorted(users_to_be_notified_via_user_topic_event), ) # No visibility_policy set for 'Topic1' self.assert_has_visibility_policy( hamlet, "Topic1", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False ) self.assert_has_visibility_policy( cordelia, "Topic1", stream, UserTopic.VisibilityPolicy.MUTED, expected=False ) self.assert_has_visibility_policy( othello, "Topic1", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False ) self.assert_has_visibility_policy( aaron, "Topic1", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False ) # No change in visibility_policy configurations for 'Topic2' self.assert_has_visibility_policy( hamlet, "Topic2", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=True ) self.assert_has_visibility_policy( cordelia, "Topic2", stream, UserTopic.VisibilityPolicy.MUTED, expected=True ) self.assert_has_visibility_policy( othello, "Topic2", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=True ) self.assert_has_visibility_policy( aaron, "Topic2", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False ) # UserTopic records moved to 'Topic 1 edited' after move-topic operation. self.assert_has_visibility_policy( hamlet, change_all_topic_name, stream, UserTopic.VisibilityPolicy.UNMUTED, expected=True ) self.assert_has_visibility_policy( cordelia, change_all_topic_name, stream, UserTopic.VisibilityPolicy.MUTED, expected=True ) self.assert_has_visibility_policy( othello, change_all_topic_name, stream, UserTopic.VisibilityPolicy.UNMUTED, expected=True, ) self.assert_has_visibility_policy( aaron, change_all_topic_name, stream, UserTopic.VisibilityPolicy.MUTED, expected=False ) def test_merge_user_topic_states_on_move_messages(self) -> None: stream_name = "Stream 123" stream = self.make_stream(stream_name) hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") aaron = self.example_user("aaron") shiva = self.example_user("shiva") self.subscribe(hamlet, stream_name) self.login_user(hamlet) self.subscribe(cordelia, stream_name) self.login_user(cordelia) self.subscribe(aaron, stream_name) self.login_user(aaron) # Test the following cases: # # orig_topic | target_topic | final behaviour # INHERIT INHERIT INHERIT # INHERIT MUTED INHERIT # INHERIT UNMUTED UNMUTED # INHERIT FOLLOWED FOLLOWED orig_topic = "Topic1" target_topic = "Topic1 edited" orig_message_id = self.send_stream_message( hamlet, stream_name, topic_name=orig_topic, content="Hello World" ) self.send_stream_message( hamlet, stream_name, topic_name=target_topic, content="Hello World 2" ) # By default: # visibility_policy of 'hamlet', 'cordelia', 'aaron' for 'orig_topic': INHERIT # visibility_policy of 'hamlet' for 'target_topic': INHERIT # # So we don't need to manually set visibility_policy to INHERIT whenever required, # here and later in this test. do_set_user_topic_visibility_policy( cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( aaron, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( shiva, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED ) check_update_message( user_profile=hamlet, message_id=orig_message_id, stream_id=None, topic_name=target_topic, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) self.assert_has_visibility_policy( hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( shiva, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( hamlet, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( aaron, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED ) self.assert_has_visibility_policy( shiva, target_topic, stream, UserTopic.VisibilityPolicy.FOLLOWED ) # Test the following cases: # # orig_topic | target_topic | final behaviour # MUTED INHERIT INHERIT # MUTED MUTED MUTED # MUTED UNMUTED UNMUTED # MUTED FOLLOWED FOLLOWED orig_topic = "Topic2" target_topic = "Topic2 edited" orig_message_id = self.send_stream_message( hamlet, stream_name, topic_name=orig_topic, content="Hello World" ) self.send_stream_message( hamlet, stream_name, topic_name=target_topic, content="Hello World 2" ) do_set_user_topic_visibility_policy( hamlet, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( cordelia, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( aaron, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( shiva, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( aaron, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( shiva, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED ) check_update_message( user_profile=hamlet, message_id=orig_message_id, stream_id=None, topic_name=target_topic, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) self.assert_has_visibility_policy( hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( shiva, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( hamlet, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, target_topic, stream, UserTopic.VisibilityPolicy.MUTED ) self.assert_has_visibility_policy( aaron, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED ) self.assert_has_visibility_policy( shiva, target_topic, stream, UserTopic.VisibilityPolicy.FOLLOWED ) # Test the following cases: # # orig_topic | target_topic | final behaviour # UNMUTED INHERIT UNMUTED # UNMUTED MUTED UNMUTED # UNMUTED UNMUTED UNMUTED # UNMUTED FOLLOWED FOLLOWED orig_topic = "Topic3" target_topic = "Topic3 edited" orig_message_id = self.send_stream_message( hamlet, stream_name, topic_name=orig_topic, content="Hello World" ) self.send_stream_message( hamlet, stream_name, topic_name=target_topic, content="Hello World 2" ) do_set_user_topic_visibility_policy( hamlet, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( cordelia, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( aaron, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( shiva, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( aaron, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( shiva, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED ) check_update_message( user_profile=hamlet, message_id=orig_message_id, stream_id=None, topic_name=target_topic, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) self.assert_has_visibility_policy( hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( shiva, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( hamlet, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED ) self.assert_has_visibility_policy( cordelia, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED ) self.assert_has_visibility_policy( aaron, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED ) self.assert_has_visibility_policy( shiva, target_topic, stream, UserTopic.VisibilityPolicy.FOLLOWED ) # Test the following cases: # # orig_topic | target_topic | final behaviour # FOLLOWED INHERIT FOLLOWED # FOLLOWED MUTED FOLLOWED # FOLLOWED UNMUTED FOLLOWED orig_topic = "Topic4" target_topic = "Topic4 edited" orig_message_id = self.send_stream_message( hamlet, stream_name, topic_name=orig_topic, content="Hello World" ) self.send_stream_message( hamlet, stream_name, topic_name=target_topic, content="Hello World 2" ) do_set_user_topic_visibility_policy( cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED ) do_set_user_topic_visibility_policy( hamlet, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED ) do_set_user_topic_visibility_policy( cordelia, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED ) do_set_user_topic_visibility_policy( cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) do_set_user_topic_visibility_policy( aaron, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED ) do_set_user_topic_visibility_policy( aaron, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) check_update_message( user_profile=hamlet, message_id=orig_message_id, stream_id=None, topic_name=target_topic, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) self.assert_has_visibility_policy( hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( hamlet, target_topic, stream, UserTopic.VisibilityPolicy.FOLLOWED ) self.assert_has_visibility_policy( cordelia, target_topic, stream, UserTopic.VisibilityPolicy.FOLLOWED ) self.assert_has_visibility_policy( aaron, target_topic, stream, UserTopic.VisibilityPolicy.FOLLOWED ) def test_user_topic_states_on_moving_to_topic_with_no_messages(self) -> None: stream_name = "Stream 123" stream = self.make_stream(stream_name) hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") aaron = self.example_user("aaron") self.subscribe(hamlet, stream_name) self.subscribe(cordelia, stream_name) self.subscribe(aaron, stream_name) # Test the case where target topic has no messages: # # orig_topic | final behaviour # INHERIT INHERIT # UNMUTED UNMUTED # MUTED MUTED orig_topic = "Topic1" target_topic = "Topic1 edited" orig_message_id = self.send_stream_message( hamlet, stream_name, topic_name=orig_topic, content="Hello World" ) do_set_user_topic_visibility_policy( hamlet, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( cordelia, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) check_update_message( user_profile=hamlet, message_id=orig_message_id, stream_id=None, topic_name=target_topic, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) self.assert_has_visibility_policy( hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( hamlet, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED ) self.assert_has_visibility_policy( cordelia, target_topic, stream, UserTopic.VisibilityPolicy.MUTED ) self.assert_has_visibility_policy( aaron, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) def test_user_topic_state_for_messages_deleted_from_target_topic( orig_topic: str, target_topic: str, original_topic_state: int ) -> None: # Test the case where target topic has no messages but has UserTopic row # due to messages being deleted from the target topic. orig_message_id = self.send_stream_message( hamlet, stream_name, topic_name=orig_topic, content="Hello World" ) target_message_id = self.send_stream_message( hamlet, stream_name, topic_name=target_topic, content="Hello World" ) if original_topic_state != UserTopic.VisibilityPolicy.INHERIT: users = [hamlet, cordelia, aaron] for user in users: do_set_user_topic_visibility_policy( user, stream, orig_topic, visibility_policy=original_topic_state ) do_set_user_topic_visibility_policy( hamlet, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED ) do_set_user_topic_visibility_policy( cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED ) # Delete the message in target topic to make it empty. self.login("hamlet") members_system_group = NamedUserGroup.objects.get( name=SystemGroups.MEMBERS, realm=hamlet.realm, is_system_group=True ) do_change_realm_permission_group_setting( hamlet.realm, "can_delete_own_message_group", members_system_group, acting_user=None, ) self.client_delete(f"/json/messages/{target_message_id}") check_update_message( user_profile=hamlet, message_id=orig_message_id, stream_id=None, topic_name=target_topic, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) self.assert_has_visibility_policy( hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy( aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT ) self.assert_has_visibility_policy(hamlet, target_topic, stream, original_topic_state) self.assert_has_visibility_policy(cordelia, target_topic, stream, original_topic_state) self.assert_has_visibility_policy(aaron, target_topic, stream, original_topic_state) # orig_topic | target_topic | final behaviour # INHERIT INHERIT INHERIT # INHERIT UNMUTED INHERIT # INHERIT MUTED INHERIT test_user_topic_state_for_messages_deleted_from_target_topic( orig_topic="Topic2", target_topic="Topic2 edited", original_topic_state=UserTopic.VisibilityPolicy.INHERIT, ) # orig_topic | target_topic | final behaviour # MUTED INHERIT MUTED # MUTED UNMUTED MUTED # MUTED MUTED MUTED test_user_topic_state_for_messages_deleted_from_target_topic( orig_topic="Topic3", target_topic="Topic3 edited", original_topic_state=UserTopic.VisibilityPolicy.MUTED, ) # orig_topic | target_topic | final behaviour # UNMUTED INHERIT UNMUTED # UNMUTED UNMUTED UNMUTED # UNMUTED MUTED UNMUTED test_user_topic_state_for_messages_deleted_from_target_topic( orig_topic="Topic4", target_topic="Topic4 edited", original_topic_state=UserTopic.VisibilityPolicy.UNMUTED, ) def test_topic_edit_history_saved_in_all_message(self) -> None: self.login("hamlet") id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1") id2 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1") id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topic1") id4 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic2") id5 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1") def verify_edit_history(new_topic_name: str, len_edit_history: int) -> None: for msg_id in [id1, id2, id5]: msg = Message.objects.get(id=msg_id) self.assertEqual( new_topic_name, msg.topic_name(), ) # Since edit history is being generated by do_update_message, # it's contents can vary over time; So, to keep this test # future proof, we only verify it's length. self.assert_length( orjson.loads(assert_is_not_none(msg.edit_history)), len_edit_history ) for msg_id in [id3, id4]: msg = Message.objects.get(id=msg_id) self.assertEqual(msg.edit_history, None) new_topic_name = "edited" result = self.client_patch( f"/json/messages/{id1}", { "topic": new_topic_name, "propagate_mode": "change_later", }, ) self.assert_json_success(result) verify_edit_history(new_topic_name, 1) new_topic_name = "edited2" result = self.client_patch( f"/json/messages/{id1}", { "topic": new_topic_name, "propagate_mode": "change_later", }, ) self.assert_json_success(result) verify_edit_history(new_topic_name, 2) def test_topic_and_content_edit(self) -> None: self.login("hamlet") id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", "message 1", "topic") id2 = self.send_stream_message(self.example_user("iago"), "Denmark", "message 2", "topic") id3 = self.send_stream_message(self.example_user("hamlet"), "Denmark", "message 3", "topic") new_topic_name = "edited" result = self.client_patch( "/json/messages/" + str(id1), { "topic": new_topic_name, "propagate_mode": "change_later", "content": "edited message", }, ) self.assert_json_success(result) # Content change of only id1 should come in edit history # and topic change should be present in all the messages. msg1 = Message.objects.get(id=id1) msg2 = Message.objects.get(id=id2) msg3 = Message.objects.get(id=id3) msg1_edit_history = orjson.loads(assert_is_not_none(msg1.edit_history)) self.assertTrue("prev_content" in msg1_edit_history[0]) for msg in [msg2, msg3]: self.assertFalse( "prev_content" in orjson.loads(assert_is_not_none(msg.edit_history))[0] ) for msg in [msg1, msg2, msg3]: self.assertEqual( new_topic_name, msg.topic_name(), ) self.assert_length(orjson.loads(assert_is_not_none(msg.edit_history)), 1) def test_propagate_topic_forward(self) -> None: self.login("hamlet") id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1") id2 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1") id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topic1") id4 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic2") id5 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1") result = self.client_patch( f"/json/messages/{id1}", { "topic": "edited", "propagate_mode": "change_later", }, ) self.assert_json_success(result) self.check_topic(id1, topic_name="edited") self.check_topic(id2, topic_name="edited") self.check_topic(id3, topic_name="topic1") self.check_topic(id4, topic_name="topic2") self.check_topic(id5, topic_name="edited") def test_propagate_all_topics(self) -> None: self.login("hamlet") id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1") id2 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1") id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topic1") id4 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic2") id5 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1") id6 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic3") result = self.client_patch( f"/json/messages/{id2}", { "topic": "edited", "propagate_mode": "change_all", }, ) self.assert_json_success(result) self.check_topic(id1, topic_name="edited") self.check_topic(id2, topic_name="edited") self.check_topic(id3, topic_name="topic1") self.check_topic(id4, topic_name="topic2") self.check_topic(id5, topic_name="edited") self.check_topic(id6, topic_name="topic3") def test_propagate_all_topics_with_different_uppercase_letters(self) -> None: self.login("hamlet") id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1") id2 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="Topic1") id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topiC1") id4 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="toPic1") result = self.client_patch( f"/json/messages/{id2}", { "topic": "edited", "propagate_mode": "change_all", }, ) self.assert_json_success(result) self.check_topic(id1, topic_name="edited") self.check_topic(id2, topic_name="edited") self.check_topic(id3, topic_name="topiC1") self.check_topic(id4, topic_name="edited") def test_change_all_propagate_mode_for_moving_from_stream_with_restricted_history(self) -> None: self.make_stream("privatestream", invite_only=True, history_public_to_subscribers=False) iago = self.example_user("iago") cordelia = self.example_user("cordelia") self.subscribe(iago, "privatestream") self.subscribe(cordelia, "privatestream") id1 = self.send_stream_message(iago, "privatestream", topic_name="topic1") id2 = self.send_stream_message(iago, "privatestream", topic_name="topic1") hamlet = self.example_user("hamlet") self.subscribe(hamlet, "privatestream") id3 = self.send_stream_message(iago, "privatestream", topic_name="topic1") id4 = self.send_stream_message(hamlet, "privatestream", topic_name="topic1") self.send_stream_message(hamlet, "privatestream", topic_name="topic1") message = Message.objects.get(id=id1) message.date_sent -= timedelta(days=10) message.save() message = Message.objects.get(id=id2) message.date_sent -= timedelta(days=9) message.save() message = Message.objects.get(id=id3) message.date_sent -= timedelta(days=8) message.save() message = Message.objects.get(id=id4) message.date_sent -= timedelta(days=6) message.save() self.login("hamlet") result = self.client_patch( f"/json/messages/{id4}", { "topic": "edited", "propagate_mode": "change_all", "send_notification_to_new_thread": "false", }, ) self.assert_json_error( result, "You only have permission to move the 2/3 most recent messages in this topic.", ) self.login("cordelia") result = self.client_patch( f"/json/messages/{id4}", { "topic": "edited", "propagate_mode": "change_all", "send_notification_to_new_thread": "false", }, ) self.assert_json_error( result, "You only have permission to move the 2/5 most recent messages in this topic.", ) def test_notify_new_topic(self) -> None: user_profile = self.example_user("iago") self.login("iago") stream = self.make_stream("public stream") self.subscribe(user_profile, stream.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name="test", content="First" ) self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second") self.send_stream_message(user_profile, stream.name, topic_name="test", content="third") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "edited", "propagate_mode": "change_all", "send_notification_to_old_thread": "false", "send_notification_to_new_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user_profile, stream, "edited") self.assert_length(messages, 4) self.assertEqual( messages[3].content, f"This topic was moved here from #**public stream>test** by @_**Iago|{user_profile.id}**.", ) def test_notify_old_topic(self) -> None: user_profile = self.example_user("iago") self.login("iago") stream = self.make_stream("public stream") self.subscribe(user_profile, stream.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name="test", content="First" ) self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second") self.send_stream_message(user_profile, stream.name, topic_name="test", content="third") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "edited", "propagate_mode": "change_all", "send_notification_to_old_thread": "true", "send_notification_to_new_thread": "false", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**public stream>edited** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, stream, "edited") self.assert_length(messages, 3) def test_notify_both_topics(self) -> None: user_profile = self.example_user("iago") self.login("iago") stream = self.make_stream("public stream") self.subscribe(user_profile, stream.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name="test", content="First" ) self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second") self.send_stream_message(user_profile, stream.name, topic_name="test", content="third") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "edited", "propagate_mode": "change_all", "send_notification_to_old_thread": "true", "send_notification_to_new_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**public stream>edited** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, stream, "edited") self.assert_length(messages, 4) self.assertEqual( messages[3].content, f"This topic was moved here from #**public stream>test** by @_**Iago|{user_profile.id}**.", ) def test_notify_no_topic(self) -> None: user_profile = self.example_user("iago") self.login("iago") stream = self.make_stream("public stream") self.subscribe(user_profile, stream.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name="test", content="First" ) self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second") self.send_stream_message(user_profile, stream.name, topic_name="test", content="third") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "edited", "propagate_mode": "change_all", "send_notification_to_old_thread": "false", "send_notification_to_new_thread": "false", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user_profile, stream, "edited") self.assert_length(messages, 3) def test_notify_old_topics_after_message_move(self) -> None: user_profile = self.example_user("iago") self.login("iago") stream = self.make_stream("public stream") self.subscribe(user_profile, stream.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name="test", content="First" ) self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second") self.send_stream_message(user_profile, stream.name, topic_name="test", content="Third") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "edited", "propagate_mode": "change_one", "send_notification_to_old_thread": "true", "send_notification_to_new_thread": "false", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, "test") self.assert_length(messages, 3) self.assertEqual(messages[0].content, "Second") self.assertEqual(messages[1].content, "Third") self.assertEqual( messages[2].content, f"A message was moved from this topic to #**public stream>edited** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, stream, "edited") self.assert_length(messages, 1) self.assertEqual(messages[0].content, "First") def test_notify_no_topic_after_message_move(self) -> None: user_profile = self.example_user("iago") self.login("iago") stream = self.make_stream("public stream") self.subscribe(user_profile, stream.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name="test", content="First" ) self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second") self.send_stream_message(user_profile, stream.name, topic_name="test", content="Third") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": "edited", "propagate_mode": "change_one", "send_notification_to_old_thread": "false", "send_notification_to_new_thread": "false", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, "test") self.assert_length(messages, 2) self.assertEqual(messages[0].content, "Second") self.assertEqual(messages[1].content, "Third") messages = get_topic_messages(user_profile, stream, "edited") self.assert_length(messages, 1) self.assertEqual(messages[0].content, "First") def test_notify_resolve_topic_long_name(self) -> None: user_profile = self.example_user("hamlet") self.login("hamlet") stream = self.make_stream("public stream") self.subscribe(user_profile, stream.name) # Marking topics with a long name as resolved causes the new topic name to be truncated. # We want to avoid having code paths believing that the topic is "moved" instead of # "resolved" in this edge case. topic_name = "a" * MAX_TOPIC_NAME_LENGTH msg_id = self.send_stream_message( user_profile, stream.name, topic_name=topic_name, content="First" ) resolved_topic = RESOLVED_TOPIC_PREFIX + topic_name result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": resolved_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) new_topic_name = truncate_topic(resolved_topic) messages = get_topic_messages(user_profile, stream, new_topic_name) self.assert_length(messages, 2) self.assertEqual(messages[0].content, "First") self.assertEqual( messages[1].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as resolved.", ) message_fetch_result = self.client_get( f"/json/messages/{msg_id}", ) self.assert_json_success(message_fetch_result) message_dict = orjson.loads(message_fetch_result.content)["message"] self.assert_json_success(result) self.assertNotIn("last_moved_timestamp", message_dict) # Note that we are removing the prefix from the already truncated topic, # so unresolved_topic_name will not be the same as the original topic_name unresolved_topic_name = new_topic_name.replace(RESOLVED_TOPIC_PREFIX, "") result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": unresolved_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, unresolved_topic_name) self.assert_length(messages, 3) self.assertEqual( messages[2].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as unresolved.", ) message_fetch_result = self.client_get( f"/json/messages/{msg_id}", ) self.assert_json_success(message_fetch_result) message_dict = orjson.loads(message_fetch_result.content)["message"] self.assert_json_success(result) self.assertNotIn("last_moved_timestamp", message_dict) def test_notify_resolve_and_move_topic(self) -> None: user_profile = self.example_user("hamlet") self.login("hamlet") stream = self.make_stream("public stream") topic_name = "test" self.subscribe(user_profile, stream.name) # Resolve a topic normally first time_zero = timezone_now().replace(microsecond=0) with time_machine.travel(time_zero, tick=False): msg_id = self.send_stream_message( user_profile, stream.name, "foo", topic_name=topic_name ) resolved_topic_name = RESOLVED_TOPIC_PREFIX + topic_name first_message_move_time = time_zero + timedelta(seconds=2) with time_machine.travel(first_message_move_time, tick=False): result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": resolved_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, resolved_topic_name) self.assert_length(messages, 2) self.assertEqual( messages[1].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as resolved.", ) message_fetch_result = self.client_get( f"/json/messages/{msg_id}", ) self.assert_json_success(message_fetch_result) message_dict = orjson.loads(message_fetch_result.content)["message"] self.assert_json_success(result) self.assertNotIn("last_moved_timestamp", message_dict) # Test unresolving a topic while moving it (✔ test -> bar) new_topic_name = "bar" second_message_move_time = time_zero + timedelta(seconds=4) with time_machine.travel(second_message_move_time, tick=False): result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": new_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, new_topic_name) self.assert_length(messages, 3) self.assertEqual( messages[2].content, f"This topic was moved here from #**public stream>✔ test** by @_**{user_profile.full_name}|{user_profile.id}**.", ) message_fetch_result = self.client_get( f"/json/messages/{msg_id}", ) self.assert_json_success(message_fetch_result) message_dict = orjson.loads(message_fetch_result.content)["message"] self.assert_json_success(result) self.assertEqual( message_dict["last_moved_timestamp"], datetime_to_timestamp(second_message_move_time), ) # Now test moving the topic while also resolving it (bar -> ✔ baz) new_resolved_topic_name = RESOLVED_TOPIC_PREFIX + "baz" third_message_move_time = time_zero + timedelta(seconds=6) with time_machine.travel(third_message_move_time, tick=False): result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": new_resolved_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, new_resolved_topic_name) self.assert_length(messages, 4) self.assertEqual( messages[3].content, f"This topic was moved here from #**public stream>{new_topic_name}** by @_**{user_profile.full_name}|{user_profile.id}**.", ) message_fetch_result = self.client_get( f"/json/messages/{msg_id}", ) self.assert_json_success(message_fetch_result) message_dict = orjson.loads(message_fetch_result.content)["message"] self.assert_json_success(result) self.assertEqual( message_dict["last_moved_timestamp"], datetime_to_timestamp(third_message_move_time), ) def test_mark_topic_as_resolved(self) -> None: self.login("iago") admin_user = self.example_user("iago") hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") aaron = self.example_user("aaron") # Set the user's translation language to German to test that # it is overridden by the realm's default language. admin_user.default_language = "de" admin_user.save() stream = self.make_stream("new") self.subscribe(admin_user, stream.name) self.subscribe(hamlet, stream.name) self.subscribe(cordelia, stream.name) self.subscribe(aaron, stream.name) original_topic_name = "topic 1" id1 = self.send_stream_message(hamlet, "new", topic_name=original_topic_name) id2 = self.send_stream_message(admin_user, "new", topic_name=original_topic_name) msg1 = Message.objects.get(id=id1) do_add_reaction(aaron, msg1, "tada", "1f389", "unicode_emoji") # Check that we don't incorrectly send "unresolve topic" # notifications when asking the preserve the current topic. result = self.client_patch( "/json/messages/" + str(id1), { "topic": original_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_error(result, "Nothing to change") resolved_topic_name = RESOLVED_TOPIC_PREFIX + original_topic_name result = self.resolve_topic_containing_message( admin_user, id1, HTTP_ACCEPT_LANGUAGE="de", ) self.assert_json_success(result) for msg_id in [id1, id2]: msg = Message.objects.get(id=msg_id) self.assertEqual( resolved_topic_name, msg.topic_name(), ) messages = get_topic_messages(admin_user, stream, resolved_topic_name) self.assert_length(messages, 3) self.assertEqual( messages[2].content, f"@_**Iago|{admin_user.id}** has marked this topic as resolved.", ) # Now move to a weird state and confirm we get the normal topic moved message. weird_topic_name = "✔ ✔✔" + original_topic_name result = self.client_patch( "/json/messages/" + str(id1), { "topic": weird_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result) for msg_id in [id1, id2]: msg = Message.objects.get(id=msg_id) self.assertEqual( weird_topic_name, msg.topic_name(), ) messages = get_topic_messages(admin_user, stream, weird_topic_name) self.assert_length(messages, 4) self.assertEqual( messages[2].content, f"@_**Iago|{admin_user.id}** has marked this topic as resolved.", ) self.assertEqual( messages[3].content, f"This topic was moved here from #**new>✔ topic 1** by @_**Iago|{admin_user.id}**.", ) unresolved_topic_name = original_topic_name result = self.client_patch( "/json/messages/" + str(id1), { "topic": unresolved_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result) for msg_id in [id1, id2]: msg = Message.objects.get(id=msg_id) self.assertEqual( unresolved_topic_name, msg.topic_name(), ) messages = get_topic_messages(admin_user, stream, unresolved_topic_name) self.assert_length(messages, 5) self.assertEqual( messages[2].content, f"@_**Iago|{admin_user.id}** has marked this topic as resolved." ) self.assertEqual( messages[4].content, f"@_**Iago|{admin_user.id}** has marked this topic as unresolved.", ) def test_resolved_topic_notice_auto_read_policy(self) -> None: # Test that resolved and unresolved-topic notices are marked as # read/unread based on the 'resolved_topic_notice_auto_read_policy' # setting for followed and unfollowed topics. self.login("iago") admin_user = self.example_user("iago") aaron = self.example_user("aaron") cordelia = self.example_user("cordelia") hamlet = self.example_user("hamlet") do_change_user_setting( aaron, "resolved_topic_notice_auto_read_policy", ResolvedTopicNoticeAutoReadPolicyEnum.always, acting_user=None, ) do_change_user_setting( cordelia, "resolved_topic_notice_auto_read_policy", ResolvedTopicNoticeAutoReadPolicyEnum.except_followed, acting_user=None, ) do_change_user_setting( hamlet, "resolved_topic_notice_auto_read_policy", ResolvedTopicNoticeAutoReadPolicyEnum.never, acting_user=None, ) stream = self.make_stream("stream") self.subscribe(admin_user, stream.name) self.subscribe(aaron, stream.name) self.subscribe(cordelia, stream.name) self.subscribe(hamlet, stream.name) followed_topic_name = "followed" msg_id_1 = self.send_stream_message(admin_user, "stream", topic_name=followed_topic_name) do_set_user_topic_visibility_policy( aaron, stream, followed_topic_name, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, ) do_set_user_topic_visibility_policy( cordelia, stream, followed_topic_name, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, ) do_set_user_topic_visibility_policy( hamlet, stream, followed_topic_name, visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, ) resolved_followed_topic_name = RESOLVED_TOPIC_PREFIX + followed_topic_name result_1 = self.resolve_topic_containing_message( admin_user, msg_id_1, ) self.assert_json_success(result_1) msg_1 = Message.objects.get(id=msg_id_1) self.assertEqual(resolved_followed_topic_name, msg_1.topic_name()) messages = get_topic_messages(admin_user, stream, resolved_followed_topic_name) self.assert_length(messages, 2) unread_user_ids = self.get_user_ids_for_whom_message_unread(messages[1].id) read_user_ids = self.get_user_ids_for_whom_message_read(messages[1].id) # For the resolved-topic notice in a "followed" topic: # - aaron: policy 'always' -> notice is marked as read. # - cordelia: policy 'except_followed' and is following -> notice remains unread. # - hamlet: policy 'never' -> notice is remains unread. # - admin_user: is the one who resolved the topic -> notice is marked as read. self.assertEqual(unread_user_ids, {hamlet.id, cordelia.id}) self.assertEqual(read_user_ids, {aaron.id, admin_user.id}) unfollowed_topic_name = "unfollowed" msg_id_2 = self.send_stream_message(admin_user, "stream", topic_name=unfollowed_topic_name) resolved_unfollowed_topic_name = RESOLVED_TOPIC_PREFIX + unfollowed_topic_name result_2 = self.resolve_topic_containing_message( admin_user, msg_id_2, ) self.assert_json_success(result_2) msg_2 = Message.objects.get(id=msg_id_2) self.assertEqual(resolved_unfollowed_topic_name, msg_2.topic_name()) messages = get_topic_messages(admin_user, stream, resolved_unfollowed_topic_name) self.assert_length(messages, 2) unread_user_ids = self.get_user_ids_for_whom_message_unread(messages[1].id) read_user_ids = self.get_user_ids_for_whom_message_read(messages[1].id) # For the resolved-topic notice in an "unfollowed" topic: # - aaron: policy 'always' -> notice is marked as read. # - cordelia: policy 'except_followed' but not following -> notice is marked as read. # - hamlet: policy 'never' -> notice remains unread. # - admin_user: is the one who resolved the topic -> notice is marked as read. self.assertEqual(unread_user_ids, {hamlet.id}) self.assertEqual(read_user_ids, {aaron.id, cordelia.id, admin_user.id}) result_3 = self.client_patch( "/json/messages/" + str(msg_id_1), { "topic": followed_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result_3) msg_3 = Message.objects.get(id=msg_id_1) self.assertEqual(followed_topic_name, msg_3.topic_name()) messages = get_topic_messages(admin_user, stream, followed_topic_name) self.assert_length(messages, 3) unread_user_ids = self.get_user_ids_for_whom_message_unread(messages[2].id) read_user_ids = self.get_user_ids_for_whom_message_read(messages[2].id) # Unresolved-topic notices in a "followed" topic # behave similar to resolved-topic notices. self.assertEqual(unread_user_ids, {hamlet.id, cordelia.id}) self.assertEqual(read_user_ids, {aaron.id, admin_user.id}) result_4 = self.client_patch( "/json/messages/" + str(msg_id_2), { "topic": unfollowed_topic_name, "propagate_mode": "change_all", }, ) self.assert_json_success(result_4) msg_4 = Message.objects.get(id=msg_id_2) self.assertEqual(unfollowed_topic_name, msg_4.topic_name()) messages = get_topic_messages(admin_user, stream, unfollowed_topic_name) self.assert_length(messages, 3) unread_user_ids = self.get_user_ids_for_whom_message_unread(messages[2].id) read_user_ids = self.get_user_ids_for_whom_message_read(messages[2].id) # Unresolved-topic notices in an "unfollowed" topic # behave similar to resolved-topic notices. self.assertEqual(unread_user_ids, {hamlet.id}) self.assertEqual(read_user_ids, {aaron.id, cordelia.id, admin_user.id}) @override_settings(RESOLVE_TOPIC_UNDO_GRACE_PERIOD_SECONDS=60) def test_mark_topic_as_resolved_within_grace_period(self) -> None: self.login("iago") admin_user = self.example_user("iago") hamlet = self.example_user("hamlet") stream = self.make_stream("new") self.subscribe(admin_user, stream.name) self.subscribe(hamlet, stream.name) original_topic = "topic 1" id1 = self.send_stream_message( hamlet, "new", content="message 1", topic_name=original_topic ) id2 = self.send_stream_message( admin_user, "new", content="message 2", topic_name=original_topic ) resolved_topic = RESOLVED_TOPIC_PREFIX + original_topic start_time = timezone_now() with time_machine.travel(start_time, tick=False): result = self.client_patch( "/json/messages/" + str(id1), { "topic": resolved_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) for msg_id in [id1, id2]: msg = Message.objects.get(id=msg_id) self.assertEqual( resolved_topic, msg.topic_name(), ) messages = get_topic_messages(admin_user, stream, resolved_topic) self.assert_length(messages, 3) self.assertEqual( messages[2].content, f"@_**Iago|{admin_user.id}** has marked this topic as resolved.", ) unresolved_topic = original_topic # Now unresolve the topic within the grace period. with time_machine.travel(start_time + timedelta(seconds=30), tick=False): result = self.client_patch( "/json/messages/" + str(id1), { "topic": unresolved_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) for msg_id in [id1, id2]: msg = Message.objects.get(id=msg_id) self.assertEqual( unresolved_topic, msg.topic_name(), ) messages = get_topic_messages(admin_user, stream, unresolved_topic) # The message about the topic having been resolved is gone. self.assert_length(messages, 2) self.assertEqual( messages[1].content, "message 2", ) self.assertEqual(messages[0].content, "message 1") # Now resolve the topic again after the grace period with time_machine.travel(start_time + timedelta(seconds=61), tick=False): result = self.client_patch( "/json/messages/" + str(id1), { "topic": resolved_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) for msg_id in [id1, id2]: msg = Message.objects.get(id=msg_id) self.assertEqual( resolved_topic, msg.topic_name(), ) messages = get_topic_messages(admin_user, stream, resolved_topic) self.assert_length(messages, 3) self.assertEqual( messages[2].content, f"@_**Iago|{admin_user.id}** has marked this topic as resolved.", ) def test_send_resolve_topic_notification_with_no_topic_messages(self) -> None: self.login("iago") admin_user = self.example_user("iago") hamlet = self.example_user("hamlet") stream = self.make_stream("new") self.subscribe(admin_user, stream.name) self.subscribe(hamlet, stream.name) original_topic = "topic 1" message_id = self.send_stream_message( hamlet, "new", content="message 1", topic_name=original_topic ) message = Message.objects.get(id=message_id) do_delete_messages(admin_user.realm, [message], acting_user=None) assert stream.recipient_id is not None resolve_topic = RESOLVED_TOPIC_PREFIX + original_topic message_edit_request = build_message_edit_request( message=message, user_profile=admin_user, propagate_mode="change_all", stream_id=None, topic_name=resolve_topic, content=None, ) assert isinstance(message_edit_request, StreamMessageEditRequest) maybe_send_resolve_topic_notifications( user_profile=admin_user, message_edit_request=message_edit_request, users_following_topic=None, ) topic_messages = get_topic_messages(admin_user, stream, resolve_topic) self.assert_length(topic_messages, 1) self.assertEqual( topic_messages[0].content, f"@_**Iago|{admin_user.id}** has marked this topic as resolved.", ) def test_resolve_empty_string_topic(self) -> None: hamlet = self.example_user("hamlet") message_id = self.send_stream_message(hamlet, "Denmark", topic_name="") result = self.resolve_topic_containing_message(hamlet, target_message_id=message_id) self.assert_json_error(result, "General chat cannot be marked as resolved") # Verification for old clients that don't support empty string topic. message_id = self.send_stream_message( hamlet, "Denmark", topic_name=Message.EMPTY_TOPIC_FALLBACK_NAME ) result = self.resolve_topic_containing_message(hamlet, target_message_id=message_id) self.assert_json_error(result, "General chat cannot be marked as resolved") def test_resolved_topic_realm_level_permissions(self) -> None: self.login("iago") admin_user = self.example_user("iago") hamlet = self.example_user("hamlet") othello = self.example_user("othello") cordelia = self.example_user("cordelia") admin_user.default_language = "de" admin_user.save() stream = self.make_stream("new") self.subscribe(admin_user, stream.name) self.subscribe(hamlet, stream.name) original_topic_name = "topic 1" id1 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) # Test resolving topics disabled by organization nobody_group = NamedUserGroup.objects.get( name=SystemGroups.NOBODY, realm=admin_user.realm, is_system_group=True ) do_change_realm_permission_group_setting( admin_user.realm, "can_resolve_topics_group", nobody_group, acting_user=None, ) result = self.resolve_topic_containing_message( admin_user, id1, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) # Test restrict resolving topics to admins only. admins_group = NamedUserGroup.objects.get( name=SystemGroups.ADMINISTRATORS, realm=admin_user.realm, is_system_group=True ) do_change_realm_permission_group_setting( admin_user.realm, "can_resolve_topics_group", admins_group, acting_user=None, ) result = self.resolve_topic_containing_message( hamlet, id1, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( admin_user, id1, ) self.assert_json_success(result) # Test restrict resolving topics to a user defined group. original_topic_name = "topic 2" id2 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) leadership_group = check_add_user_group( admin_user.realm, "leadership", [hamlet], acting_user=hamlet ) do_change_realm_permission_group_setting( admin_user.realm, "can_resolve_topics_group", leadership_group, acting_user=None ) result = self.resolve_topic_containing_message( othello, id2, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( admin_user, id2, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( hamlet, id2, ) self.assert_json_success(result) # Test restrict topics to an anonymous group. original_topic_name = "topic 3" id3 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) staff_group = check_add_user_group( admin_user.realm, "Staff", [admin_user], acting_user=admin_user ) anonymous_setting_group = self.create_or_update_anonymous_group_for_setting( [cordelia], [staff_group] ) do_change_realm_permission_group_setting( admin_user.realm, "can_resolve_topics_group", anonymous_setting_group, acting_user=None ) result = self.resolve_topic_containing_message( othello, id3, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( cordelia, id3, ) self.assert_json_success(result) id3 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) result = self.resolve_topic_containing_message( admin_user, id3, ) self.assert_json_success(result) # Test resolving topics when there is no permission to move topics. do_change_realm_permission_group_setting( admin_user.realm, "can_move_messages_between_topics_group", nobody_group, acting_user=None, ) original_topic_name = "topic 4" id4 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) # Do not allow if there is some change other than adding # RESOLVED_TOPIC_PREFIX self.login_user(cordelia) result = self.client_patch( "/json/messages/" + str(id4), { "topic": RESOLVED_TOPIC_PREFIX + "topic 45", "propagate_mode": "change_all", }, ) self.assert_json_error(result, "You don't have permission to edit this message") result = self.resolve_topic_containing_message( cordelia, id4, ) self.assert_json_success(result) # Test resolving topics when time limit for moving messages between # topics has passed. do_change_realm_permission_group_setting( admin_user.realm, "can_move_messages_between_topics_group", anonymous_setting_group, acting_user=None, ) do_set_realm_property( admin_user.realm, "move_messages_within_stream_limit_seconds", 3600, acting_user=None ) original_topic_name = "topic 5" id5 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) message = Message.objects.get(id=id5) message.date_sent -= timedelta(seconds=4000) message.save() # Do not allow if there is some change other than adding # RESOLVED_TOPIC_PREFIX self.login_user(cordelia) result = self.client_patch( "/json/messages/" + str(id5), { "topic": RESOLVED_TOPIC_PREFIX + "topic 56", "propagate_mode": "change_all", }, ) self.assert_json_error( result, "The time limit for editing this message's topic has passed." ) result = self.resolve_topic_containing_message( cordelia, id5, ) self.assert_json_success(result) def test_resolved_topic_channel_level_permissions(self) -> None: admin_user = self.example_user("iago") hamlet = self.example_user("hamlet") othello = self.example_user("othello") stream = self.make_stream("new") self.subscribe(admin_user, stream.name) self.subscribe(hamlet, stream.name) # Set resolving topics disabled by organization nobody_group = NamedUserGroup.objects.get( name=SystemGroups.NOBODY, realm=admin_user.realm, is_system_group=True ) do_change_realm_permission_group_setting( admin_user.realm, "can_resolve_topics_group", nobody_group, acting_user=None, ) # Test resolving topics disabled in a particular channel. original_topic_name = "topic 1" id1 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) do_change_stream_group_based_setting( stream, "can_resolve_topics_group", nobody_group, acting_user=admin_user ) result = self.resolve_topic_containing_message( admin_user, id1, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) # Test restrict resolving topics to admins only in a particular channel. admins_group = NamedUserGroup.objects.get( name=SystemGroups.ADMINISTRATORS, realm=admin_user.realm, is_system_group=True ) do_change_stream_group_based_setting( stream, "can_resolve_topics_group", admins_group, acting_user=admin_user ) result = self.resolve_topic_containing_message( hamlet, id1, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( admin_user, id1, ) self.assert_json_success(result) # Test restrict resolving topics to a user defined group in a particular channel. original_topic_name = "topic 2" id2 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) leadership_group = check_add_user_group( admin_user.realm, "leadership", [hamlet], acting_user=hamlet ) do_change_stream_group_based_setting( stream, "can_resolve_topics_group", leadership_group, acting_user=admin_user ) result = self.resolve_topic_containing_message( othello, id2, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( admin_user, id2, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( hamlet, id2, ) self.assert_json_success(result) # Test restrict topics to an anonymous group in a particular channel. original_topic_name = "topic 3" id3 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) othello_group_member_dict = UserGroupMembersData( direct_members=[othello.id], direct_subgroups=[] ) do_change_stream_group_based_setting( stream, "can_resolve_topics_group", othello_group_member_dict, acting_user=othello ) result = self.resolve_topic_containing_message( admin_user, id3, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) result = self.resolve_topic_containing_message( othello, id3, ) self.assert_json_success(result) # Test resolving topics while moving topics between different streams. # User can resolve the topic when moving it between streams if they have # permission to resolve topics in either the source or destination stream. original_topic_name = "topic 4" new_stream = self.make_stream("new stream") self.subscribe(hamlet, "new stream") id4 = self.send_stream_message(hamlet, new_stream.name, topic_name=original_topic_name) do_change_stream_group_based_setting( new_stream, "can_resolve_topics_group", nobody_group, acting_user=admin_user ) result = self.api_patch( admin_user, "/api/v1/messages/" + str(id4), {"topic": RESOLVED_TOPIC_PREFIX + original_topic_name, "stream_id": stream.id}, ) self.assert_json_error( result, "You don't have permission to resolve topics in this channel." ) # Othello can resolve the topic while moving it since he has permission to resolve topics # in the destination stream. result = self.api_patch( othello, "/api/v1/messages/" + str(id4), {"topic": RESOLVED_TOPIC_PREFIX + original_topic_name, "stream_id": stream.id}, ) self.assert_json_success(result) do_change_realm_permission_group_setting( admin_user.realm, "can_move_messages_between_topics_group", nobody_group, acting_user=None, ) original_topic_name = "topic 5" id5 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) # Do not allow if there is some change other than adding # RESOLVED_TOPIC_PREFIX result = self.api_patch( othello, "/api/v1/messages/" + str(id5), { "topic": RESOLVED_TOPIC_PREFIX + "topic 45", "propagate_mode": "change_all", }, ) self.assert_json_error(result, "You don't have permission to edit this message") result = self.resolve_topic_containing_message( othello, id5, ) self.assert_json_success(result) # Test resolving topics when time limit for moving messages between # topics has passed. do_change_realm_permission_group_setting( admin_user.realm, "can_move_messages_between_topics_group", leadership_group, acting_user=None, ) do_set_realm_property( admin_user.realm, "move_messages_within_stream_limit_seconds", 3600, acting_user=None ) do_change_stream_group_based_setting( stream, "can_resolve_topics_group", leadership_group, acting_user=admin_user ) original_topic_name = "topic 6" id6 = self.send_stream_message(hamlet, stream.name, topic_name=original_topic_name) message = Message.objects.get(id=id6) message.date_sent -= timedelta(seconds=4000) message.save() # Do not allow if there is some change other than adding # RESOLVED_TOPIC_PREFIX result = self.api_patch( hamlet, "/api/v1/messages/" + str(id6), { "topic": RESOLVED_TOPIC_PREFIX + "topic 56", "propagate_mode": "change_all", }, ) self.assert_json_error( result, "The time limit for editing this message's topic has passed." ) result = self.resolve_topic_containing_message( hamlet, id6, ) self.assert_json_success(result) def test_can_move_messages_within_channel_group(self) -> None: hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") iago = self.example_user("iago") realm = hamlet.realm members_system_group = NamedUserGroup.objects.get( name=SystemGroups.MEMBERS, realm=realm, is_system_group=True ) moderators_system_group = NamedUserGroup.objects.get( name=SystemGroups.MODERATORS, realm=realm, is_system_group=True ) nobody_system_group = NamedUserGroup.objects.get( name=SystemGroups.NOBODY, realm=realm, is_system_group=True ) expected_error = "You don't have permission to edit this message" do_change_realm_permission_group_setting( realm, "can_move_messages_between_topics_group", nobody_system_group, acting_user=None, ) do_change_realm_permission_group_setting( realm, "can_move_messages_between_channels_group", members_system_group, acting_user=None, ) stream_1 = get_stream("Denmark", realm) stream_2 = get_stream("Verona", realm) # Nobody is allowed to move messages. self.assert_move_message( "hamlet", stream_1, topic_name="new topic", expected_error=expected_error ) # Realm admin can always move messages within the channel. self.assert_move_message("iago", stream_1, topic_name="new topic") do_change_stream_group_based_setting( stream_1, "can_move_messages_within_channel_group", members_system_group, acting_user=iago, ) # Only members are allowed to move messages within the channel. self.assert_move_message("hamlet", stream_1, topic_name="new topic") self.assert_move_message("cordelia", stream_1, topic_name="new topic") # Guests are not allowed. self.assert_move_message( "polonius", stream_1, topic_name="new topic", expected_error=expected_error ) # Users are allowed to move messages to different topics out of the channel # if they are in `can_move_messages_within_channel_group` permission of either # the original channel or the destination channel. self.assert_move_message( "hamlet", stream_1, stream_id=stream_2.id, topic_name="new topic", ) self.assert_move_message( "hamlet", stream_2, stream_id=stream_1.id, topic_name="new topic", ) do_change_stream_group_based_setting( stream_1, "can_move_messages_within_channel_group", moderators_system_group, acting_user=iago, ) # Hamlet can't move message to another topic now as he is not in # the `can_move_messages_within_channel_group` of either of the channel. self.assert_move_message( "hamlet", stream_1, stream_id=stream_2.id, topic_name="new topic", expected_error=expected_error, ) # But he still can move messages between channel without changing the topic. self.assert_move_message( "hamlet", stream_1, stream_id=stream_2.id, ) user_group = check_add_user_group( realm, "new_group", [hamlet, cordelia], acting_user=hamlet ) do_change_stream_group_based_setting( stream_1, "can_move_messages_within_channel_group", user_group, acting_user=iago ) # Hamlet and Cordelia are in the `can_move_messages_within_channel_group`, # so they can move messages within the channel. self.assert_move_message("cordelia", stream_1, topic_name="new topic") self.assert_move_message("hamlet", stream_1, topic_name="new topic") # But Shiva is not, so he can't. self.assert_move_message( "shiva", stream_1, topic_name="new topic", expected_error=expected_error ) do_change_stream_group_based_setting( stream_1, "can_administer_channel_group", members_system_group, acting_user=iago ) # Channel administrators with content access can always move messages within # the channel even if they are not in `can_move_messages_within_channel_group`. self.assert_move_message("shiva", stream_1, topic_name="new topic") def test_move_messages_within_channels_with_updated_topics_policy(self) -> None: desdemona = self.example_user("desdemona") realm = desdemona.realm members_system_group = NamedUserGroup.objects.get( name=SystemGroups.MEMBERS, realm=realm, is_system_group=True ) do_change_realm_permission_group_setting( realm, "can_move_messages_between_topics_group", members_system_group, acting_user=None, ) stream_1 = get_stream("Denmark", realm) self.assert_move_message("desdemona", stream_1, topic_name="") self.assert_move_message("desdemona", stream_1, topic_name="new topic") do_set_stream_property( stream_1, "topics_policy", StreamTopicsPolicyEnum.disable_empty_topic.value, acting_user=desdemona, ) # Cannot move messages to empty topic as `topics_policy` is set to `disable_empty_topic`. self.assert_move_message( "desdemona", stream_1, topic_name="", expected_error="Sending messages to the general chat is not allowed in this channel.", ) self.assert_move_message("desdemona", stream_1, topic_name="new topic") do_set_stream_property( stream_1, "topics_policy", StreamTopicsPolicyEnum.empty_topic_only.value, acting_user=desdemona, ) # Cannot move messages to topics other than empty topic in the channels with # `topics_policy` set to `empty_topic_only`. self.assert_move_message( "desdemona", stream_1, orig_topic_name="", topic_name="new topic", expected_error="Only the general chat topic is allowed in this channel.", )