Files
zulip/zerver/tests/test_message_report.py
Sahil Batra eb57fe10a2 streams: Handle empty topic only streams being used for announcements.
This commit updates code to send messages to "general chat" topic
if streams used for announcements for "New user signups",
"New created streams", "Moderation requests" and "Zulip updates"
have topics policy set to allow only "general chat" messages.
2025-07-22 16:59:47 -07:00

398 lines
15 KiB
Python

from django.conf import settings
from typing_extensions import Any, override
from zerver.actions.realm_settings import do_set_realm_moderation_request_channel
from zerver.actions.streams import do_set_stream_property
from zerver.lib.markdown.fenced_code import get_unused_fence
from zerver.lib.mention import silent_mention_syntax_for_user
from zerver.lib.message import truncate_content
from zerver.lib.message_report import MAX_REPORT_MESSAGE_SNIPPET_LENGTH
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.topic import DB_TOPIC_NAME
from zerver.lib.topic_link_util import (
get_message_link_syntax,
will_produce_broken_stream_topic_link,
)
from zerver.models import UserProfile
from zerver.models.messages import Message
from zerver.models.recipients import get_or_create_direct_message_group
from zerver.models.streams import StreamTopicsPolicyEnum
from zerver.models.users import get_system_bot
class ReportMessageTest(ZulipTestCase):
@override
def setUp(self) -> None:
super().setUp()
self.hamlet = self.example_user("hamlet")
self.realm = self.hamlet.realm
self.reported_user = self.example_user("othello")
# Set moderation request channel
self.moderation_request_channel = self.make_stream(
"reported messages", self.realm, invite_only=True
)
do_set_realm_moderation_request_channel(
self.hamlet.realm,
self.moderation_request_channel,
self.moderation_request_channel.id,
acting_user=self.hamlet,
)
# Send a message to be reported in a public channel
self.reported_message_id = self.send_stream_message(
self.reported_user,
"Denmark",
topic_name="civillized discussions",
content="I squeeze toothpaste from the middle",
)
self.reported_message = self.get_last_message()
assert self.reported_message.id == self.reported_message_id
def report_message(
self,
user_profile: UserProfile,
msg_id: int,
report_type: str,
description: str | None = None,
) -> Any:
report_info = {"report_type": report_type}
if description:
report_info["description"] = description
return self.api_post(user_profile, f"/api/v1/messages/{msg_id}/report", report_info)
def get_submitted_moderation_requests(self) -> list[dict[str, Any]]:
notification_bot = get_system_bot(settings.NOTIFICATION_BOT, self.realm.id)
return (
Message.objects.filter(
realm_id=self.realm.id,
sender_id=notification_bot.id,
recipient=self.moderation_request_channel.recipient,
)
.order_by("-id")
.values(*["id", "content", DB_TOPIC_NAME])
)
def test_disabled_moderation_request_feature(self) -> None:
# Disable moderation request feature
do_set_realm_moderation_request_channel(
self.hamlet.realm, None, -1, acting_user=self.hamlet
)
result = self.report_message(
self.hamlet,
self.reported_message_id,
report_type="harassment",
)
self.assert_json_error(result, "Message reporting is not enabled in this organization.")
def test_channel_message_report(self) -> None:
reporting_user = self.example_user("hamlet")
report_type = "harassment"
description = "this is crime against food"
reporting_user_mention = silent_mention_syntax_for_user(reporting_user)
reported_user_mention = silent_mention_syntax_for_user(self.reported_user)
channel = self.reported_message.recipient.label()
topic_name = self.reported_message.topic_name()
message_sent_to = f"{reporting_user_mention} reported #**{channel}>{topic_name}@{self.reported_message_id}** sent by {reported_user_mention}."
expected_message = """
{message_sent_to}
- Reason: **{report_type}**
- Notes:
```quote
{description}
```
{fence} spoiler **Message sent by {reported_user}**
{reported_message}
{fence}
""".format(
report_type=report_type,
description=description,
reported_user=reported_user_mention,
message_sent_to=message_sent_to,
reported_message=self.reported_message.content,
fence=get_unused_fence(self.reported_message.content),
)
result = self.report_message(
reporting_user, self.reported_message_id, report_type, description
)
self.assert_json_success(result)
reports = self.get_submitted_moderation_requests()
assert len(reports) == 1
self.assertEqual(reports[0]["content"], expected_message.strip())
expected_report_topic = f"{self.reported_user.full_name}'s moderation requests"
self.assertEqual(reports[0][DB_TOPIC_NAME], expected_report_topic)
# User can report messages in public channels they're not subscribed
# to.
self.unsubscribe(reporting_user, "Denmark")
result = self.report_message(
reporting_user, self.reported_message_id, report_type, description
)
self.assert_json_success(result)
reports = self.get_submitted_moderation_requests()
assert len(reports) == 2
self.assertEqual(reports[0]["content"], expected_message.strip())
expected_report_topic = f"{self.reported_user.full_name}'s moderation requests"
self.assertEqual(reports[0][DB_TOPIC_NAME], expected_report_topic)
# User can't report a message in channels they're not a part of.
private_channel = self.make_stream("private channel", self.realm, invite_only=True)
self.subscribe(self.reported_user, private_channel.name, True)
self.reported_message_id = self.send_stream_message(
self.reported_user,
private_channel.name,
topic_name="private discussions",
content="foo bar",
)
private_message = self.get_last_message()
result = self.report_message(reporting_user, private_message.id, report_type, description)
self.assert_json_error(result, msg="Invalid message(s)")
def test_reported_channel_message_narrow_link(self) -> None:
reporting_user = self.example_user("hamlet")
report_type = "harassment"
description = "this is crime against food"
# Check object IDs in message link syntax is accurate.
# This channel name will generate a narrow URL for its link syntax.
obscure_channel_name = "Sw*den"
self.assertTrue(will_produce_broken_stream_topic_link(obscure_channel_name))
obscure_channel = self.make_stream(obscure_channel_name, self.realm)
self.subscribe(self.reported_user, obscure_channel.name, True)
message_id = self.send_stream_message(
self.reported_user,
obscure_channel.name,
topic_name="",
content="foo baz",
)
result = self.report_message(
reporting_user,
message_id,
report_type,
description,
)
self.assert_json_success(result)
expected_message_link_syntax = get_message_link_syntax(
obscure_channel.id,
obscure_channel.name,
"",
message_id,
)
reports = self.get_submitted_moderation_requests()
assert len(reports) == 1
self.assertIn(expected_message_link_syntax, reports[0]["content"])
def test_dm_report(self) -> None:
# Send a DM to be reported
reported_dm_id = self.send_personal_message(
self.reported_user,
self.hamlet,
content="I dip fries in ice cream",
)
reported_dm = self.get_last_message()
assert reported_dm.id == reported_dm_id
reporting_user = self.example_user("hamlet")
report_type = "harassment"
description = "this is crime against food"
reporting_user_mention = silent_mention_syntax_for_user(reporting_user)
reported_user_mention = silent_mention_syntax_for_user(self.reported_user)
message_sent_to = f"{reporting_user_mention} reported a DM sent by {reported_user_mention}."
expected_message = """
{message_sent_to}
- Reason: **{report_type}**
- Notes:
```quote
{description}
```
{fence} spoiler **Message sent by {reported_user}**
{reported_message}
{fence}
""".format(
report_type=report_type,
description=description,
reported_user=reported_user_mention,
message_sent_to=message_sent_to,
reported_message=reported_dm.content,
fence=get_unused_fence(reported_dm.content),
)
result = self.report_message(reporting_user, reported_dm_id, report_type, description)
self.assert_json_success(result)
reports = self.get_submitted_moderation_requests()
assert len(reports) == 1
self.assertEqual(reports[0]["content"], expected_message.strip())
# User can't report DM they're not a part of.
ZOE = self.example_user("ZOE")
result = self.report_message(ZOE, reported_dm_id, report_type, description)
self.assert_json_error(result, msg="Invalid message(s)")
def test_personal_message_report_using_direct_message_group(self) -> None:
direct_message_group = get_or_create_direct_message_group(
id_list=[self.hamlet.id, self.reported_user.id],
)
# Send a DM to be reported
reported_dm_id = self.send_personal_message(
self.reported_user,
self.hamlet,
content="I dip fries in ice cream",
)
reported_dm = self.get_last_message()
assert reported_dm.id == reported_dm_id
assert reported_dm.recipient == direct_message_group.recipient
reporting_user = self.hamlet
report_type = "harassment"
description = "this is crime against food"
reporting_user_mention = silent_mention_syntax_for_user(reporting_user)
reported_user_mention = silent_mention_syntax_for_user(self.reported_user)
message_sent_to = f"{reporting_user_mention} reported a DM sent by {reported_user_mention}."
expected_message = """
{message_sent_to}
- Reason: **{report_type}**
- Notes:
```quote
{description}
```
{fence} spoiler **Message sent by {reported_user}**
{reported_message}
{fence}
""".format(
report_type=report_type,
description=description,
reported_user=reported_user_mention,
message_sent_to=message_sent_to,
reported_message=reported_dm.content,
fence=get_unused_fence(reported_dm.content),
)
result = self.report_message(reporting_user, reported_dm_id, report_type, description)
self.assert_json_success(result)
reports = self.get_submitted_moderation_requests()
assert len(reports) == 1
self.assertEqual(reports[0]["content"], expected_message.strip())
# User can't report DM they're not a part of.
ZOE = self.example_user("ZOE")
result = self.report_message(ZOE, reported_dm_id, report_type, description)
self.assert_json_error(result, msg="Invalid message(s)")
def test_gdm_report(self) -> None:
# Send a group DM to be reported
reported_gdm_id = self.send_group_direct_message(
self.reported_user,
[self.hamlet, self.reported_user, self.example_user("iago")],
content="I eat cereal with water",
)
reported_gdm = self.get_last_message()
assert reported_gdm.id == reported_gdm_id
reporting_user = self.example_user("hamlet")
report_type = "harassment"
description = "Call the police please"
reporting_user_mention = silent_mention_syntax_for_user(reporting_user)
reported_user_mention = silent_mention_syntax_for_user(self.reported_user)
iago_user_mention = silent_mention_syntax_for_user(self.example_user("iago"))
gdm_user_mention = (
f"{reporting_user_mention}, {iago_user_mention}, and {reported_user_mention}"
)
message_sent_to = f"{reporting_user_mention} reported a DM sent by {reported_user_mention} to {gdm_user_mention}."
expected_message = """
{message_sent_to}
- Reason: **{report_type}**
- Notes:
```quote
{description}
```
{fence} spoiler **Message sent by {reported_user}**
{reported_message}
{fence}
""".format(
report_type=report_type,
description=description,
reported_user=reported_user_mention,
message_sent_to=message_sent_to,
reported_message=reported_gdm.content,
fence=get_unused_fence(reported_gdm.content),
)
result = self.report_message(reporting_user, reported_gdm_id, report_type, description)
self.assert_json_success(result)
reports = self.get_submitted_moderation_requests()
assert len(reports) == 1
self.assertEqual(reports[0]["content"], expected_message.strip())
# User can't report group direct messages they're not a part of.
ZOE = self.example_user("ZOE")
result = self.report_message(ZOE, reported_gdm_id, report_type, description)
self.assert_json_error(result, msg="Invalid message(s)")
def test_truncate_reported_message(self) -> None:
large_message = "." * (MAX_REPORT_MESSAGE_SNIPPET_LENGTH + 1)
reported_truncate_message_id = self.send_stream_message(
self.reported_user,
"Denmark",
topic_name="civillized discussions",
content=large_message,
)
reported_truncate_message = self.get_last_message()
assert reported_truncate_message.id == reported_truncate_message_id
reporting_user = self.example_user("hamlet")
result = self.report_message(reporting_user, reported_truncate_message_id, "spam")
self.assert_json_success(result)
reports = self.get_submitted_moderation_requests()
assert len(reports) == 1
self.assertNotIn(large_message, reports[0]["content"])
expected_truncated_message = truncate_content(
large_message, MAX_REPORT_MESSAGE_SNIPPET_LENGTH, "\n[message truncated]"
)
self.assertIn(expected_truncated_message, reports[0]["content"])
def test_other_report_type_with_no_description(self) -> None:
result = self.report_message(self.hamlet, self.reported_message_id, report_type="other")
self.assert_json_error(result, "An explanation is required.")
result = self.report_message(
self.hamlet,
self.reported_message_id,
report_type="other",
description="This is crime against food.",
)
self.assert_json_success(result)
def test_message_report_to_channel_with_topics_disabled(self) -> None:
notification_bot = get_system_bot(settings.NOTIFICATION_BOT, self.realm.id)
do_set_stream_property(
self.moderation_request_channel,
"topics_policy",
StreamTopicsPolicyEnum.empty_topic_only.value,
self.hamlet,
)
result = self.report_message(
self.hamlet,
self.reported_message_id,
report_type="harassment",
)
self.assert_json_success(result)
report_msg = self.get_last_message()
self.assertEqual(report_msg.sender_id, notification_bot.id)
self.assertEqual(report_msg.topic_name(), "")
self.assertIn("reported", report_msg.content)