import datetime from operator import itemgetter from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from unittest import mock import orjson from django.db import IntegrityError from django.utils.timezone import now as timezone_now from zerver.actions.message_delete import do_delete_messages from zerver.actions.message_edit import ( check_update_message, do_update_message, get_mentions_for_message_updates, ) from zerver.actions.reactions import do_add_reaction from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property from zerver.actions.streams import do_change_stream_post_policy, do_deactivate_stream from zerver.actions.users import do_change_user_role from zerver.lib.message import MessageDict, has_message_access, messages_for_ids, truncate_topic from zerver.lib.test_classes import ZulipTestCase, get_topic_messages from zerver.lib.test_helpers import cache_tries_captured, queries_captured from zerver.lib.topic import RESOLVED_TOPIC_PREFIX, TOPIC_NAME from zerver.lib.user_topics import ( get_topic_mutes, get_users_muting_topic, set_topic_mutes, topic_is_muted, ) from zerver.lib.utils import assert_is_not_none from zerver.models import ( MAX_TOPIC_NAME_LENGTH, Message, Realm, Stream, UserMessage, UserProfile, UserTopic, get_realm, get_stream, ) if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse class EditMessageTestCase(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 check_message(self, msg_id: int, topic_name: str, content: str) -> None: # Make sure we saved the message correctly to the DB. msg = Message.objects.get(id=msg_id) self.assertEqual(msg.topic_name(), topic_name) self.assertEqual(msg.content, content) """ We assume our caller just edited a message. Next, we will make sure we properly cached the messages. We still have to do a query to hydrate recipient info, but we won't need to hit the zerver_message table. """ with queries_captured(keep_cache_warm=True) as queries: (fetch_message_dict,) = messages_for_ids( message_ids=[msg.id], user_message_flags={msg_id: []}, search_fields={}, apply_markdown=False, client_gravatar=False, allow_edit_history=True, ) self.assert_length(queries, 1) for query in queries: self.assertNotIn("message", query["sql"]) self.assertEqual( fetch_message_dict[TOPIC_NAME], msg.topic_name(), ) self.assertEqual( fetch_message_dict["content"], msg.content, ) self.assertEqual( fetch_message_dict["sender_id"], msg.sender_id, ) if msg.edit_history: self.assertEqual( fetch_message_dict["edit_history"], orjson.loads(msg.edit_history), ) def prepare_move_topics( self, user_email: str, old_stream: str, new_stream: str, topic: str, language: Optional[str] = None, ) -> Tuple[UserProfile, Stream, Stream, int, int]: user_profile = self.example_user(user_email) if language is not None: user_profile.default_language = language user_profile.save(update_fields=["default_language"]) self.login(user_email) stream = self.make_stream(old_stream) stream_to = self.make_stream(new_stream) self.subscribe(user_profile, stream.name) self.subscribe(user_profile, stream_to.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name=topic, content="First" ) msg_id_lt = self.send_stream_message( user_profile, stream.name, topic_name=topic, content="Second" ) self.send_stream_message(user_profile, stream.name, topic_name=topic, content="third") return (user_profile, stream, stream_to, msg_id, msg_id_lt) class EditMessagePayloadTest(EditMessageTestCase): def test_edit_message_no_changes(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), {}, ) self.assert_json_error(result, "Nothing to change") def test_move_message_cant_move_private_message(self) -> None: hamlet = self.example_user("hamlet") self.login("hamlet") cordelia = self.example_user("cordelia") msg_id = self.send_personal_message(hamlet, cordelia) verona = get_stream("Verona", hamlet.realm) result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": verona.id, }, ) self.assert_json_error(result, "Direct messages cannot be moved to streams.") 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_no_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": " ", }, ) self.assert_json_error(result, "Topic can't be empty!") 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!") def test_move_message_to_stream_with_content(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "propagate_mode": "change_all", "content": "Not allowed", }, ) self.assert_json_error(result, "Cannot change message content while changing stream") messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 3) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 0) # Right now, we prevent users from editing widgets. def test_edit_submessage(self) -> None: self.login("hamlet") msg_id = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="/poll Games?\nYES\nNO", ) result = self.client_patch( "/json/messages/" + str(msg_id), { "content": "/poll Games?\nYES\nNO\nMaybe", }, ) self.assert_json_error(result, "Widgets cannot be edited.") class EditMessageTest(EditMessageTestCase): def test_query_count_on_to_dict_uncached(self) -> None: # `to_dict_uncached` method is used by the mechanisms # tested in this class. Hence, its performance is tested here. # Generate 2 messages user = self.example_user("hamlet") realm = user.realm self.login_user(user) stream_name = "public_stream" self.subscribe(user, stream_name) message_ids = [] message_ids.append(self.send_stream_message(user, stream_name, "Message one")) user_2 = self.example_user("cordelia") self.subscribe(user_2, stream_name) message_ids.append(self.send_stream_message(user_2, stream_name, "Message two")) self.subscribe(self.notification_bot(realm), stream_name) message_ids.append( self.send_stream_message(self.notification_bot(realm), stream_name, "Message three") ) messages = [ Message.objects.select_related().get(id=message_id) for message_id in message_ids ] # Check number of queries performed # 1 query for realm_id per message = 3 # 1 query each for reactions & submessage for all messages = 2 with self.assert_database_query_count(5): MessageDict.to_dict_uncached(messages) realm_id = 2 # Fetched from stream object # Check number of queries performed with realm_id # 1 query each for reactions & submessage for all messages = 2 with self.assert_database_query_count(2): MessageDict.to_dict_uncached(messages, realm_id) def test_save_message(self) -> None: """This is also tested by a client test, but here we can verify the cache against the database""" self.login("hamlet") msg_id = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="before edit" ) result = self.client_patch( f"/json/messages/{msg_id}", { "content": "after edit", }, ) self.assert_json_success(result) self.check_message(msg_id, topic_name="editing", content="after edit") result = self.client_patch( f"/json/messages/{msg_id}", { "topic": "edited", }, ) self.assert_json_success(result) self.check_topic(msg_id, topic_name="edited") def test_fetch_message_from_id(self) -> None: self.login("hamlet") msg_id = self.send_personal_message( from_user=self.example_user("hamlet"), to_user=self.example_user("cordelia"), content="Personal message", ) result = self.client_get("/json/messages/" + str(msg_id)) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["raw_content"], "Personal message") self.assertEqual(response_dict["message"]["id"], msg_id) self.assertEqual(response_dict["message"]["flags"], []) # Send message to web-public stream where hamlet is not subscribed. # This will test case of user having no `UserMessage` but having access # to message. web_public_stream = self.make_stream("web-public-stream", is_web_public=True) self.subscribe(self.example_user("cordelia"), web_public_stream.name) web_public_stream_msg_id = self.send_stream_message( self.example_user("cordelia"), web_public_stream.name, content="web-public message" ) result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["raw_content"], "web-public message") self.assertEqual(response_dict["message"]["id"], web_public_stream_msg_id) self.assertEqual(response_dict["message"]["flags"], ["read", "historical"]) # Spectator should be able to fetch message in web-public stream. self.logout() result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["raw_content"], "web-public message") self.assertEqual(response_dict["message"]["id"], web_public_stream_msg_id) # Verify default is apply_markdown=True self.assertEqual(response_dict["message"]["content"], "

web-public message

") # Verify apply_markdown=False works correctly. result = self.client_get( "/json/messages/" + str(web_public_stream_msg_id), {"apply_markdown": "false"} ) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["raw_content"], "web-public message") self.assertEqual(response_dict["message"]["content"], "web-public message") with self.settings(WEB_PUBLIC_STREAMS_ENABLED=False): result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", status_code=401 ) # Test error cases self.login("hamlet") result = self.client_get("/json/messages/999999") self.assert_json_error(result, "Invalid message(s)") self.login("cordelia") result = self.client_get(f"/json/messages/{msg_id}") self.assert_json_success(result) self.login("othello") result = self.client_get(f"/json/messages/{msg_id}") self.assert_json_error(result, "Invalid message(s)") def test_fetch_raw_message_spectator(self) -> None: user_profile = self.example_user("iago") self.login("iago") web_public_stream = self.make_stream("web-public-stream", is_web_public=True) self.subscribe(user_profile, web_public_stream.name) web_public_stream_msg_id = self.send_stream_message( user_profile, web_public_stream.name, content="web-public message" ) non_web_public_stream = self.make_stream("non-web-public-stream") self.subscribe(user_profile, non_web_public_stream.name) non_web_public_stream_msg_id = self.send_stream_message( user_profile, non_web_public_stream.name, content="non-web-public message" ) # Generate a private message to use in verification. private_message_id = self.send_personal_message(user_profile, user_profile) invalid_message_id = private_message_id + 1000 self.logout() # Confirm WEB_PUBLIC_STREAMS_ENABLED is enforced. with self.settings(WEB_PUBLIC_STREAMS_ENABLED=False): result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) do_set_realm_property( user_profile.realm, "enable_spectator_access", False, acting_user=None ) result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) do_set_realm_property(user_profile.realm, "enable_spectator_access", True, acting_user=None) # Verify success with web-public stream and default SELF_HOSTED plan type. result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["raw_content"], "web-public message") self.assertEqual(response_dict["message"]["flags"], ["read"]) # Verify LIMITED plan type does not allow web-public access. do_change_realm_plan_type(user_profile.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None) result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) do_set_realm_property(user_profile.realm, "enable_spectator_access", True, acting_user=None) result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) # Verify works with STANDARD_FREE plan type too. do_change_realm_plan_type( user_profile.realm, Realm.PLAN_TYPE_STANDARD_FREE, acting_user=None ) result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["raw_content"], "web-public message") # Verify private messages are rejected. result = self.client_get("/json/messages/" + str(private_message_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) # Verify an actual public stream is required. result = self.client_get("/json/messages/" + str(non_web_public_stream_msg_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) # Verify invalid message IDs are rejected with the same error message. result = self.client_get("/json/messages/" + str(invalid_message_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) # Verify deactivated streams are rejected. This may change in the future. do_deactivate_stream(web_public_stream, acting_user=None) result = self.client_get("/json/messages/" + str(web_public_stream_msg_id)) self.assert_json_error( result, "Not logged in: API authentication or user session required", 401 ) def test_fetch_raw_message_stream_wrong_realm(self) -> None: user_profile = self.example_user("hamlet") self.login_user(user_profile) 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="test" ) result = self.client_get(f"/json/messages/{msg_id}") self.assert_json_success(result) mit_user = self.mit_user("sipbtest") self.login_user(mit_user) result = self.client_get(f"/json/messages/{msg_id}", subdomain="zephyr") self.assert_json_error(result, "Invalid message(s)") def test_fetch_raw_message_private_stream(self) -> None: user_profile = self.example_user("hamlet") self.login_user(user_profile) stream = self.make_stream("private_stream", invite_only=True) self.subscribe(user_profile, stream.name) msg_id = self.send_stream_message( user_profile, stream.name, topic_name="test", content="test" ) result = self.client_get(f"/json/messages/{msg_id}") self.assert_json_success(result) self.login("othello") result = self.client_get(f"/json/messages/{msg_id}") self.assert_json_error(result, "Invalid message(s)") def test_edit_message_no_permission(self) -> None: self.login("hamlet") msg_id = self.send_stream_message( self.example_user("iago"), "Denmark", topic_name="editing", content="before edit" ) result = self.client_patch( f"/json/messages/{msg_id}", { "content": "content after edit", }, ) self.assert_json_error(result, "You don't have permission to edit this message") def test_edit_message_no_content(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( f"/json/messages/{msg_id}", { "content": " ", }, ) self.assert_json_success(result) content = Message.objects.filter(id=msg_id).values_list("content", flat=True)[0] self.assertEqual(content, "(deleted)") def test_edit_message_history_disabled(self) -> None: user_profile = self.example_user("hamlet") do_set_realm_property(user_profile.realm, "allow_edit_history", False, acting_user=None) self.login("hamlet") # Single-line edit msg_id_1 = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="content before edit", ) new_content_1 = "content after edit" result_1 = self.client_patch( f"/json/messages/{msg_id_1}", { "content": new_content_1, }, ) self.assert_json_success(result_1) result = self.client_get(f"/json/messages/{msg_id_1}/history") self.assert_json_error(result, "Message edit history is disabled in this organization") # Now verify that if we fetch the message directly, there's no # edit history data attached. messages_result = self.client_get( "/json/messages", {"anchor": msg_id_1, "num_before": 0, "num_after": 10} ) self.assert_json_success(messages_result) json_messages = orjson.loads(messages_result.content) for msg in json_messages["messages"]: self.assertNotIn("edit_history", msg) def test_edit_message_history(self) -> None: self.login("hamlet") # Single-line edit msg_id_1 = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="content before edit", ) new_content_1 = "content after edit" result_1 = self.client_patch( f"/json/messages/{msg_id_1}", { "content": new_content_1, }, ) self.assert_json_success(result_1) message_edit_history_1 = self.client_get(f"/json/messages/{msg_id_1}/history") json_response_1 = orjson.loads(message_edit_history_1.content) message_history_1 = json_response_1["message_history"] # Check content of message after edit. self.assertEqual(message_history_1[0]["rendered_content"], "

