Files
zulip/zerver/tests/test_message_move_topic.py
Pedro Almeida ddfc2d230f resolve_topic: Prevent incorrect notification during message move.
This commit fixes the bug where the "topic unresolved" notification
is wrongly triggered when moving a message between a resolved and
unresolved topic, except for when the topics have the same name.

To resolve this issue, the commit ensures that resolved/unresolved
notifications are not sent if a message has been moved to a new
topic. This is achieved by comparing the names of the old and new
topics without considering the "resolved prefix".

The commit also accounts for the scenario where `new_topic_name`
has been truncated, indicating that it was resolved and the name
had to change to accommodate the "resolved prefix".

This solution does not try to specially handle the possible case that
a stream has two topics with the same name, even if one is resolved
and another unresolved.

Fixes #29007.
2024-07-07 11:38:14 -07:00

1756 lines
72 KiB
Python

from datetime import timedelta
from typing import Any, Dict, List
from unittest import mock
import orjson
import time_machine
from django.test import override_settings
from django.utils.timezone import now as timezone_now
from zerver.actions.message_delete import do_delete_messages
from zerver.actions.message_edit import (
check_update_message,
do_update_message,
maybe_send_resolve_topic_notifications,
)
from zerver.actions.reactions import do_add_reaction
from zerver.actions.realm_settings import do_set_realm_property
from zerver.actions.user_topics import do_set_user_topic_visibility_policy
from zerver.lib.message import truncate_topic
from zerver.lib.test_classes import ZulipTestCase, get_topic_messages
from zerver.lib.topic import RESOLVED_TOPIC_PREFIX, messages_for_topic
from zerver.lib.user_topics import (
get_users_with_user_topic_visibility_policy,
set_topic_visibility_policy,
topic_has_visibility_policy,
)
from zerver.lib.utils import assert_is_not_none
from zerver.models import Message, UserMessage, UserProfile, UserTopic
from zerver.models.constants import MAX_TOPIC_NAME_LENGTH
from zerver.models.realms import CommonMessagePolicyEnum
from zerver.models.streams import Stream
class MessageMoveTopicTest(ZulipTestCase):
def check_topic(self, msg_id: int, topic_name: str) -> None:
msg = Message.objects.get(id=msg_id)
self.assertEqual(msg.topic_name(), topic_name)
def assert_has_visibility_policy(
self,
user_profile: UserProfile,
topic_name: str,
stream: Stream,
visibility_policy: int,
*,
expected: bool = True,
) -> None:
if expected:
self.assertTrue(
topic_has_visibility_policy(user_profile, stream.id, topic_name, visibility_policy)
)
else:
self.assertFalse(
topic_has_visibility_policy(user_profile, stream.id, topic_name, visibility_policy)
)
def test_private_message_edit_topic(self) -> None:
hamlet = self.example_user("hamlet")
self.login("hamlet")
cordelia = self.example_user("cordelia")
msg_id = self.send_personal_message(hamlet, cordelia)
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": "Should not exist",
},
)
self.assert_json_error(result, "Direct messages cannot have topics.")
def test_propagate_invalid(self) -> None:
self.login("hamlet")
id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1")
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": "edited",
"propagate_mode": "invalid",
},
)
self.assert_json_error(result, "Invalid propagate_mode")
self.check_topic(id1, topic_name="topic1")
result = self.client_patch(
"/json/messages/" + str(id1),
{
"content": "edited",
"propagate_mode": "change_all",
},
)
self.assert_json_error(result, "Invalid propagate_mode without topic edit")
self.check_topic(id1, topic_name="topic1")
def test_edit_message_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!")
@mock.patch("zerver.actions.message_edit.send_event_on_commit")
def test_edit_topic_public_history_stream(self, mock_send_event: mock.MagicMock) -> None:
stream_name = "Macbeth"
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
self.make_stream(stream_name, history_public_to_subscribers=True)
self.subscribe(hamlet, stream_name)
self.login_user(hamlet)
message_id = self.send_stream_message(hamlet, stream_name, "Where am I?")
self.login_user(cordelia)
self.subscribe(cordelia, stream_name)
message = Message.objects.get(id=message_id)
def do_update_message_topic_success(
user_profile: UserProfile,
message: Message,
topic_name: str,
users_to_be_notified: List[Dict[str, Any]],
) -> None:
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.user_topics.send_event_on_commit")
def test_edit_muted_topic(self, mock_send_event_on_commit: mock.MagicMock) -> None:
stream_name = "Stream 123"
stream = self.make_stream(stream_name)
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
aaron = self.example_user("aaron")
self.subscribe(hamlet, stream_name)
self.login_user(hamlet)
message_id = self.send_stream_message(
hamlet, stream_name, topic_name="Topic1", content="Hello World"
)
self.subscribe(cordelia, stream_name)
self.login_user(cordelia)
self.subscribe(aaron, stream_name)
self.login_user(aaron)
def assert_is_topic_muted(
user_profile: UserProfile,
stream_id: int,
topic_name: str,
*,
muted: bool,
) -> None:
if muted:
self.assertTrue(
topic_has_visibility_policy(
user_profile, stream_id, topic_name, UserTopic.VisibilityPolicy.MUTED
)
)
else:
self.assertFalse(
topic_has_visibility_policy(
user_profile, stream_id, topic_name, UserTopic.VisibilityPolicy.MUTED
)
)
already_muted_topic_name = "Already muted topic"
muted_topics = [
[stream_name, "Topic1"],
[stream_name, "Topic2"],
[stream_name, already_muted_topic_name],
]
set_topic_visibility_policy(hamlet, muted_topics, UserTopic.VisibilityPolicy.MUTED)
set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED)
# users that need to be notified by send_event in the case of change-topic-name operation.
users_to_be_notified_via_muted_topics_event: List[int] = []
users_to_be_notified_via_user_topic_event: List[int] = []
for user_topic in get_users_with_user_topic_visibility_policy(stream.id, "Topic1"):
# We are appending the same data twice because 'user_topic' event notifies
# the user during delete and create operation.
users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id)
users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id)
# 'muted_topics' event notifies the user of muted topics during create
# operation only.
users_to_be_notified_via_muted_topics_event.append(user_topic.user_profile_id)
change_all_topic_name = "Topic 1 edited"
# Verify how many total database queries are required. We
# expect 6 queries (4/visibility_policy to update the muted
# state + 1/user with a UserTopic row for the events data)
# beyond what is typical were there not UserTopic records to
# update. Ideally, we'd eliminate the per-user component.
with self.assert_database_query_count(25):
check_update_message(
user_profile=hamlet,
message_id=message_id,
stream_id=None,
topic_name=change_all_topic_name,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
# Extract the send_event call where event type is 'user_topic' or 'muted_topics.
# Here we assert that the expected users are notified properly.
users_notified_via_muted_topics_event: List[int] = []
users_notified_via_user_topic_event: List[int] = []
for call_args in mock_send_event_on_commit.call_args_list:
(arg_realm, arg_event, arg_notified_users) = call_args[0]
if arg_event["type"] == "user_topic":
users_notified_via_user_topic_event.append(*arg_notified_users)
elif arg_event["type"] == "muted_topics":
users_notified_via_muted_topics_event.append(*arg_notified_users)
self.assertEqual(
sorted(users_notified_via_muted_topics_event),
sorted(users_to_be_notified_via_muted_topics_event),
)
self.assertEqual(
sorted(users_notified_via_user_topic_event),
sorted(users_to_be_notified_via_user_topic_event),
)
assert_is_topic_muted(hamlet, stream.id, "Topic1", muted=False)
assert_is_topic_muted(cordelia, stream.id, "Topic1", muted=False)
assert_is_topic_muted(aaron, stream.id, "Topic1", muted=False)
assert_is_topic_muted(hamlet, stream.id, "Topic2", muted=True)
assert_is_topic_muted(cordelia, stream.id, "Topic2", muted=True)
assert_is_topic_muted(aaron, stream.id, "Topic2", muted=False)
assert_is_topic_muted(hamlet, stream.id, change_all_topic_name, muted=True)
assert_is_topic_muted(cordelia, stream.id, change_all_topic_name, muted=True)
assert_is_topic_muted(aaron, stream.id, change_all_topic_name, muted=False)
change_later_topic_name = "Topic 1 edited again"
check_update_message(
user_profile=hamlet,
message_id=message_id,
stream_id=None,
topic_name=change_later_topic_name,
propagate_mode="change_later",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
assert_is_topic_muted(hamlet, stream.id, change_all_topic_name, muted=False)
assert_is_topic_muted(hamlet, stream.id, change_later_topic_name, muted=True)
# Make sure we safely handle the case of the new topic being already muted.
check_update_message(
user_profile=hamlet,
message_id=message_id,
stream_id=None,
topic_name=already_muted_topic_name,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
assert_is_topic_muted(hamlet, stream.id, change_later_topic_name, muted=False)
assert_is_topic_muted(hamlet, stream.id, already_muted_topic_name, muted=True)
change_one_topic_name = "Topic 1 edited change_one"
check_update_message(
user_profile=hamlet,
message_id=message_id,
stream_id=None,
topic_name=change_one_topic_name,
propagate_mode="change_one",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
assert_is_topic_muted(hamlet, stream.id, change_one_topic_name, muted=True)
assert_is_topic_muted(hamlet, stream.id, change_later_topic_name, muted=False)
# Move topic between two public streams.
desdemona = self.example_user("desdemona")
message_id = self.send_stream_message(
hamlet, stream_name, topic_name="New topic", content="Hello World"
)
new_public_stream = self.make_stream("New public stream")
self.subscribe(desdemona, new_public_stream.name)
self.login_user(desdemona)
muted_topics = [
[stream_name, "New topic"],
]
set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED)
set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED)
with self.assert_database_query_count(27):
check_update_message(
user_profile=desdemona,
message_id=message_id,
stream_id=new_public_stream.id,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
assert_is_topic_muted(desdemona, stream.id, "New topic", muted=False)
assert_is_topic_muted(cordelia, stream.id, "New topic", muted=False)
assert_is_topic_muted(aaron, stream.id, "New topic", muted=False)
assert_is_topic_muted(desdemona, new_public_stream.id, "New topic", muted=True)
assert_is_topic_muted(cordelia, new_public_stream.id, "New topic", muted=True)
assert_is_topic_muted(aaron, new_public_stream.id, "New topic", muted=False)
# Move topic to a private stream.
message_id = self.send_stream_message(
hamlet, stream_name, topic_name="New topic", content="Hello World"
)
new_private_stream = self.make_stream("New private stream", invite_only=True)
self.subscribe(desdemona, new_private_stream.name)
self.login_user(desdemona)
muted_topics = [
[stream_name, "New topic"],
]
set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED)
set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED)
with self.assert_database_query_count(33):
check_update_message(
user_profile=desdemona,
message_id=message_id,
stream_id=new_private_stream.id,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
# Cordelia is not subscribed to the private stream, so
# Cordelia should have had the topic unmuted, while Desdemona
# should have had her muted topic record moved.
assert_is_topic_muted(desdemona, stream.id, "New topic", muted=False)
assert_is_topic_muted(cordelia, stream.id, "New topic", muted=False)
assert_is_topic_muted(aaron, stream.id, "New topic", muted=False)
assert_is_topic_muted(desdemona, new_private_stream.id, "New topic", muted=True)
assert_is_topic_muted(cordelia, new_private_stream.id, "New topic", muted=False)
assert_is_topic_muted(aaron, new_private_stream.id, "New topic", muted=False)
# Move topic between two public streams with change in topic name.
desdemona = self.example_user("desdemona")
message_id = self.send_stream_message(
hamlet, stream_name, topic_name="New topic 2", content="Hello World"
)
self.login_user(desdemona)
muted_topics = [
[stream_name, "New topic 2"],
]
set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED)
set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED)
with self.assert_database_query_count(27):
check_update_message(
user_profile=desdemona,
message_id=message_id,
stream_id=new_public_stream.id,
topic_name="changed topic name",
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
assert_is_topic_muted(desdemona, stream.id, "New topic 2", muted=False)
assert_is_topic_muted(cordelia, stream.id, "New topic 2", muted=False)
assert_is_topic_muted(aaron, stream.id, "New topic 2", muted=False)
assert_is_topic_muted(desdemona, new_public_stream.id, "changed topic name", muted=True)
assert_is_topic_muted(cordelia, new_public_stream.id, "changed topic name", muted=True)
assert_is_topic_muted(aaron, new_public_stream.id, "changed topic name", muted=False)
# Moving only half the messages doesn't move UserTopic records.
second_message_id = self.send_stream_message(
hamlet, stream_name, topic_name="changed topic name", content="Second message"
)
with self.assert_database_query_count(22):
check_update_message(
user_profile=desdemona,
message_id=second_message_id,
stream_id=new_public_stream.id,
topic_name="final topic name",
propagate_mode="change_later",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
assert_is_topic_muted(desdemona, new_public_stream.id, "changed topic name", muted=True)
assert_is_topic_muted(cordelia, new_public_stream.id, "changed topic name", muted=True)
assert_is_topic_muted(aaron, new_public_stream.id, "changed topic name", muted=False)
assert_is_topic_muted(desdemona, new_public_stream.id, "final topic name", muted=False)
assert_is_topic_muted(cordelia, new_public_stream.id, "final topic name", muted=False)
assert_is_topic_muted(aaron, new_public_stream.id, "final topic name", muted=False)
@mock.patch("zerver.actions.user_topics.send_event_on_commit")
def test_edit_unmuted_topic(self, mock_send_event_on_commit: mock.MagicMock) -> None:
stream_name = "Stream 123"
stream = self.make_stream(stream_name)
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
aaron = self.example_user("aaron")
othello = self.example_user("othello")
self.subscribe(hamlet, stream_name)
self.login_user(hamlet)
message_id = self.send_stream_message(
hamlet, stream_name, topic_name="Topic1", content="Hello World"
)
self.subscribe(cordelia, stream_name)
self.login_user(cordelia)
self.subscribe(aaron, stream_name)
self.login_user(aaron)
self.subscribe(othello, stream_name)
self.login_user(othello)
# Initially, hamlet and othello set visibility_policy as UNMUTED for 'Topic1' and 'Topic2',
# cordelia sets visibility_policy as MUTED for 'Topic1' and 'Topic2', while
# aaron doesn't have a visibility_policy set for 'Topic1' or 'Topic2'.
#
# After moving messages from 'Topic1' to 'Topic 1 edited', the expected behaviour is:
# hamlet and othello have UNMUTED 'Topic 1 edited' and no visibility_policy set for 'Topic1'
# cordelia has MUTED 'Topic 1 edited' and no visibility_policy set for 'Topic1'
#
# There is no change in visibility_policy configurations for 'Topic2', i.e.
# hamlet and othello have UNMUTED 'Topic2' + cordelia has MUTED 'Topic2'
# aaron still doesn't have visibility_policy set for any topic.
#
# Note: We have used two users with UNMUTED 'Topic1' to verify that the query count
# doesn't increase (in order to update UserTopic records) with an increase in users.
# (We are using bulk database operations.)
# 1 query/user is added in order to send muted_topics event.(which will be deprecated)
topics = [
[stream_name, "Topic1"],
[stream_name, "Topic2"],
]
set_topic_visibility_policy(hamlet, topics, UserTopic.VisibilityPolicy.UNMUTED)
set_topic_visibility_policy(cordelia, topics, UserTopic.VisibilityPolicy.MUTED)
set_topic_visibility_policy(othello, topics, UserTopic.VisibilityPolicy.UNMUTED)
# users that need to be notified by send_event in the case of change-topic-name operation.
users_to_be_notified_via_muted_topics_event: List[int] = []
users_to_be_notified_via_user_topic_event: List[int] = []
for user_topic in get_users_with_user_topic_visibility_policy(stream.id, "Topic1"):
# We are appending the same data twice because 'user_topic' event notifies
# the user during delete and create operation.
users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id)
users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id)
# 'muted_topics' event notifies the user of muted topics during create
# operation only.
users_to_be_notified_via_muted_topics_event.append(user_topic.user_profile_id)
change_all_topic_name = "Topic 1 edited"
with self.assert_database_query_count(30):
check_update_message(
user_profile=hamlet,
message_id=message_id,
stream_id=None,
topic_name=change_all_topic_name,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
# Extract the send_event call where event type is 'user_topic' or 'muted_topics.
# Here we assert that the expected users are notified properly.
users_notified_via_muted_topics_event: List[int] = []
users_notified_via_user_topic_event: List[int] = []
for call_args in mock_send_event_on_commit.call_args_list:
(arg_realm, arg_event, arg_notified_users) = call_args[0]
if arg_event["type"] == "user_topic":
users_notified_via_user_topic_event.append(*arg_notified_users)
elif arg_event["type"] == "muted_topics":
users_notified_via_muted_topics_event.append(*arg_notified_users)
self.assertEqual(
sorted(users_notified_via_muted_topics_event),
sorted(users_to_be_notified_via_muted_topics_event),
)
self.assertEqual(
sorted(users_notified_via_user_topic_event),
sorted(users_to_be_notified_via_user_topic_event),
)
# No visibility_policy set for 'Topic1'
self.assert_has_visibility_policy(
hamlet, "Topic1", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False
)
self.assert_has_visibility_policy(
cordelia, "Topic1", stream, UserTopic.VisibilityPolicy.MUTED, expected=False
)
self.assert_has_visibility_policy(
othello, "Topic1", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False
)
self.assert_has_visibility_policy(
aaron, "Topic1", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False
)
# No change in visibility_policy configurations for 'Topic2'
self.assert_has_visibility_policy(
hamlet, "Topic2", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=True
)
self.assert_has_visibility_policy(
cordelia, "Topic2", stream, UserTopic.VisibilityPolicy.MUTED, expected=True
)
self.assert_has_visibility_policy(
othello, "Topic2", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=True
)
self.assert_has_visibility_policy(
aaron, "Topic2", stream, UserTopic.VisibilityPolicy.UNMUTED, expected=False
)
# UserTopic records moved to 'Topic 1 edited' after move-topic operation.
self.assert_has_visibility_policy(
hamlet, change_all_topic_name, stream, UserTopic.VisibilityPolicy.UNMUTED, expected=True
)
self.assert_has_visibility_policy(
cordelia, change_all_topic_name, stream, UserTopic.VisibilityPolicy.MUTED, expected=True
)
self.assert_has_visibility_policy(
othello,
change_all_topic_name,
stream,
UserTopic.VisibilityPolicy.UNMUTED,
expected=True,
)
self.assert_has_visibility_policy(
aaron, change_all_topic_name, stream, UserTopic.VisibilityPolicy.MUTED, expected=False
)
def test_merge_user_topic_states_on_move_messages(self) -> None:
stream_name = "Stream 123"
stream = self.make_stream(stream_name)
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
aaron = self.example_user("aaron")
self.subscribe(hamlet, stream_name)
self.login_user(hamlet)
self.subscribe(cordelia, stream_name)
self.login_user(cordelia)
self.subscribe(aaron, stream_name)
self.login_user(aaron)
# Test the following cases:
#
# orig_topic | target_topic | final behaviour
# INHERIT INHERIT INHERIT
# INHERIT MUTED INHERIT
# INHERIT UNMUTED UNMUTED
orig_topic = "Topic1"
target_topic = "Topic1 edited"
orig_message_id = self.send_stream_message(
hamlet, stream_name, topic_name=orig_topic, content="Hello World"
)
self.send_stream_message(
hamlet, stream_name, topic_name=target_topic, content="Hello World 2"
)
# By default:
# visibility_policy of 'hamlet', 'cordelia', 'aaron' for 'orig_topic': INHERIT
# visibility_policy of 'hamlet' for 'target_topic': INHERIT
#
# So we don't need to manually set visibility_policy to INHERIT whenever required,
# here and later in this test.
do_set_user_topic_visibility_policy(
cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
do_set_user_topic_visibility_policy(
aaron, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
check_update_message(
user_profile=hamlet,
message_id=orig_message_id,
stream_id=None,
topic_name=target_topic,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
self.assert_has_visibility_policy(
hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
hamlet, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
cordelia, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
aaron, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED
)
# Test the following cases:
#
# orig_topic | target_topic | final behaviour
# MUTED INHERIT INHERIT
# MUTED MUTED MUTED
# MUTED UNMUTED UNMUTED
orig_topic = "Topic2"
target_topic = "Topic2 edited"
orig_message_id = self.send_stream_message(
hamlet, stream_name, topic_name=orig_topic, content="Hello World"
)
self.send_stream_message(
hamlet, stream_name, topic_name=target_topic, content="Hello World 2"
)
do_set_user_topic_visibility_policy(
hamlet, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
do_set_user_topic_visibility_policy(
cordelia, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
do_set_user_topic_visibility_policy(
aaron, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
do_set_user_topic_visibility_policy(
cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
do_set_user_topic_visibility_policy(
aaron, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
check_update_message(
user_profile=hamlet,
message_id=orig_message_id,
stream_id=None,
topic_name=target_topic,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
self.assert_has_visibility_policy(
hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
hamlet, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
cordelia, target_topic, stream, UserTopic.VisibilityPolicy.MUTED
)
self.assert_has_visibility_policy(
aaron, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED
)
# Test the following cases:
#
# orig_topic | target_topic | final behaviour
# UNMUTED INHERIT UNMUTED
# UNMUTED MUTED UNMUTED
# UNMUTED UNMUTED UNMUTED
orig_topic = "Topic3"
target_topic = "Topic3 edited"
orig_message_id = self.send_stream_message(
hamlet, stream_name, topic_name=orig_topic, content="Hello World"
)
self.send_stream_message(
hamlet, stream_name, topic_name=target_topic, content="Hello World 2"
)
do_set_user_topic_visibility_policy(
hamlet, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
do_set_user_topic_visibility_policy(
cordelia, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
do_set_user_topic_visibility_policy(
aaron, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
do_set_user_topic_visibility_policy(
cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
do_set_user_topic_visibility_policy(
aaron, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
check_update_message(
user_profile=hamlet,
message_id=orig_message_id,
stream_id=None,
topic_name=target_topic,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
self.assert_has_visibility_policy(
hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
hamlet, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED
)
self.assert_has_visibility_policy(
cordelia, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED
)
self.assert_has_visibility_policy(
aaron, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED
)
def test_user_topic_states_on_moving_to_topic_with_no_messages(self) -> None:
stream_name = "Stream 123"
stream = self.make_stream(stream_name)
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
aaron = self.example_user("aaron")
self.subscribe(hamlet, stream_name)
self.subscribe(cordelia, stream_name)
self.subscribe(aaron, stream_name)
# Test the case where target topic has no messages:
#
# orig_topic | final behaviour
# INHERIT INHERIT
# UNMUTED UNMUTED
# MUTED MUTED
orig_topic = "Topic1"
target_topic = "Topic1 edited"
orig_message_id = self.send_stream_message(
hamlet, stream_name, topic_name=orig_topic, content="Hello World"
)
do_set_user_topic_visibility_policy(
hamlet, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
do_set_user_topic_visibility_policy(
cordelia, stream, orig_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
check_update_message(
user_profile=hamlet,
message_id=orig_message_id,
stream_id=None,
topic_name=target_topic,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
self.assert_has_visibility_policy(
hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
hamlet, target_topic, stream, UserTopic.VisibilityPolicy.UNMUTED
)
self.assert_has_visibility_policy(
cordelia, target_topic, stream, UserTopic.VisibilityPolicy.MUTED
)
self.assert_has_visibility_policy(
aaron, target_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
def test_user_topic_state_for_messages_deleted_from_target_topic(
orig_topic: str, target_topic: str, original_topic_state: int
) -> None:
# Test the case where target topic has no messages but has UserTopic row
# due to messages being deleted from the target topic.
orig_message_id = self.send_stream_message(
hamlet, stream_name, topic_name=orig_topic, content="Hello World"
)
target_message_id = self.send_stream_message(
hamlet, stream_name, topic_name=target_topic, content="Hello World"
)
if original_topic_state != UserTopic.VisibilityPolicy.INHERIT:
users = [hamlet, cordelia, aaron]
for user in users:
do_set_user_topic_visibility_policy(
user, stream, orig_topic, visibility_policy=original_topic_state
)
do_set_user_topic_visibility_policy(
hamlet, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
)
do_set_user_topic_visibility_policy(
cordelia, stream, target_topic, visibility_policy=UserTopic.VisibilityPolicy.MUTED
)
# Delete the message in target topic to make it empty.
self.login("hamlet")
do_set_realm_property(
hamlet.realm,
"delete_own_message_policy",
CommonMessagePolicyEnum.MEMBERS_ONLY,
acting_user=None,
)
self.client_delete(f"/json/messages/{target_message_id}")
check_update_message(
user_profile=hamlet,
message_id=orig_message_id,
stream_id=None,
topic_name=target_topic,
propagate_mode="change_all",
send_notification_to_old_thread=False,
send_notification_to_new_thread=False,
content=None,
)
self.assert_has_visibility_policy(
hamlet, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
cordelia, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(
aaron, orig_topic, stream, UserTopic.VisibilityPolicy.INHERIT
)
self.assert_has_visibility_policy(hamlet, target_topic, stream, original_topic_state)
self.assert_has_visibility_policy(cordelia, target_topic, stream, original_topic_state)
self.assert_has_visibility_policy(aaron, target_topic, stream, original_topic_state)
# orig_topic | target_topic | final behaviour
# INHERIT INHERIT INHERIT
# INHERIT UNMUTED INHERIT
# INHERIT MUTED INHERIT
test_user_topic_state_for_messages_deleted_from_target_topic(
orig_topic="Topic2",
target_topic="Topic2 edited",
original_topic_state=UserTopic.VisibilityPolicy.INHERIT,
)
# orig_topic | target_topic | final behaviour
# MUTED INHERIT MUTED
# MUTED UNMUTED MUTED
# MUTED MUTED MUTED
test_user_topic_state_for_messages_deleted_from_target_topic(
orig_topic="Topic3",
target_topic="Topic3 edited",
original_topic_state=UserTopic.VisibilityPolicy.MUTED,
)
# orig_topic | target_topic | final behaviour
# UNMUTED INHERIT UNMUTED
# UNMUTED UNMUTED UNMUTED
# UNMUTED MUTED UNMUTED
test_user_topic_state_for_messages_deleted_from_target_topic(
orig_topic="Topic4",
target_topic="Topic4 edited",
original_topic_state=UserTopic.VisibilityPolicy.UNMUTED,
)
def test_topic_edit_history_saved_in_all_message(self) -> None:
self.login("hamlet")
id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1")
id2 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1")
id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topic1")
id4 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic2")
id5 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1")
def verify_edit_history(new_topic_name: str, len_edit_history: int) -> None:
for msg_id in [id1, id2, id5]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(
new_topic_name,
msg.topic_name(),
)
# Since edit history is being generated by do_update_message,
# it's contents can vary over time; So, to keep this test
# future proof, we only verify it's length.
self.assert_length(
orjson.loads(assert_is_not_none(msg.edit_history)), len_edit_history
)
for msg_id in [id3, id4]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(msg.edit_history, None)
new_topic_name = "edited"
result = self.client_patch(
f"/json/messages/{id1}",
{
"topic": new_topic_name,
"propagate_mode": "change_later",
},
)
self.assert_json_success(result)
verify_edit_history(new_topic_name, 1)
new_topic_name = "edited2"
result = self.client_patch(
f"/json/messages/{id1}",
{
"topic": new_topic_name,
"propagate_mode": "change_later",
},
)
self.assert_json_success(result)
verify_edit_history(new_topic_name, 2)
def test_topic_and_content_edit(self) -> None:
self.login("hamlet")
id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", "message 1", "topic")
id2 = self.send_stream_message(self.example_user("iago"), "Denmark", "message 2", "topic")
id3 = self.send_stream_message(self.example_user("hamlet"), "Denmark", "message 3", "topic")
new_topic_name = "edited"
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": new_topic_name,
"propagate_mode": "change_later",
"content": "edited message",
},
)
self.assert_json_success(result)
# Content change of only id1 should come in edit history
# and topic change should be present in all the messages.
msg1 = Message.objects.get(id=id1)
msg2 = Message.objects.get(id=id2)
msg3 = Message.objects.get(id=id3)
msg1_edit_history = orjson.loads(assert_is_not_none(msg1.edit_history))
self.assertTrue("prev_content" in msg1_edit_history[0])
for msg in [msg2, msg3]:
self.assertFalse(
"prev_content" in orjson.loads(assert_is_not_none(msg.edit_history))[0]
)
for msg in [msg1, msg2, msg3]:
self.assertEqual(
new_topic_name,
msg.topic_name(),
)
self.assert_length(orjson.loads(assert_is_not_none(msg.edit_history)), 1)
def test_propagate_topic_forward(self) -> None:
self.login("hamlet")
id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1")
id2 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1")
id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topic1")
id4 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic2")
id5 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1")
result = self.client_patch(
f"/json/messages/{id1}",
{
"topic": "edited",
"propagate_mode": "change_later",
},
)
self.assert_json_success(result)
self.check_topic(id1, topic_name="edited")
self.check_topic(id2, topic_name="edited")
self.check_topic(id3, topic_name="topic1")
self.check_topic(id4, topic_name="topic2")
self.check_topic(id5, topic_name="edited")
def test_propagate_all_topics(self) -> None:
self.login("hamlet")
id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1")
id2 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1")
id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topic1")
id4 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic2")
id5 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic1")
id6 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="topic3")
result = self.client_patch(
f"/json/messages/{id2}",
{
"topic": "edited",
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
self.check_topic(id1, topic_name="edited")
self.check_topic(id2, topic_name="edited")
self.check_topic(id3, topic_name="topic1")
self.check_topic(id4, topic_name="topic2")
self.check_topic(id5, topic_name="edited")
self.check_topic(id6, topic_name="topic3")
def test_propagate_all_topics_with_different_uppercase_letters(self) -> None:
self.login("hamlet")
id1 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="topic1")
id2 = self.send_stream_message(self.example_user("hamlet"), "Denmark", topic_name="Topic1")
id3 = self.send_stream_message(self.example_user("iago"), "Verona", topic_name="topiC1")
id4 = self.send_stream_message(self.example_user("iago"), "Denmark", topic_name="toPic1")
result = self.client_patch(
f"/json/messages/{id2}",
{
"topic": "edited",
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
self.check_topic(id1, topic_name="edited")
self.check_topic(id2, topic_name="edited")
self.check_topic(id3, topic_name="topiC1")
self.check_topic(id4, topic_name="edited")
def test_change_all_propagate_mode_for_moving_from_stream_with_restricted_history(self) -> None:
self.make_stream("privatestream", invite_only=True, history_public_to_subscribers=False)
iago = self.example_user("iago")
cordelia = self.example_user("cordelia")
self.subscribe(iago, "privatestream")
self.subscribe(cordelia, "privatestream")
id1 = self.send_stream_message(iago, "privatestream", topic_name="topic1")
id2 = self.send_stream_message(iago, "privatestream", topic_name="topic1")
hamlet = self.example_user("hamlet")
self.subscribe(hamlet, "privatestream")
id3 = self.send_stream_message(iago, "privatestream", topic_name="topic1")
id4 = self.send_stream_message(hamlet, "privatestream", topic_name="topic1")
self.send_stream_message(hamlet, "privatestream", topic_name="topic1")
message = Message.objects.get(id=id1)
message.date_sent = message.date_sent - timedelta(days=10)
message.save()
message = Message.objects.get(id=id2)
message.date_sent = message.date_sent - timedelta(days=9)
message.save()
message = Message.objects.get(id=id3)
message.date_sent = message.date_sent - timedelta(days=8)
message.save()
message = Message.objects.get(id=id4)
message.date_sent = message.date_sent - timedelta(days=6)
message.save()
self.login("hamlet")
result = self.client_patch(
f"/json/messages/{id4}",
{
"topic": "edited",
"propagate_mode": "change_all",
"send_notification_to_new_thread": "false",
},
)
self.assert_json_error(
result,
"You only have permission to move the 2/3 most recent messages in this topic.",
)
self.login("cordelia")
result = self.client_patch(
f"/json/messages/{id4}",
{
"topic": "edited",
"propagate_mode": "change_all",
"send_notification_to_new_thread": "false",
},
)
self.assert_json_error(
result,
"You only have permission to move the 2/5 most recent messages in this topic.",
)
def test_notify_new_topic(self) -> None:
user_profile = self.example_user("iago")
self.login("iago")
stream = self.make_stream("public stream")
self.subscribe(user_profile, stream.name)
msg_id = self.send_stream_message(
user_profile, stream.name, topic_name="test", content="First"
)
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second")
self.send_stream_message(user_profile, stream.name, topic_name="test", content="third")
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": "edited",
"propagate_mode": "change_all",
"send_notification_to_old_thread": "false",
"send_notification_to_new_thread": "true",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, "test")
self.assert_length(messages, 0)
messages = get_topic_messages(user_profile, stream, "edited")
self.assert_length(messages, 4)
self.assertEqual(
messages[3].content,
f"This topic was moved here from #**public stream>test** by @_**Iago|{user_profile.id}**.",
)
def test_notify_old_topic(self) -> None:
user_profile = self.example_user("iago")
self.login("iago")
stream = self.make_stream("public stream")
self.subscribe(user_profile, stream.name)
msg_id = self.send_stream_message(
user_profile, stream.name, topic_name="test", content="First"
)
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second")
self.send_stream_message(user_profile, stream.name, topic_name="test", content="third")
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": "edited",
"propagate_mode": "change_all",
"send_notification_to_old_thread": "true",
"send_notification_to_new_thread": "false",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, "test")
self.assert_length(messages, 1)
self.assertEqual(
messages[0].content,
f"This topic was moved to #**public stream>edited** by @_**Iago|{user_profile.id}**.",
)
messages = get_topic_messages(user_profile, stream, "edited")
self.assert_length(messages, 3)
def test_notify_both_topics(self) -> None:
user_profile = self.example_user("iago")
self.login("iago")
stream = self.make_stream("public stream")
self.subscribe(user_profile, stream.name)
msg_id = self.send_stream_message(
user_profile, stream.name, topic_name="test", content="First"
)
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second")
self.send_stream_message(user_profile, stream.name, topic_name="test", content="third")
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": "edited",
"propagate_mode": "change_all",
"send_notification_to_old_thread": "true",
"send_notification_to_new_thread": "true",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, "test")
self.assert_length(messages, 1)
self.assertEqual(
messages[0].content,
f"This topic was moved to #**public stream>edited** by @_**Iago|{user_profile.id}**.",
)
messages = get_topic_messages(user_profile, stream, "edited")
self.assert_length(messages, 4)
self.assertEqual(
messages[3].content,
f"This topic was moved here from #**public stream>test** by @_**Iago|{user_profile.id}**.",
)
def test_notify_no_topic(self) -> None:
user_profile = self.example_user("iago")
self.login("iago")
stream = self.make_stream("public stream")
self.subscribe(user_profile, stream.name)
msg_id = self.send_stream_message(
user_profile, stream.name, topic_name="test", content="First"
)
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second")
self.send_stream_message(user_profile, stream.name, topic_name="test", content="third")
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": "edited",
"propagate_mode": "change_all",
"send_notification_to_old_thread": "false",
"send_notification_to_new_thread": "false",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, "test")
self.assert_length(messages, 0)
messages = get_topic_messages(user_profile, stream, "edited")
self.assert_length(messages, 3)
def test_notify_old_topics_after_message_move(self) -> None:
user_profile = self.example_user("iago")
self.login("iago")
stream = self.make_stream("public stream")
self.subscribe(user_profile, stream.name)
msg_id = self.send_stream_message(
user_profile, stream.name, topic_name="test", content="First"
)
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second")
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Third")
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": "edited",
"propagate_mode": "change_one",
"send_notification_to_old_thread": "true",
"send_notification_to_new_thread": "false",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, "test")
self.assert_length(messages, 3)
self.assertEqual(messages[0].content, "Second")
self.assertEqual(messages[1].content, "Third")
self.assertEqual(
messages[2].content,
f"A message was moved from this topic to #**public stream>edited** by @_**Iago|{user_profile.id}**.",
)
messages = get_topic_messages(user_profile, stream, "edited")
self.assert_length(messages, 1)
self.assertEqual(messages[0].content, "First")
def test_notify_no_topic_after_message_move(self) -> None:
user_profile = self.example_user("iago")
self.login("iago")
stream = self.make_stream("public stream")
self.subscribe(user_profile, stream.name)
msg_id = self.send_stream_message(
user_profile, stream.name, topic_name="test", content="First"
)
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Second")
self.send_stream_message(user_profile, stream.name, topic_name="test", content="Third")
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": "edited",
"propagate_mode": "change_one",
"send_notification_to_old_thread": "false",
"send_notification_to_new_thread": "false",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, "test")
self.assert_length(messages, 2)
self.assertEqual(messages[0].content, "Second")
self.assertEqual(messages[1].content, "Third")
messages = get_topic_messages(user_profile, stream, "edited")
self.assert_length(messages, 1)
self.assertEqual(messages[0].content, "First")
def test_notify_resolve_topic_long_name(self) -> None:
user_profile = self.example_user("hamlet")
self.login("hamlet")
stream = self.make_stream("public stream")
self.subscribe(user_profile, stream.name)
# Marking topics with a long name as resolved causes the new topic name to be truncated.
# We want to avoid having code paths believing that the topic is "moved" instead of
# "resolved" in this edge case.
topic_name = "a" * MAX_TOPIC_NAME_LENGTH
msg_id = self.send_stream_message(
user_profile, stream.name, topic_name=topic_name, content="First"
)
resolved_topic = RESOLVED_TOPIC_PREFIX + topic_name
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": resolved_topic,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
new_topic_name = truncate_topic(resolved_topic)
messages = get_topic_messages(user_profile, stream, new_topic_name)
self.assert_length(messages, 2)
self.assertEqual(messages[0].content, "First")
self.assertEqual(
messages[1].content,
f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as resolved.",
)
# 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_name = "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_name)
resolved_topic_name = RESOLVED_TOPIC_PREFIX + topic_name
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": resolved_topic_name,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, resolved_topic_name)
self.assert_length(messages, 2)
self.assertEqual(
messages[1].content,
f"@_**{user_profile.full_name}|{user_profile.id}** has marked this topic as resolved.",
)
# Test unresolving a topic while moving it (✔ test -> bar)
new_topic_name = "bar"
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": new_topic_name,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, new_topic_name)
self.assert_length(messages, 3)
self.assertEqual(
messages[2].content,
f"This topic was moved here from #**public stream>✔ test** by @_**{user_profile.full_name}|{user_profile.id}**.",
)
# Now test moving the topic while also resolving it (bar -> ✔ baz)
new_resolved_topic_name = RESOLVED_TOPIC_PREFIX + "baz"
result = self.client_patch(
"/json/messages/" + str(msg_id),
{
"topic": new_resolved_topic_name,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
messages = get_topic_messages(user_profile, stream, new_resolved_topic_name)
self.assert_length(messages, 4)
self.assertEqual(
messages[3].content,
f"This topic was moved here from #**public stream>{new_topic_name}** by @_**{user_profile.full_name}|{user_profile.id}**.",
)
def test_mark_topic_as_resolved(self) -> None:
self.login("iago")
admin_user = self.example_user("iago")
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
aaron = self.example_user("aaron")
# Set the user's translation language to German to test that
# it is overridden by the realm's default language.
admin_user.default_language = "de"
admin_user.save()
stream = self.make_stream("new")
self.subscribe(admin_user, stream.name)
self.subscribe(hamlet, stream.name)
self.subscribe(cordelia, stream.name)
self.subscribe(aaron, stream.name)
original_topic_name = "topic 1"
id1 = self.send_stream_message(hamlet, "new", topic_name=original_topic_name)
id2 = self.send_stream_message(admin_user, "new", topic_name=original_topic_name)
msg1 = Message.objects.get(id=id1)
do_add_reaction(aaron, msg1, "tada", "1f389", "unicode_emoji")
# Check that we don't incorrectly send "unresolve topic"
# notifications when asking the preserve the current topic.
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": original_topic_name,
"propagate_mode": "change_all",
},
)
self.assert_json_error(result, "Nothing to change")
resolved_topic_name = RESOLVED_TOPIC_PREFIX + original_topic_name
result = self.resolve_topic_containing_message(
admin_user,
id1,
HTTP_ACCEPT_LANGUAGE="de",
)
self.assert_json_success(result)
for msg_id in [id1, id2]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(
resolved_topic_name,
msg.topic_name(),
)
messages = get_topic_messages(admin_user, stream, resolved_topic_name)
self.assert_length(messages, 3)
self.assertEqual(
messages[2].content,
f"@_**Iago|{admin_user.id}** has marked this topic as resolved.",
)
# 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()]) # noqa: S610
.count()
== 3
)
assert (
not UserMessage.objects.filter(user_profile=cordelia, message__id=messages[2].id)
.extra(where=[UserMessage.where_unread()]) # noqa: S610
.exists()
)
# Now move to a weird state and confirm we get the normal topic moved message.
weird_topic_name = "✔ ✔✔" + original_topic_name
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": weird_topic_name,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
for msg_id in [id1, id2]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(
weird_topic_name,
msg.topic_name(),
)
messages = get_topic_messages(admin_user, stream, weird_topic_name)
self.assert_length(messages, 4)
self.assertEqual(
messages[2].content,
f"@_**Iago|{admin_user.id}** has marked this topic as resolved.",
)
self.assertEqual(
messages[3].content,
f"This topic was moved here from #**new>✔ topic 1** by @_**Iago|{admin_user.id}**.",
)
unresolved_topic_name = original_topic_name
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": unresolved_topic_name,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
for msg_id in [id1, id2]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(
unresolved_topic_name,
msg.topic_name(),
)
messages = get_topic_messages(admin_user, stream, unresolved_topic_name)
self.assert_length(messages, 5)
self.assertEqual(
messages[2].content, f"@_**Iago|{admin_user.id}** has marked this topic as resolved."
)
self.assertEqual(
messages[4].content,
f"@_**Iago|{admin_user.id}** has marked this topic as unresolved.",
)
# 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()]) # noqa: S610
.count()
== 3
)
assert (
not UserMessage.objects.filter(user_profile=cordelia, message__id=messages[4].id)
.extra(where=[UserMessage.where_unread()]) # noqa: S610
.exists()
)
@override_settings(RESOLVE_TOPIC_UNDO_GRACE_PERIOD_SECONDS=60)
def test_mark_topic_as_resolved_within_grace_period(self) -> None:
self.login("iago")
admin_user = self.example_user("iago")
hamlet = self.example_user("hamlet")
stream = self.make_stream("new")
self.subscribe(admin_user, stream.name)
self.subscribe(hamlet, stream.name)
original_topic = "topic 1"
id1 = self.send_stream_message(
hamlet, "new", content="message 1", topic_name=original_topic
)
id2 = self.send_stream_message(
admin_user, "new", content="message 2", topic_name=original_topic
)
resolved_topic = RESOLVED_TOPIC_PREFIX + original_topic
start_time = timezone_now()
with time_machine.travel(start_time, tick=False):
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": resolved_topic,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
for msg_id in [id1, id2]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(
resolved_topic,
msg.topic_name(),
)
messages = get_topic_messages(admin_user, stream, resolved_topic)
self.assert_length(messages, 3)
self.assertEqual(
messages[2].content,
f"@_**Iago|{admin_user.id}** has marked this topic as resolved.",
)
unresolved_topic = original_topic
# Now unresolve the topic within the grace period.
with time_machine.travel(start_time + timedelta(seconds=30), tick=False):
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": unresolved_topic,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
for msg_id in [id1, id2]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(
unresolved_topic,
msg.topic_name(),
)
messages = get_topic_messages(admin_user, stream, unresolved_topic)
# The message about the topic having been resolved is gone.
self.assert_length(messages, 2)
self.assertEqual(
messages[1].content,
"message 2",
)
self.assertEqual(messages[0].content, "message 1")
# Now resolve the topic again after the grace period
with time_machine.travel(start_time + timedelta(seconds=61), tick=False):
result = self.client_patch(
"/json/messages/" + str(id1),
{
"topic": resolved_topic,
"propagate_mode": "change_all",
},
)
self.assert_json_success(result)
for msg_id in [id1, id2]:
msg = Message.objects.get(id=msg_id)
self.assertEqual(
resolved_topic,
msg.topic_name(),
)
messages = get_topic_messages(admin_user, stream, resolved_topic)
self.assert_length(messages, 3)
self.assertEqual(
messages[2].content,
f"@_**Iago|{admin_user.id}** has marked this topic as resolved.",
)
def test_send_resolve_topic_notification_with_no_topic_messages(self) -> None:
self.login("iago")
admin_user = self.example_user("iago")
hamlet = self.example_user("hamlet")
stream = self.make_stream("new")
self.subscribe(admin_user, stream.name)
self.subscribe(hamlet, stream.name)
original_topic = "topic 1"
message_id = self.send_stream_message(
hamlet, "new", content="message 1", topic_name=original_topic
)
message = Message.objects.get(id=message_id)
do_delete_messages(admin_user.realm, [message])
assert stream.recipient_id is not None
changed_messages = messages_for_topic(stream.realm_id, stream.recipient_id, original_topic)
resolve_topic = RESOLVED_TOPIC_PREFIX + original_topic
maybe_send_resolve_topic_notifications(
user_profile=admin_user,
stream=stream,
old_topic_name=original_topic,
new_topic_name=resolve_topic,
changed_messages=changed_messages,
pre_truncation_new_topic_name=resolve_topic,
)
topic_messages = get_topic_messages(admin_user, stream, resolve_topic)
self.assert_length(topic_messages, 1)
self.assertEqual(
topic_messages[0].content,
f"@_**Iago|{admin_user.id}** has marked this topic as resolved.",
)