content before edit

") self.assertEqual(message_history_1[1]["rendered_content"], "

content after edit

") self.assertEqual( message_history_1[1]["content_html_diff"], ( "

content " 'after ' 'before' " edit

" ), ) # Check content of message before edit. self.assertEqual( message_history_1[1]["prev_rendered_content"], "

content before edit

" ) # Edits on new lines msg_id_2 = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="content before edit, line 1\n\ncontent before edit, line 3", ) new_content_2 = ( "content before edit, line 1\n" "content after edit, line 2\n" "content before edit, line 3" ) result_2 = self.client_patch( f"/json/messages/{msg_id_2}", { "content": new_content_2, }, ) self.assert_json_success(result_2) message_edit_history_2 = self.client_get(f"/json/messages/{msg_id_2}/history") json_response_2 = orjson.loads(message_edit_history_2.content) message_history_2 = json_response_2["message_history"] self.assertEqual( message_history_2[0]["rendered_content"], "

content before edit, line 1

\n

content before edit, line 3

", ) self.assertEqual( message_history_2[1]["rendered_content"], ( "

content before edit, line 1
\n" "content after edit, line 2
\n" "content before edit, line 3

" ), ) self.assertEqual( message_history_2[1]["content_html_diff"], ( "

content before edit, line 1
" 'content after edit, line 2
' "content
before edit, line 3

" ), ) self.assertEqual( message_history_2[1]["prev_rendered_content"], "

content before edit, line 1

\n

content before edit, line 3

", ) def test_empty_message_edit(self) -> None: self.login("hamlet") msg_id = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="We will edit this to render as empty.", ) # Edit that manually to simulate a rendering bug message = Message.objects.get(id=msg_id) message.rendered_content = "" message.save(update_fields=["rendered_content"]) self.assert_json_success( self.client_patch( "/json/messages/" + str(msg_id), { "content": "We will edit this to also render as empty.", }, ) ) # And again tweak to simulate a rendering bug message = Message.objects.get(id=msg_id) message.rendered_content = "" message.save(update_fields=["rendered_content"]) history = self.client_get("/json/messages/" + str(msg_id) + "/history") message_history = orjson.loads(history.content)["message_history"] self.assertEqual(message_history[0]["rendered_content"], "") self.assertEqual(message_history[1]["rendered_content"], "") self.assertEqual(message_history[1]["content_html_diff"], "
") def test_edit_link(self) -> None: # Link editing self.login("hamlet") msg_id_1 = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="Here is a link to [zulip](www.zulip.org).", ) new_content_1 = "Here is a link to [zulip](www.zulipchat.com)." result_1 = self.client_patch( f"/json/messages/{msg_id_1}", { "content": new_content_1, }, ) self.assert_json_success(result_1) message_edit_history_1 = self.client_get(f"/json/messages/{msg_id_1}/history") json_response_1 = orjson.loads(message_edit_history_1.content) message_history_1 = json_response_1["message_history"] # Check content of message after edit. self.assertEqual( message_history_1[0]["rendered_content"], '

Here is a link to zulip.

', ) self.assertEqual( message_history_1[1]["rendered_content"], '

Here is a link to zulip.

', ) self.assertEqual( message_history_1[1]["content_html_diff"], ( '

Here is a link to zulip " ' Link: http://www.zulipchat.com .' ' Link: http://www.zulip.org .' "

" ), ) def test_edit_history_unedited(self) -> None: self.login("hamlet") msg_id = self.send_stream_message( self.example_user("hamlet"), "Denmark", topic_name="editing", content="This message has not been edited.", ) result = self.client_get(f"/json/messages/{msg_id}/history") message_history = self.assert_json_success(result)["message_history"] self.assert_length(message_history, 1) def test_mentions_for_message_updates(self) -> None: hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") self.login_user(hamlet) self.subscribe(hamlet, "Denmark") self.subscribe(cordelia, "Denmark") msg_id = self.send_stream_message( hamlet, "Denmark", content="@**Cordelia, Lear's daughter**" ) mention_user_ids = get_mentions_for_message_updates(msg_id) self.assertEqual(mention_user_ids, {cordelia.id}) def test_edit_cases(self) -> None: """This test verifies the accuracy of construction of Zulip's edit history data structures.""" self.login("hamlet") hamlet = self.example_user("hamlet") stream_1 = self.make_stream("stream 1") stream_2 = self.make_stream("stream 2") stream_3 = self.make_stream("stream 3") self.subscribe(hamlet, stream_1.name) self.subscribe(hamlet, stream_2.name) self.subscribe(hamlet, stream_3.name) msg_id = self.send_stream_message( self.example_user("hamlet"), "stream 1", topic_name="topic 1", content="content 1" ) result = self.client_patch( f"/json/messages/{msg_id}", { "content": "content 2", }, ) self.assert_json_success(result) history = orjson.loads(assert_is_not_none(Message.objects.get(id=msg_id).edit_history)) self.assertEqual(history[0]["prev_content"], "content 1") self.assertEqual(history[0]["user_id"], hamlet.id) self.assertEqual( set(history[0].keys()), { "timestamp", "prev_content", "user_id", "prev_rendered_content", "prev_rendered_content_version", }, ) result = self.client_patch( f"/json/messages/{msg_id}", { "topic": "topic 2", }, ) self.assert_json_success(result) history = orjson.loads(assert_is_not_none(Message.objects.get(id=msg_id).edit_history)) self.assertEqual(history[0]["prev_topic"], "topic 1") self.assertEqual(history[0]["topic"], "topic 2") self.assertEqual(history[0]["user_id"], hamlet.id) self.assertEqual( set(history[0].keys()), {"timestamp", "prev_topic", "topic", "user_id"}, ) self.login("iago") result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": stream_2.id, }, ) self.assert_json_success(result) history = orjson.loads(assert_is_not_none(Message.objects.get(id=msg_id).edit_history)) self.assertEqual(history[0]["prev_stream"], stream_1.id) self.assertEqual(history[0]["stream"], stream_2.id) self.assertEqual(history[0]["user_id"], self.example_user("iago").id) self.assertEqual(set(history[0].keys()), {"timestamp", "prev_stream", "stream", "user_id"}) self.login("hamlet") result = self.client_patch( f"/json/messages/{msg_id}", { "content": "content 3", "topic": "topic 3", }, ) self.assert_json_success(result) history = orjson.loads(assert_is_not_none(Message.objects.get(id=msg_id).edit_history)) self.assertEqual(history[0]["prev_content"], "content 2") self.assertEqual(history[0]["prev_topic"], "topic 2") self.assertEqual(history[0]["topic"], "topic 3") self.assertEqual(history[0]["user_id"], hamlet.id) self.assertEqual( set(history[0].keys()), { "timestamp", "prev_topic", "topic", "prev_content", "user_id", "prev_rendered_content", "prev_rendered_content_version", }, ) result = self.client_patch( f"/json/messages/{msg_id}", { "content": "content 4", }, ) self.assert_json_success(result) history = orjson.loads(assert_is_not_none(Message.objects.get(id=msg_id).edit_history)) self.assertEqual(history[0]["prev_content"], "content 3") self.assertEqual(history[0]["user_id"], hamlet.id) self.login("iago") result = self.client_patch( f"/json/messages/{msg_id}", { "topic": "topic 4", "stream_id": stream_3.id, }, ) self.assert_json_success(result) history = orjson.loads(assert_is_not_none(Message.objects.get(id=msg_id).edit_history)) self.assertEqual(history[0]["prev_topic"], "topic 3") self.assertEqual(history[0]["topic"], "topic 4") self.assertEqual(history[0]["prev_stream"], stream_2.id) self.assertEqual(history[0]["stream"], stream_3.id) self.assertEqual(history[0]["user_id"], self.example_user("iago").id) self.assertEqual( set(history[0].keys()), { "timestamp", "prev_topic", "topic", "prev_stream", "stream", "user_id", }, ) # Now, we verify that all of the edits stored in the message.edit_history # have the correct data structure history = orjson.loads(assert_is_not_none(Message.objects.get(id=msg_id).edit_history)) self.assertEqual(history[0]["prev_topic"], "topic 3") self.assertEqual(history[0]["topic"], "topic 4") self.assertEqual(history[0]["stream"], stream_3.id) self.assertEqual(history[0]["prev_stream"], stream_2.id) self.assertEqual(history[1]["prev_content"], "content 3") self.assertEqual(history[2]["prev_topic"], "topic 2") self.assertEqual(history[2]["topic"], "topic 3") self.assertEqual(history[2]["prev_content"], "content 2") self.assertEqual(history[3]["stream"], stream_2.id) self.assertEqual(history[3]["prev_stream"], stream_1.id) self.assertEqual(history[4]["prev_topic"], "topic 1") self.assertEqual(history[4]["topic"], "topic 2") self.assertEqual(history[5]["prev_content"], "content 1") # Now, we verify that the edit history data sent back has the # correct filled-out fields message_edit_history = self.client_get(f"/json/messages/{msg_id}/history") json_response = orjson.loads(message_edit_history.content) # We reverse the message history view output so that the IDs line up with the above. message_history = list(reversed(json_response["message_history"])) i = 0 for entry in message_history: expected_entries = {"content", "rendered_content", "topic", "timestamp", "user_id"} if i in {0, 2, 4}: expected_entries.add("prev_topic") expected_entries.add("topic") if i in {1, 2, 5}: expected_entries.add("prev_content") expected_entries.add("prev_rendered_content") expected_entries.add("content_html_diff") if i in {0, 3}: expected_entries.add("prev_stream") expected_entries.add("stream") i += 1 self.assertEqual(expected_entries, set(entry.keys())) self.assert_length(message_history, 7) self.assertEqual(message_history[0]["topic"], "topic 4") self.assertEqual(message_history[0]["prev_topic"], "topic 3") self.assertEqual(message_history[0]["stream"], stream_3.id) self.assertEqual(message_history[0]["prev_stream"], stream_2.id) self.assertEqual(message_history[0]["content"], "content 4") self.assertEqual(message_history[1]["topic"], "topic 3") self.assertEqual(message_history[1]["content"], "content 4") self.assertEqual(message_history[1]["prev_content"], "content 3") self.assertEqual(message_history[2]["topic"], "topic 3") self.assertEqual(message_history[2]["prev_topic"], "topic 2") self.assertEqual(message_history[2]["content"], "content 3") self.assertEqual(message_history[2]["prev_content"], "content 2") self.assertEqual(message_history[3]["topic"], "topic 2") self.assertEqual(message_history[3]["stream"], stream_2.id) self.assertEqual(message_history[3]["prev_stream"], stream_1.id) self.assertEqual(message_history[3]["content"], "content 2") self.assertEqual(message_history[4]["topic"], "topic 2") self.assertEqual(message_history[4]["prev_topic"], "topic 1") self.assertEqual(message_history[4]["content"], "content 2") self.assertEqual(message_history[5]["topic"], "topic 1") self.assertEqual(message_history[5]["content"], "content 2") self.assertEqual(message_history[5]["prev_content"], "content 1") self.assertEqual(message_history[6]["content"], "content 1") self.assertEqual(message_history[6]["topic"], "topic 1") def test_edit_message_content_limit(self) -> None: def set_message_editing_params( allow_message_editing: bool, message_content_edit_limit_seconds: Union[int, str], edit_topic_policy: int, ) -> None: result = self.client_patch( "/json/realm", { "allow_message_editing": orjson.dumps(allow_message_editing).decode(), "message_content_edit_limit_seconds": orjson.dumps( message_content_edit_limit_seconds ).decode(), "edit_topic_policy": edit_topic_policy, }, ) self.assert_json_success(result) def do_edit_message_assert_success( id_: int, unique_str: str, topic_only: bool = False ) -> None: new_topic = "topic" + unique_str new_content = "content" + unique_str params_dict = {"topic": new_topic} if not topic_only: params_dict["content"] = new_content result = self.client_patch(f"/json/messages/{id_}", params_dict) self.assert_json_success(result) if topic_only: self.check_topic(id_, topic_name=new_topic) else: self.check_message(id_, topic_name=new_topic, content=new_content) def do_edit_message_assert_error( id_: int, unique_str: str, error: str, topic_only: bool = False ) -> None: message = Message.objects.get(id=id_) old_topic = message.topic_name() old_content = message.content new_topic = "topic" + unique_str new_content = "content" + unique_str params_dict = {"topic": new_topic} if not topic_only: params_dict["content"] = new_content result = self.client_patch(f"/json/messages/{id_}", params_dict) message = Message.objects.get(id=id_) self.assert_json_error(result, error) msg = Message.objects.get(id=id_) self.assertEqual(msg.topic_name(), old_topic) self.assertEqual(msg.content, old_content) self.login("iago") # send a message in the past id_ = self.send_stream_message( self.example_user("iago"), "Denmark", content="content", topic_name="topic" ) message = Message.objects.get(id=id_) message.date_sent = message.date_sent - datetime.timedelta(seconds=180) message.save() # test the various possible message editing settings # high enough time limit, all edits allowed set_message_editing_params(True, 240, Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_success(id_, "A") # out of time, only topic editing allowed set_message_editing_params(True, 120, Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_success(id_, "B", True) do_edit_message_assert_error(id_, "C", "The time limit for editing this message has passed") # infinite time, all edits allowed set_message_editing_params(True, "unlimited", Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_success(id_, "D") # without allow_message_editing, editing content is not allowed but # editing topic is allowed if topic-edit time limit has not passed # irrespective of content-edit time limit. set_message_editing_params(False, 240, Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_success(id_, "B", True) set_message_editing_params(False, 240, Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_success(id_, "E", True) set_message_editing_params(False, 120, Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_success(id_, "F", True) set_message_editing_params(False, "unlimited", Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_success(id_, "G", True) def test_edit_topic_policy(self) -> None: def set_message_editing_params( allow_message_editing: bool, message_content_edit_limit_seconds: Union[int, str], edit_topic_policy: int, ) -> None: self.login("iago") result = self.client_patch( "/json/realm", { "allow_message_editing": orjson.dumps(allow_message_editing).decode(), "message_content_edit_limit_seconds": orjson.dumps( message_content_edit_limit_seconds ).decode(), "edit_topic_policy": edit_topic_policy, }, ) self.assert_json_success(result) def do_edit_message_assert_success(id_: int, unique_str: str, acting_user: str) -> None: self.login(acting_user) new_topic = "topic" + unique_str params_dict = {"topic": new_topic} result = self.client_patch(f"/json/messages/{id_}", params_dict) self.assert_json_success(result) self.check_topic(id_, topic_name=new_topic) def do_edit_message_assert_error( id_: int, unique_str: str, error: str, acting_user: str ) -> None: self.login(acting_user) message = Message.objects.get(id=id_) old_topic = message.topic_name() old_content = message.content new_topic = "topic" + unique_str params_dict = {"topic": new_topic} result = self.client_patch(f"/json/messages/{id_}", params_dict) message = Message.objects.get(id=id_) self.assert_json_error(result, error) msg = Message.objects.get(id=id_) self.assertEqual(msg.topic_name(), old_topic) self.assertEqual(msg.content, old_content) # send a message in the past id_ = self.send_stream_message( self.example_user("hamlet"), "Denmark", content="content", topic_name="topic" ) message = Message.objects.get(id=id_) message.date_sent = message.date_sent - datetime.timedelta(seconds=180) message.save() # Guest user must be subscribed to the stream to access the message. polonius = self.example_user("polonius") self.subscribe(polonius, "Denmark") # any user can edit the topic of a message set_message_editing_params(True, "unlimited", Realm.POLICY_EVERYONE) do_edit_message_assert_success(id_, "A", "polonius") # only members can edit topic of a message set_message_editing_params(True, "unlimited", Realm.POLICY_MEMBERS_ONLY) do_edit_message_assert_error( id_, "B", "You don't have permission to edit this message", "polonius" ) do_edit_message_assert_success(id_, "B", "cordelia") # only full members can edit topic of a message set_message_editing_params(True, "unlimited", Realm.POLICY_FULL_MEMBERS_ONLY) cordelia = self.example_user("cordelia") hamlet = self.example_user("hamlet") do_set_realm_property(cordelia.realm, "waiting_period_threshold", 10, acting_user=None) cordelia.date_joined = timezone_now() - datetime.timedelta(days=9) cordelia.save() hamlet.date_joined = timezone_now() - datetime.timedelta(days=9) hamlet.save() do_edit_message_assert_error( id_, "C", "You don't have permission to edit this message", "cordelia" ) # User who sent the message but is not a full member cannot edit # the topic do_edit_message_assert_error( id_, "C", "You don't have permission to edit this message", "hamlet" ) cordelia.date_joined = timezone_now() - datetime.timedelta(days=11) cordelia.save() hamlet.date_joined = timezone_now() - datetime.timedelta(days=11) hamlet.save() do_edit_message_assert_success(id_, "C", "cordelia") do_edit_message_assert_success(id_, "CD", "hamlet") # only moderators can edit topic of a message set_message_editing_params(True, "unlimited", Realm.POLICY_MODERATORS_ONLY) do_edit_message_assert_error( id_, "D", "You don't have permission to edit this message", "cordelia" ) # even user who sent the message but is not a moderator cannot edit the topic. do_edit_message_assert_error( id_, "D", "You don't have permission to edit this message", "hamlet" ) do_edit_message_assert_success(id_, "D", "shiva") # only admins can edit the topics of messages set_message_editing_params(True, "unlimited", Realm.POLICY_ADMINS_ONLY) do_edit_message_assert_error( id_, "E", "You don't have permission to edit this message", "shiva" ) do_edit_message_assert_success(id_, "E", "iago") # even owners and admins cannot edit the topics of messages set_message_editing_params(True, "unlimited", Realm.POLICY_NOBODY) do_edit_message_assert_error( id_, "H", "You don't have permission to edit this message", "desdemona" ) do_edit_message_assert_error( id_, "H", "You don't have permission to edit this message", "iago" ) # users can edit topics even if allow_message_editing is False set_message_editing_params(False, "unlimited", Realm.POLICY_EVERYONE) do_edit_message_assert_success(id_, "D", "cordelia") # non-admin users cannot edit topics sent > 1 week ago including # sender of the message. message.date_sent = message.date_sent - datetime.timedelta(seconds=604900) message.save() set_message_editing_params(True, "unlimited", Realm.POLICY_EVERYONE) do_edit_message_assert_success(id_, "E", "iago") do_edit_message_assert_success(id_, "F", "shiva") do_edit_message_assert_error( id_, "G", "The time limit for editing this message's topic has passed", "cordelia" ) do_edit_message_assert_error( id_, "G", "The time limit for editing this message's topic has passed", "hamlet" ) # set the topic edit limit to two weeks do_set_realm_property( hamlet.realm, "move_messages_within_stream_limit_seconds", 604800 * 2, acting_user=None, ) do_edit_message_assert_success(id_, "G", "cordelia") do_edit_message_assert_success(id_, "H", "hamlet") # anyone should be able to edit "no topic" indefinitely message.set_topic_name("(no topic)") message.save() do_edit_message_assert_success(id_, "D", "cordelia") @mock.patch("zerver.actions.message_edit.send_event") 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: do_update_message( user_profile=user_profile, target_message=message, new_stream=None, topic_name=topic_name, propagate_mode="change_later", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, 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.message_edit.send_event") def test_edit_muted_topic(self, mock_send_event: 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) already_muted_topic = "Already muted topic" muted_topics = [ [stream_name, "Topic1"], [stream_name, "Topic2"], [stream_name, already_muted_topic], ] set_topic_mutes(hamlet, muted_topics) set_topic_mutes(cordelia, muted_topics) # 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, aaron.id])) change_all_topic_name = "Topic 1 edited" # This code path adds 9 (1 + 4/user with muted topics) + 1 to # the number of database queries for moving a topic. with self.assert_database_query_count(19): 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, ) for muting_user in get_users_muting_topic(stream.id, change_all_topic_name): for user in users_to_be_notified: if muting_user.id == user["id"]: user["muted_topics"] = get_topic_mutes(muting_user) break self.assertFalse(topic_is_muted(hamlet, stream.id, "Topic1")) self.assertFalse(topic_is_muted(cordelia, stream.id, "Topic1")) self.assertFalse(topic_is_muted(aaron, stream.id, "Topic1")) self.assertTrue(topic_is_muted(hamlet, stream.id, "Topic2")) self.assertTrue(topic_is_muted(cordelia, stream.id, "Topic2")) self.assertFalse(topic_is_muted(aaron, stream.id, "Topic2")) self.assertTrue(topic_is_muted(hamlet, stream.id, change_all_topic_name)) self.assertTrue(topic_is_muted(cordelia, stream.id, change_all_topic_name)) self.assertFalse(topic_is_muted(aaron, stream.id, change_all_topic_name)) 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, ) self.assertFalse(topic_is_muted(hamlet, stream.id, change_all_topic_name)) self.assertTrue(topic_is_muted(hamlet, stream.id, change_later_topic_name)) # Make sure we safely handle the case of the new topic being already muted. with self.assertLogs(level="INFO") as info_logs: check_update_message( user_profile=hamlet, message_id=message_id, stream_id=None, topic_name=already_muted_topic, propagate_mode="change_all", send_notification_to_old_thread=False, send_notification_to_new_thread=False, content=None, ) self.assertEqual( set(info_logs.output), { f"INFO:root:User {hamlet.id} tried to set visibility_policy to its current value of {UserTopic.VisibilityPolicy.MUTED}", f"INFO:root:User {cordelia.id} tried to set visibility_policy to its current value of {UserTopic.VisibilityPolicy.MUTED}", }, ) self.assertFalse(topic_is_muted(hamlet, stream.id, change_later_topic_name)) self.assertTrue(topic_is_muted(hamlet, stream.id, already_muted_topic)) 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, ) self.assertTrue(topic_is_muted(hamlet, stream.id, change_one_topic_name)) self.assertFalse(topic_is_muted(hamlet, stream.id, change_later_topic_name)) # 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_mutes(desdemona, muted_topics) set_topic_mutes(cordelia, muted_topics) with self.assert_database_query_count(30): 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, ) self.assertFalse(topic_is_muted(desdemona, stream.id, "New topic")) self.assertFalse(topic_is_muted(cordelia, stream.id, "New topic")) self.assertFalse(topic_is_muted(aaron, stream.id, "New topic")) self.assertTrue(topic_is_muted(desdemona, new_public_stream.id, "New topic")) self.assertTrue(topic_is_muted(cordelia, new_public_stream.id, "New topic")) self.assertFalse(topic_is_muted(aaron, new_public_stream.id, "New topic")) # 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_mutes(desdemona, muted_topics) set_topic_mutes(cordelia, muted_topics) with self.assert_database_query_count(31): 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. self.assertFalse(topic_is_muted(desdemona, stream.id, "New topic")) self.assertFalse(topic_is_muted(cordelia, stream.id, "New topic")) self.assertFalse(topic_is_muted(aaron, stream.id, "New topic")) self.assertTrue(topic_is_muted(desdemona, new_private_stream.id, "New topic")) self.assertFalse(topic_is_muted(cordelia, new_private_stream.id, "New topic")) self.assertFalse(topic_is_muted(aaron, new_private_stream.id, "New topic")) # 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_mutes(desdemona, muted_topics) set_topic_mutes(cordelia, muted_topics) with self.assert_database_query_count(30): 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, ) self.assertFalse(topic_is_muted(desdemona, stream.id, "New topic 2")) self.assertFalse(topic_is_muted(cordelia, stream.id, "New topic 2")) self.assertFalse(topic_is_muted(aaron, stream.id, "New topic 2")) self.assertTrue(topic_is_muted(desdemona, new_public_stream.id, "changed topic name")) self.assertTrue(topic_is_muted(cordelia, new_public_stream.id, "changed topic name")) self.assertFalse(topic_is_muted(aaron, new_public_stream.id, "changed topic name")) # Moving only half the messages doesn't move MutedTopic 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(24): 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, ) self.assertTrue(topic_is_muted(desdemona, new_public_stream.id, "changed topic name")) self.assertTrue(topic_is_muted(cordelia, new_public_stream.id, "changed topic name")) self.assertFalse(topic_is_muted(aaron, new_public_stream.id, "changed topic name")) self.assertFalse(topic_is_muted(desdemona, new_public_stream.id, "final topic name")) self.assertFalse(topic_is_muted(cordelia, new_public_stream.id, "final topic name")) self.assertFalse(topic_is_muted(aaron, new_public_stream.id, "final topic name")) @mock.patch("zerver.actions.message_edit.send_event") def test_wildcard_mention(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.subscribe(cordelia, stream_name) self.login_user(hamlet) message_id = self.send_stream_message(hamlet, stream_name, "Hello everyone") def notify(user_id: int) -> Dict[str, Any]: return { "id": user_id, "flags": ["wildcard_mentioned"], } users_to_be_notified = sorted(map(notify, [cordelia.id, hamlet.id]), key=itemgetter("id")) result = self.client_patch( f"/json/messages/{message_id}", { "content": "Hello @**everyone**", }, ) self.assert_json_success(result) # Extract the send_event call where event type is 'update_message'. # Here we assert wildcard_mention_user_ids has been set properly. called = False for call_args in mock_send_event.call_args_list: (arg_realm, arg_event, arg_notified_users) = call_args[0] if arg_event["type"] == "update_message": self.assertEqual(arg_event["type"], "update_message") self.assertEqual(arg_event["wildcard_mention_user_ids"], [cordelia.id, hamlet.id]) self.assertEqual( sorted(arg_notified_users, key=itemgetter("id")), users_to_be_notified ) called = True self.assertTrue(called) def test_wildcard_mention_restrictions_when_editing(self) -> None: cordelia = self.example_user("cordelia") shiva = self.example_user("shiva") self.login("cordelia") stream_name = "Macbeth" self.make_stream(stream_name, history_public_to_subscribers=True) self.subscribe(cordelia, stream_name) self.subscribe(shiva, stream_name) message_id = self.send_stream_message(cordelia, stream_name, "Hello everyone") realm = cordelia.realm do_set_realm_property( realm, "wildcard_mention_policy", Realm.WILDCARD_MENTION_POLICY_MODERATORS, acting_user=None, ) with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=17): result = self.client_patch( "/json/messages/" + str(message_id), { "content": "Hello @**everyone**", }, ) self.assert_json_error( result, "You do not have permission to use wildcard mentions in this stream." ) with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=14): result = self.client_patch( "/json/messages/" + str(message_id), { "content": "Hello @**everyone**", }, ) self.assert_json_success(result) self.login("shiva") message_id = self.send_stream_message(shiva, stream_name, "Hi everyone") with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=17): result = self.client_patch( "/json/messages/" + str(message_id), { "content": "Hello @**everyone**", }, ) self.assert_json_success(result) 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: str, len_edit_history: int) -> None: for msg_id in [id1, id2, id5]: msg = Message.objects.get(id=msg_id) self.assertEqual( new_topic, 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 = "edited" result = self.client_patch( f"/json/messages/{id1}", { "topic": new_topic, "propagate_mode": "change_later", }, ) self.assert_json_success(result) verify_edit_history(new_topic, 1) new_topic = "edited2" result = self.client_patch( f"/json/messages/{id1}", { "topic": new_topic, "propagate_mode": "change_later", }, ) self.assert_json_success(result) verify_edit_history(new_topic, 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 = "edited" result = self.client_patch( "/json/messages/" + str(id1), { "topic": new_topic, "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, 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_move_message_to_stream(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_lt) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test", # Set the user's translation language to German to test that # it is overridden by the realm's default language. "de", ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "propagate_mode": "change_all", "send_notification_to_old_thread": "true", }, HTTP_ACCEPT_LANGUAGE="de", ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) self.assertEqual( messages[3].content, f"This topic was moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_preexisting_topic(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_lt) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test", # Set the user's translation language to German to test that # it is overridden by the realm's default language. "de", ) self.send_stream_message( sender=self.example_user("iago"), stream_name="new stream", topic_name="test", content="Always here", ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "propagate_mode": "change_all", "send_notification_to_old_thread": "true", }, HTTP_ACCEPT_LANGUAGE="de", ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 5) self.assertEqual( messages[4].content, f"3 messages were moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_realm_admin_cant_move_to_another_realm(self) -> None: user_profile = self.example_user("iago") self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_ADMINISTRATOR) self.login("iago") lear_realm = get_realm("lear") new_stream = self.make_stream("new", lear_realm) msg_id = self.send_stream_message(user_profile, "Verona", topic_name="test123") result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "propagate_mode": "change_all", }, ) self.assert_json_error(result, "Invalid stream ID") def test_move_message_realm_admin_cant_move_to_private_stream_without_subscription( self, ) -> None: user_profile = self.example_user("iago") self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_ADMINISTRATOR) self.login("iago") new_stream = self.make_stream("new", invite_only=True) msg_id = self.send_stream_message(user_profile, "Verona", topic_name="test123") result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "propagate_mode": "change_all", }, ) self.assert_json_error(result, "Invalid stream ID") def test_move_message_realm_admin_cant_move_from_private_stream_without_subscription( self, ) -> None: user_profile = self.example_user("iago") self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_ADMINISTRATOR) self.login("iago") self.make_stream("privatestream", invite_only=True) self.subscribe(user_profile, "privatestream") msg_id = self.send_stream_message(user_profile, "privatestream", topic_name="test123") self.unsubscribe(user_profile, "privatestream") verona = get_stream("Verona", user_profile.realm) result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": verona.id, "propagate_mode": "change_all", }, ) self.assert_json_error( result, "You don't have permission to move this message due to missing access to its stream", ) def test_move_message_from_private_stream_message_access_checks( self, ) -> None: hamlet = self.example_user("hamlet") user_profile = self.example_user("iago") self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_ADMINISTRATOR) self.login("iago") private_stream = self.make_stream( "privatestream", invite_only=True, history_public_to_subscribers=False ) self.subscribe(hamlet, "privatestream") original_msg_id = self.send_stream_message(hamlet, "privatestream", topic_name="test123") self.subscribe(user_profile, "privatestream") new_msg_id = self.send_stream_message(user_profile, "privatestream", topic_name="test123") # Now we unsub and hamlet sends a new message (we won't have access to it even after re-subbing!) self.unsubscribe(user_profile, "privatestream") new_inaccessible_msg_id = self.send_stream_message( hamlet, "privatestream", topic_name="test123" ) # Re-subscribe and send another message: self.subscribe(user_profile, "privatestream") newest_msg_id = self.send_stream_message( user_profile, "privatestream", topic_name="test123" ) verona = get_stream("Verona", user_profile.realm) result = self.client_patch( "/json/messages/" + str(new_msg_id), { "stream_id": verona.id, "propagate_mode": "change_all", }, ) self.assert_json_success(result) self.assertEqual(Message.objects.get(id=new_msg_id).recipient_id, verona.recipient_id) self.assertEqual(Message.objects.get(id=newest_msg_id).recipient_id, verona.recipient_id) # The original message and the new, inaccessible message weren't moved, # because user_profile doesn't have access to them. self.assertEqual( Message.objects.get(id=original_msg_id).recipient_id, private_stream.recipient_id ) self.assertEqual( Message.objects.get(id=new_inaccessible_msg_id).recipient_id, private_stream.recipient_id, ) def test_move_message_to_stream_change_later(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( f"/json/messages/{msg_id_later}", { "stream_id": new_stream.id, "propagate_mode": "change_later", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 2) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[1].content, f"2 messages were moved from this topic to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 3) self.assertEqual(messages[0].id, msg_id_later) self.assertEqual( messages[2].content, f"2 messages were moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_preexisting_topic_change_later(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) self.send_stream_message( sender=self.example_user("iago"), stream_name="new stream", topic_name="test", content="Always here", ) result = self.client_patch( f"/json/messages/{msg_id_later}", { "stream_id": new_stream.id, "propagate_mode": "change_later", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 2) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[1].content, f"2 messages were moved from this topic to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) self.assertEqual(messages[0].id, msg_id_later) self.assertEqual( messages[3].content, f"2 messages were moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_stream_change_later_all_moved(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "propagate_mode": "change_later", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[3].content, f"This topic was moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_preexisting_topic_change_later_all_moved(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) self.send_stream_message( sender=self.example_user("iago"), stream_name="new stream", topic_name="test", content="Always here", ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "propagate_mode": "change_later", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 5) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[4].content, f"3 messages were moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_stream_change_one(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( "/json/messages/" + str(msg_id_later), { "stream_id": new_stream.id, "propagate_mode": "change_one", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 3) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[2].content, f"A message was moved from this topic to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 2) self.assertEqual(messages[0].id, msg_id_later) self.assertEqual( messages[1].content, f"A message was moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_preexisting_topic_change_one(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) self.send_stream_message( sender=self.example_user("iago"), stream_name="new stream", topic_name="test", content="Always here", ) result = self.client_patch( "/json/messages/" + str(msg_id_later), { "stream_id": new_stream.id, "propagate_mode": "change_one", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 3) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[2].content, f"A message was moved from this topic to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 3) self.assertEqual(messages[0].id, msg_id_later) self.assertEqual( messages[2].content, f"A message was moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_stream_change_all(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( "/json/messages/" + str(msg_id_later), { "stream_id": new_stream.id, "propagate_mode": "change_all", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[3].content, f"This topic was moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_to_preexisting_topic_change_all(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) self.send_stream_message( sender=self.example_user("iago"), stream_name="new stream", topic_name="test", content="Always here", ) result = self.client_patch( "/json/messages/" + str(msg_id_later), { "stream_id": new_stream.id, "propagate_mode": "change_all", "send_notification_to_old_thread": "true", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 5) self.assertEqual(messages[0].id, msg_id) self.assertEqual( messages[4].content, f"3 messages were moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_move_message_between_streams_policy_setting(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_1", "new_stream_1", "test" ) def check_move_message_according_to_policy(role: int, expect_fail: bool = False) -> None: do_change_user_role(user_profile, role, acting_user=None) result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "propagate_mode": "change_all", }, ) if expect_fail: self.assert_json_error(result, "You don't have permission to move this message") messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 3) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 0) else: self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) # Check sending messages when policy is Realm.POLICY_NOBODY. do_set_realm_property( user_profile.realm, "move_messages_between_streams_policy", Realm.POLICY_NOBODY, acting_user=None, ) check_move_message_according_to_policy(UserProfile.ROLE_REALM_OWNER, expect_fail=True) check_move_message_according_to_policy( UserProfile.ROLE_REALM_ADMINISTRATOR, expect_fail=True ) # Check sending messages when policy is Realm.POLICY_ADMINS_ONLY. do_set_realm_property( user_profile.realm, "move_messages_between_streams_policy", Realm.POLICY_ADMINS_ONLY, acting_user=None, ) check_move_message_according_to_policy(UserProfile.ROLE_MODERATOR, expect_fail=True) check_move_message_according_to_policy(UserProfile.ROLE_REALM_ADMINISTRATOR) (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_2", "new_stream_2", "test" ) # Check sending messages when policy is Realm.POLICY_MODERATORS_ONLY. do_set_realm_property( user_profile.realm, "move_messages_between_streams_policy", Realm.POLICY_MODERATORS_ONLY, acting_user=None, ) check_move_message_according_to_policy(UserProfile.ROLE_MEMBER, expect_fail=True) check_move_message_according_to_policy(UserProfile.ROLE_MODERATOR) (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_3", "new_stream_3", "test" ) # Check sending messages when policy is Realm.POLICY_FULL_MEMBERS_ONLY. do_set_realm_property( user_profile.realm, "move_messages_between_streams_policy", Realm.POLICY_FULL_MEMBERS_ONLY, acting_user=None, ) do_set_realm_property( user_profile.realm, "waiting_period_threshold", 100000, acting_user=None ) check_move_message_according_to_policy(UserProfile.ROLE_MEMBER, expect_fail=True) do_set_realm_property(user_profile.realm, "waiting_period_threshold", 0, acting_user=None) check_move_message_according_to_policy(UserProfile.ROLE_MEMBER) (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_4", "new_stream_4", "test" ) # Check sending messages when policy is Realm.POLICY_MEMBERS_ONLY. do_set_realm_property( user_profile.realm, "move_messages_between_streams_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None, ) check_move_message_according_to_policy(UserProfile.ROLE_GUEST, expect_fail=True) check_move_message_according_to_policy(UserProfile.ROLE_MEMBER) def test_move_message_to_stream_time_limit(self) -> None: shiva = self.example_user("shiva") iago = self.example_user("iago") cordelia = self.example_user("cordelia") test_stream_1 = self.make_stream("test_stream_1") test_stream_2 = self.make_stream("test_stream_2") self.subscribe(shiva, test_stream_1.name) self.subscribe(iago, test_stream_1.name) self.subscribe(cordelia, test_stream_1.name) self.subscribe(shiva, test_stream_2.name) self.subscribe(iago, test_stream_2.name) self.subscribe(cordelia, test_stream_2.name) msg_id = self.send_stream_message( cordelia, test_stream_1.name, topic_name="test", content="First" ) self.send_stream_message(cordelia, test_stream_1.name, topic_name="test", content="Second") self.send_stream_message(cordelia, test_stream_1.name, topic_name="test", content="third") do_set_realm_property( cordelia.realm, "move_messages_between_streams_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None, ) def check_move_message_to_stream( user: UserProfile, old_stream: Stream, new_stream: Stream, *, expect_error_message: Optional[str] = None, ) -> None: self.login_user(user) result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "propagate_mode": "change_all", "send_notification_to_new_thread": orjson.dumps(False).decode(), }, ) if expect_error_message is not None: self.assert_json_error(result, expect_error_message) messages = get_topic_messages(user, old_stream, "test") self.assert_length(messages, 3) messages = get_topic_messages(user, new_stream, "test") self.assert_length(messages, 0) else: self.assert_json_success(result) messages = get_topic_messages(user, old_stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user, new_stream, "test") self.assert_length(messages, 3) # non-admin and non-moderator users cannot move messages sent > 1 week ago # including sender of the message. message = Message.objects.get(id=msg_id) message.date_sent = message.date_sent - datetime.timedelta(seconds=604900) message.save() check_move_message_to_stream( cordelia, test_stream_1, test_stream_2, expect_error_message="The time limit for editing this message's stream has passed", ) # admins and moderators can move messages irrespective of time limit. check_move_message_to_stream(shiva, test_stream_1, test_stream_2, expect_error_message=None) check_move_message_to_stream(iago, test_stream_2, test_stream_1, expect_error_message=None) # set the topic edit limit to two weeks do_set_realm_property( cordelia.realm, "move_messages_between_streams_limit_seconds", 604800 * 2, acting_user=None, ) check_move_message_to_stream( cordelia, test_stream_1, test_stream_2, expect_error_message=None ) def test_move_message_to_stream_based_on_stream_post_policy(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_1", "new_stream_1", "test" ) do_set_realm_property( user_profile.realm, "move_messages_between_streams_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None, ) def check_move_message_to_stream(role: int, error_msg: Optional[str] = None) -> None: do_change_user_role(user_profile, role, acting_user=None) result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "propagate_mode": "change_all", }, ) if error_msg is not None: self.assert_json_error(result, error_msg) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 3) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 0) else: self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) # Check when stream_post_policy is STREAM_POST_POLICY_ADMINS. do_change_stream_post_policy( new_stream, Stream.STREAM_POST_POLICY_ADMINS, acting_user=user_profile ) error_msg = "Only organization administrators can send to this stream." check_move_message_to_stream(UserProfile.ROLE_MODERATOR, error_msg) check_move_message_to_stream(UserProfile.ROLE_REALM_ADMINISTRATOR) (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_2", "new_stream_2", "test" ) # Check when stream_post_policy is STREAM_POST_POLICY_MODERATORS. do_change_stream_post_policy( new_stream, Stream.STREAM_POST_POLICY_MODERATORS, acting_user=user_profile ) error_msg = "Only organization administrators and moderators can send to this stream." check_move_message_to_stream(UserProfile.ROLE_MEMBER, error_msg) check_move_message_to_stream(UserProfile.ROLE_MODERATOR) (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_3", "new_stream_3", "test" ) # Check when stream_post_policy is STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS. do_change_stream_post_policy( new_stream, Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS, acting_user=user_profile ) error_msg = "New members cannot send to this stream." do_set_realm_property( user_profile.realm, "waiting_period_threshold", 100000, acting_user=None ) check_move_message_to_stream(UserProfile.ROLE_MEMBER, error_msg) do_set_realm_property(user_profile.realm, "waiting_period_threshold", 0, acting_user=None) check_move_message_to_stream(UserProfile.ROLE_MEMBER) (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_4", "new_stream_4", "test" ) # Check when stream_post_policy is STREAM_POST_POLICY_EVERYONE. # In this case also, guest is not allowed as we do not allow guest to move # messages between streams in any case, so stream_post_policy of new stream does # not matter. do_change_stream_post_policy( new_stream, Stream.STREAM_POST_POLICY_EVERYONE, acting_user=user_profile ) do_set_realm_property( user_profile.realm, "waiting_period_threshold", 100000, acting_user=None ) check_move_message_to_stream( UserProfile.ROLE_GUEST, "You don't have permission to move this message" ) check_move_message_to_stream(UserProfile.ROLE_MEMBER) def test_move_message_to_stream_with_topic_editing_not_allowed(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_1", "new_stream_1", "test" ) realm = user_profile.realm realm.edit_topic_policy = Realm.POLICY_ADMINS_ONLY realm.save() self.login("cordelia") do_set_realm_property( user_profile.realm, "move_messages_between_streams_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None, ) result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "propagate_mode": "change_all", "topic": "new topic", }, ) self.assert_json_error(result, "You don't have permission to edit this message") result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) def test_move_message_to_stream_and_topic(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) with self.assert_database_query_count(55), cache_tries_captured() as cache_tries: result = self.client_patch( f"/json/messages/{msg_id}", { "propagate_mode": "change_all", "send_notification_to_old_thread": "true", "stream_id": new_stream.id, "topic": "new topic", }, ) self.assert_length(cache_tries, 13) messages = get_topic_messages(user_profile, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>new topic** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "new topic") self.assert_length(messages, 4) self.assertEqual( messages[3].content, f"This topic was moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) self.assert_json_success(result) def test_inaccessible_msg_after_stream_change(self) -> None: """Simulates the case where message is moved to a stream where user is not a subscribed""" (user_profile, old_stream, new_stream, msg_id, msg_id_lt) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) guest_user = self.example_user("polonius") non_guest_user = self.example_user("hamlet") self.subscribe(guest_user, old_stream.name) self.subscribe(non_guest_user, old_stream.name) msg_id_to_test_acesss = self.send_stream_message( user_profile, old_stream.name, topic_name="test", content="fourth" ) self.assertEqual( has_message_access( guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False ), True, ) self.assertEqual( has_message_access( guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, stream=old_stream, ), True, ) self.assertEqual( has_message_access( non_guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, ), True, ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "propagate_mode": "change_all", "topic": "new topic", }, ) self.assert_json_success(result) self.assertEqual( has_message_access( guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, ), False, ) self.assertEqual( has_message_access( non_guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, ), True, ) self.assertEqual( # If the guest user were subscribed to the new stream, # they'd have access; has_message_access does not validate # the is_subscribed parameter. has_message_access( guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, stream=new_stream, is_subscribed=True, ), True, ) self.assertEqual( has_message_access( guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, stream=new_stream, ), False, ) with self.assertRaises(AssertionError): # Raises assertion if you pass an invalid stream. has_message_access( guest_user, Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, stream=old_stream, ) self.assertEqual( UserMessage.objects.filter( user_profile_id=non_guest_user.id, message_id=msg_id_to_test_acesss, ).count(), 0, ) self.assertEqual( has_message_access( self.example_user("iago"), Message.objects.get(id=msg_id_to_test_acesss), has_user_message=False, ), True, ) def test_no_notify_move_message_to_stream(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_lt) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "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, old_stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 3) def test_notify_new_thread_move_message_to_stream(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_lt) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "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, old_stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 4) self.assertEqual( messages[3].content, f"This topic was moved here from #**test move stream>test** by @_**Iago|{user_profile.id}**.", ) def test_notify_old_thread_move_message_to_stream(self) -> None: (user_profile, old_stream, new_stream, msg_id, msg_id_lt) = self.prepare_move_topics( "iago", "test move stream", "new stream", "test" ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "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, old_stream, "test") self.assert_length(messages, 1) self.assertEqual( messages[0].content, f"This topic was moved to #**new stream>test** by @_**Iago|{user_profile.id}**.", ) messages = get_topic_messages(user_profile, new_stream, "test") self.assert_length(messages, 3) 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_new_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": "false", "send_notification_to_new_thread": "true", }, ) 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, 2) self.assertEqual(messages[0].content, "First") self.assertEqual( messages[1].content, f"A message was moved here from #**public stream>test** by @_**Iago|{user_profile.id}**.", ) 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_both_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": "true", }, ) 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, 2) self.assertEqual(messages[0].content, "First") self.assertEqual( messages[1].content, f"A message was moved here from #**public stream>test** by @_**Iago|{user_profile.id}**.", ) 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.", ) # 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.", ) 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 = "test" self.subscribe(user_profile, stream.name) # Resolve a topic normally first msg_id = self.send_stream_message(user_profile, stream.name, "foo", topic_name=topic) resolved_topic = RESOLVED_TOPIC_PREFIX + topic result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": resolved_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, resolved_topic) self.assert_length(messages, 2) self.assertEqual( messages[1].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as resolved.", ) # Test unresolving a topic while moving it (✔ test -> bar) new_topic = "bar" result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": new_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, new_topic) self.assert_length(messages, 4) self.assertEqual( messages[2].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as unresolved.", ) self.assertEqual( messages[3].content, f"This topic was moved here from #**public stream>✔ test** by @_**{user_profile.full_name}|{user_profile.id}**.", ) # Now test moving the topic while also resolving it (bar -> ✔ baz) new_resolved_topic = RESOLVED_TOPIC_PREFIX + "baz" result = self.client_patch( "/json/messages/" + str(msg_id), { "topic": new_resolved_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, stream, new_resolved_topic) self.assert_length(messages, 6) self.assertEqual( messages[4].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as resolved.", ) self.assertEqual( messages[5].content, f"This topic was moved here from #**public stream>{new_topic}** by @_**{user_profile.full_name}|{user_profile.id}**.", ) def test_notify_resolve_topic_and_move_stream(self) -> None: ( user_profile, first_stream, second_stream, msg_id, msg_id_later, ) = self.prepare_move_topics("iago", "first stream", "second stream", "test") # 'prepare_move_topics' sends 3 messages in the first_stream messages = get_topic_messages(user_profile, first_stream, "test") self.assert_length(messages, 3) # Test resolving a topic (test -> ✔ test) while changing stream (first_stream -> second_stream) new_topic = "✔ test" new_stream = second_stream result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "topic": new_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, new_stream, new_topic) self.assert_length(messages, 5) self.assertEqual( messages[3].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as resolved.", ) self.assertEqual( messages[4].content, f"This topic was moved here from #**{first_stream.name}>test** by @_**{user_profile.full_name}|{user_profile.id}**.", ) # Test unresolving a topic (✔ test -> test) while changing stream (second_stream -> first_stream) new_topic = "test" new_stream = first_stream result = self.client_patch( "/json/messages/" + str(msg_id), { "stream_id": new_stream.id, "topic": new_topic, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(user_profile, new_stream, new_topic) self.assert_length(messages, 7) self.assertEqual( messages[5].content, f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as unresolved.", ) self.assertEqual( messages[6].content, f"This topic was moved here from #**{second_stream.name}>✔ test** by @_**{user_profile.full_name}|{user_profile.id}**.", ) def parameterized_test_move_message_involving_private_stream( self, from_invite_only: bool, history_public_to_subscribers: bool, user_messages_created: bool, to_invite_only: bool = True, ) -> None: admin_user = self.example_user("iago") user_losing_access = self.example_user("cordelia") user_gaining_access = self.example_user("hamlet") self.login("iago") old_stream = self.make_stream("test move stream", invite_only=from_invite_only) new_stream = self.make_stream( "new stream", invite_only=to_invite_only, history_public_to_subscribers=history_public_to_subscribers, ) self.subscribe(admin_user, old_stream.name) self.subscribe(user_losing_access, old_stream.name) self.subscribe(admin_user, new_stream.name) self.subscribe(user_gaining_access, new_stream.name) msg_id = self.send_stream_message( admin_user, old_stream.name, topic_name="test", content="First" ) self.send_stream_message(admin_user, old_stream.name, topic_name="test", content="Second") self.assertEqual( UserMessage.objects.filter( user_profile_id=user_losing_access.id, message_id=msg_id, ).count(), 1, ) self.assertEqual( UserMessage.objects.filter( user_profile_id=user_gaining_access.id, message_id=msg_id, ).count(), 0, ) result = self.client_patch( f"/json/messages/{msg_id}", { "stream_id": new_stream.id, "propagate_mode": "change_all", }, ) self.assert_json_success(result) messages = get_topic_messages(admin_user, old_stream, "test") self.assert_length(messages, 0) messages = get_topic_messages(admin_user, new_stream, "test") self.assert_length(messages, 3) self.assertEqual( UserMessage.objects.filter( user_profile_id=user_losing_access.id, message_id=msg_id, ).count(), 0, ) # When the history is shared, UserMessage is not created for the user but the user # can see the message. self.assertEqual( UserMessage.objects.filter( user_profile_id=user_gaining_access.id, message_id=msg_id, ).count(), 1 if user_messages_created else 0, ) def test_move_message_from_public_to_private_stream_not_shared_history(self) -> None: self.parameterized_test_move_message_involving_private_stream( from_invite_only=False, history_public_to_subscribers=False, user_messages_created=True, ) def test_move_message_from_public_to_private_stream_shared_history(self) -> None: self.parameterized_test_move_message_involving_private_stream( from_invite_only=False, history_public_to_subscribers=True, user_messages_created=False, ) def test_move_message_from_private_to_private_stream_not_shared_history(self) -> None: self.parameterized_test_move_message_involving_private_stream( from_invite_only=True, history_public_to_subscribers=False, user_messages_created=True, ) def test_move_message_from_private_to_private_stream_shared_history(self) -> None: self.parameterized_test_move_message_involving_private_stream( from_invite_only=True, history_public_to_subscribers=True, user_messages_created=False, ) def test_move_message_from_private_to_public(self) -> None: self.parameterized_test_move_message_involving_private_stream( from_invite_only=True, history_public_to_subscribers=True, user_messages_created=False, to_invite_only=False, ) def test_can_move_messages_between_streams(self) -> None: def validation_func(user_profile: UserProfile) -> bool: user_profile.refresh_from_db() return user_profile.can_move_messages_between_streams() self.check_has_permission_policies("move_messages_between_streams_policy", validation_func) 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 = "topic 1" id1 = self.send_stream_message(hamlet, "new", topic_name=original_topic) id2 = self.send_stream_message(admin_user, "new", topic_name=original_topic) 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, "propagate_mode": "change_all", }, ) self.assert_json_error(result, "Nothing to change") resolved_topic = RESOLVED_TOPIC_PREFIX + original_topic 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, 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.", ) # Check topic resolved notification message is only unread for participants. assert ( UserMessage.objects.filter( user_profile__in=[admin_user, hamlet, aaron], message__id=messages[2].id ) .extra(where=[UserMessage.where_unread()]) .count() == 3 ) assert ( UserMessage.objects.filter(user_profile=cordelia, message__id=messages[2].id) .extra(where=[UserMessage.where_unread()]) .count() == 0 ) # Now move to a weird state and confirm we get the normal topic moved message. weird_topic = "✔ ✔✔" + original_topic result = self.client_patch( "/json/messages/" + str(id1), { "topic": weird_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( weird_topic, msg.topic_name(), ) messages = get_topic_messages(admin_user, stream, weird_topic) 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 = original_topic 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) 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.", ) # Check topic unresolved notification message is only unread for participants. assert ( UserMessage.objects.filter( user_profile__in=[admin_user, hamlet, aaron], message__id=messages[4].id ) .extra(where=[UserMessage.where_unread()]) .count() == 3 ) assert ( UserMessage.objects.filter(user_profile=cordelia, message__id=messages[4].id) .extra(where=[UserMessage.where_unread()]) .count() == 0 ) class DeleteMessageTest(ZulipTestCase): def test_delete_message_invalid_request_format(self) -> None: self.login("iago") hamlet = self.example_user("hamlet") msg_id = self.send_stream_message(hamlet, "Denmark") result = self.client_delete(f"/json/messages/{msg_id + 1}", {"message_id": msg_id}) self.assert_json_error(result, "Invalid message(s)") result = self.client_delete(f"/json/messages/{msg_id}") self.assert_json_success(result) def test_delete_message_by_user(self) -> None: def set_message_deleting_params( delete_own_message_policy: int, message_content_delete_limit_seconds: Union[int, str] ) -> None: self.login("iago") result = self.client_patch( "/json/realm", { "delete_own_message_policy": delete_own_message_policy, "message_content_delete_limit_seconds": orjson.dumps( message_content_delete_limit_seconds ).decode(), }, ) self.assert_json_success(result) def test_delete_message_by_admin(msg_id: int) -> "TestHttpResponse": self.login("iago") result = self.client_delete(f"/json/messages/{msg_id}") return result def test_delete_message_by_owner(msg_id: int) -> "TestHttpResponse": self.login("hamlet") result = self.client_delete(f"/json/messages/{msg_id}") return result def test_delete_message_by_other_user(msg_id: int) -> "TestHttpResponse": self.login("cordelia") result = self.client_delete(f"/json/messages/{msg_id}") return result # Test if message deleting is not allowed(default). set_message_deleting_params(Realm.POLICY_ADMINS_ONLY, "unlimited") hamlet = self.example_user("hamlet") self.login_user(hamlet) msg_id = self.send_stream_message(hamlet, "Denmark") result = test_delete_message_by_owner(msg_id=msg_id) self.assert_json_error(result, "You don't have permission to delete this message") result = test_delete_message_by_other_user(msg_id=msg_id) self.assert_json_error(result, "You don't have permission to delete this message") result = test_delete_message_by_admin(msg_id=msg_id) self.assert_json_success(result) # Test if message deleting is allowed. # Test if time limit is None(no limit). set_message_deleting_params(Realm.POLICY_EVERYONE, "unlimited") msg_id = self.send_stream_message(hamlet, "Denmark") message = Message.objects.get(id=msg_id) message.date_sent = message.date_sent - datetime.timedelta(seconds=600) message.save() result = test_delete_message_by_other_user(msg_id=msg_id) self.assert_json_error(result, "You don't have permission to delete this message") result = test_delete_message_by_owner(msg_id=msg_id) self.assert_json_success(result) # Test if time limit is non-zero. set_message_deleting_params(Realm.POLICY_EVERYONE, 240) msg_id_1 = self.send_stream_message(hamlet, "Denmark") message = Message.objects.get(id=msg_id_1) message.date_sent = message.date_sent - datetime.timedelta(seconds=120) message.save() msg_id_2 = self.send_stream_message(hamlet, "Denmark") message = Message.objects.get(id=msg_id_2) message.date_sent = message.date_sent - datetime.timedelta(seconds=360) message.save() result = test_delete_message_by_other_user(msg_id=msg_id_1) self.assert_json_error(result, "You don't have permission to delete this message") result = test_delete_message_by_owner(msg_id=msg_id_1) self.assert_json_success(result) result = test_delete_message_by_owner(msg_id=msg_id_2) self.assert_json_error(result, "The time limit for deleting this message has passed") # No limit for admin. result = test_delete_message_by_admin(msg_id=msg_id_2) self.assert_json_success(result) # Test multiple delete requests with no latency issues msg_id = self.send_stream_message(hamlet, "Denmark") result = test_delete_message_by_owner(msg_id=msg_id) self.assert_json_success(result) result = test_delete_message_by_owner(msg_id=msg_id) self.assert_json_error(result, "Invalid message(s)") # Test handling of 500 error caused by multiple delete requests due to latency. # see issue #11219. with mock.patch("zerver.views.message_edit.do_delete_messages") as m, mock.patch( "zerver.views.message_edit.validate_can_delete_message", return_value=None ), mock.patch("zerver.views.message_edit.access_message", return_value=(None, None)): m.side_effect = IntegrityError() result = test_delete_message_by_owner(msg_id=msg_id) self.assert_json_error(result, "Message already deleted") m.side_effect = Message.DoesNotExist() result = test_delete_message_by_owner(msg_id=msg_id) self.assert_json_error(result, "Message already deleted") def test_delete_message_according_to_delete_own_message_policy(self) -> None: def check_delete_message_by_sender( sender_name: str, error_msg: Optional[str] = None ) -> None: sender = self.example_user(sender_name) msg_id = self.send_stream_message(sender, "Verona") self.login_user(sender) result = self.client_delete(f"/json/messages/{msg_id}") if error_msg is None: self.assert_json_success(result) else: self.assert_json_error(result, error_msg) realm = get_realm("zulip") do_set_realm_property( realm, "delete_own_message_policy", Realm.POLICY_ADMINS_ONLY, acting_user=None ) check_delete_message_by_sender("shiva", "You don't have permission to delete this message") check_delete_message_by_sender("iago") do_set_realm_property( realm, "delete_own_message_policy", Realm.POLICY_MODERATORS_ONLY, acting_user=None ) check_delete_message_by_sender( "cordelia", "You don't have permission to delete this message" ) check_delete_message_by_sender("shiva") do_set_realm_property( realm, "delete_own_message_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None ) check_delete_message_by_sender( "polonius", "You don't have permission to delete this message" ) check_delete_message_by_sender("cordelia") do_set_realm_property( realm, "delete_own_message_policy", Realm.POLICY_FULL_MEMBERS_ONLY, acting_user=None ) do_set_realm_property(realm, "waiting_period_threshold", 10, acting_user=None) cordelia = self.example_user("cordelia") cordelia.date_joined = timezone_now() - datetime.timedelta(days=9) cordelia.save() check_delete_message_by_sender( "cordelia", "You don't have permission to delete this message" ) cordelia.date_joined = timezone_now() - datetime.timedelta(days=11) cordelia.save() check_delete_message_by_sender("cordelia") do_set_realm_property( realm, "delete_own_message_policy", Realm.POLICY_EVERYONE, acting_user=None ) check_delete_message_by_sender("cordelia") check_delete_message_by_sender("polonius") def test_delete_event_sent_after_transaction_commits(self) -> None: """ Tests that `send_event` is hooked to `transaction.on_commit`. This is important, because we don't want to end up holding locks on message rows for too long if the event queue runs into a problem. """ hamlet = self.example_user("hamlet") self.send_stream_message(hamlet, "Denmark") message = self.get_last_message() with self.tornado_redirected_to_list([], expected_num_events=1): with mock.patch("zerver.actions.message_edit.send_event") as m: m.side_effect = AssertionError( "Events should be sent only after the transaction commits." ) do_delete_messages(hamlet.realm, [message])