Files
zulip/zerver/tests/test_scheduled_messages.py
Sahil Batra b655bd14ea messages: Use "\x07" as topic for DMs and group DMs.
This commit updates code to use "\x07" as value for
"subject" field of Message objects for DMs and group
DMs, so that we have a unique value for DMs and group
DMs which cannot be used for channel messages.

This helps in avoiding having an empty string value as
topic for DMs, which is also used for "general chat"
channel messages, as large number of DMs in the realm
resulted in PostgreSQL query planner thinking that there
are too many "general chat" messages and thus generated
bad query plans for operations like fetching
"general chat" messages in a stream or moving messages
to and from "general chat" topic.

This change as done for ArchivedMessage and
ScheduledMessage objects as well.

Note that the clients still get "subject" value as
an empty string "".

This commit also adds tests for checking that "\x07"
cannot be used as topic for channel messages.

Fixes #34360.
2025-06-19 10:44:37 -07:00

735 lines
33 KiB
Python

import re
import time
from datetime import timedelta
from io import StringIO
from typing import TYPE_CHECKING, Any
from unittest import mock
import orjson
import time_machine
from django.utils.timezone import now as timezone_now
from zerver.actions.scheduled_messages import (
SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES,
try_deliver_one_scheduled_message,
)
from zerver.actions.users import change_user_is_active
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import most_recent_message
from zerver.lib.timestamp import timestamp_to_datetime
from zerver.models import Attachment, Message, Recipient, ScheduledMessage, UserMessage
if TYPE_CHECKING:
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
class ScheduledMessageTest(ZulipTestCase):
def last_scheduled_message(self) -> ScheduledMessage:
return ScheduledMessage.objects.all().order_by("-id")[0]
def get_scheduled_message(self, id: str) -> ScheduledMessage:
return ScheduledMessage.objects.get(id=id)
def do_schedule_message(
self,
msg_type: str,
to: int | list[str] | list[int],
msg: str,
scheduled_delivery_timestamp: int,
) -> "TestHttpResponse":
self.login("hamlet")
topic_name = ""
if msg_type in ["stream", "channel"]:
topic_name = "Test topic"
payload = {
"type": msg_type,
"to": orjson.dumps(to).decode(),
"content": msg,
"topic": topic_name,
"scheduled_delivery_timestamp": scheduled_delivery_timestamp,
}
result = self.client_post("/json/scheduled_messages", payload)
return result
def test_schedule_message(self) -> None:
content = "Test message"
scheduled_delivery_timestamp = int(time.time() + 86400)
verona_stream_id = self.get_stream_id("Verona")
# Scheduling a message to a stream you are subscribed is successful.
result = self.do_schedule_message(
"channel", verona_stream_id, f"{content} 1", scheduled_delivery_timestamp
)
scheduled_message = self.last_scheduled_message()
self.assert_json_success(result)
self.assertEqual(scheduled_message.content, "Test message 1")
self.assertEqual(scheduled_message.rendered_content, "<p>Test message 1</p>")
self.assertEqual(scheduled_message.topic_name(), "Test topic")
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(scheduled_delivery_timestamp),
)
# Scheduling a direct message with user IDs is successful.
othello = self.example_user("othello")
result = self.do_schedule_message(
"direct", [othello.id], f"{content} 3", scheduled_delivery_timestamp
)
scheduled_message = self.last_scheduled_message()
self.assert_json_success(result)
self.assertEqual(scheduled_message.content, "Test message 3")
self.assertEqual(scheduled_message.rendered_content, "<p>Test message 3</p>")
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(scheduled_delivery_timestamp),
)
self.assertEqual(scheduled_message.topic_name(), Message.DM_TOPIC)
# Cannot schedule a direct message with user emails.
result = self.do_schedule_message(
"direct", [othello.email], f"{content} 4", scheduled_delivery_timestamp
)
self.assert_json_error(result, 'to["int"] is not an integer')
def create_scheduled_message(self) -> None:
content = "Test message"
scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
verona_stream_id = self.get_stream_id("Verona")
result = self.do_schedule_message(
"channel", verona_stream_id, f"{content} 1", scheduled_delivery_timestamp
)
self.assert_json_success(result)
def test_successful_deliver_stream_scheduled_message(self) -> None:
# No scheduled message
result = try_deliver_one_scheduled_message()
self.assertFalse(result)
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
# mock current time to be greater than the scheduled time, so that the `scheduled_message` can be sent.
more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
minutes=1
)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})"
],
)
scheduled_message.refresh_from_db()
assert isinstance(scheduled_message.delivered_message_id, int)
self.assertEqual(scheduled_message.delivered, True)
self.assertEqual(scheduled_message.failed, False)
delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
self.assertEqual(delivered_message.content, scheduled_message.content)
self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
self.assertEqual(delivered_message.topic_name(), scheduled_message.topic_name())
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
def test_successful_deliver_direct_scheduled_message(self) -> None:
# No scheduled message
self.assertFalse(try_deliver_one_scheduled_message())
content = "Test message"
scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
sender = self.example_user("hamlet")
othello = self.example_user("othello")
response = self.do_schedule_message(
"direct", [othello.id], f"{content} 3", scheduled_delivery_timestamp
)
self.assert_json_success(response)
scheduled_message = self.last_scheduled_message()
# mock current time to be greater than the scheduled time.
more_than_scheduled_delivery_datetime = scheduled_delivery_datetime + timedelta(minutes=1)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})"
],
)
scheduled_message.refresh_from_db()
assert isinstance(scheduled_message.delivered_message_id, int)
self.assertEqual(scheduled_message.delivered, True)
self.assertEqual(scheduled_message.failed, False)
delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
self.assertEqual(delivered_message.content, scheduled_message.content)
self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
sender_user_message = UserMessage.objects.get(
message_id=scheduled_message.delivered_message_id, user_profile_id=sender.id
)
self.assertTrue(sender_user_message.flags.read)
# Check error is sent if an edit happens after the scheduled
# message is successfully sent.
new_delivery_datetime = timezone_now() + timedelta(minutes=7)
new_delivery_timestamp = int(new_delivery_datetime.timestamp())
content = "New message content"
payload = {
"content": content,
"scheduled_delivery_timestamp": new_delivery_timestamp,
}
updated_response = self.client_patch(
f"/json/scheduled_messages/{scheduled_message.id}", payload
)
self.assert_json_error(updated_response, "Scheduled message was already sent")
def test_successful_deliver_direct_scheduled_message_to_self(self) -> None:
# No scheduled message
self.assertFalse(try_deliver_one_scheduled_message())
content = "Test message to self"
scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
sender = self.example_user("hamlet")
response = self.do_schedule_message(
"direct", [sender.id], content, scheduled_delivery_timestamp
)
self.assert_json_success(response)
scheduled_message = self.last_scheduled_message()
# mock current time to be greater than the scheduled time.
more_than_scheduled_delivery_datetime = scheduled_delivery_datetime + timedelta(minutes=1)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})"
],
)
scheduled_message.refresh_from_db()
assert isinstance(scheduled_message.delivered_message_id, int)
self.assertEqual(scheduled_message.delivered, True)
self.assertEqual(scheduled_message.failed, False)
delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
self.assertEqual(delivered_message.content, scheduled_message.content)
self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
sender_user_message = UserMessage.objects.get(
message_id=scheduled_message.delivered_message_id, user_profile_id=sender.id
)
self.assertFalse(sender_user_message.flags.read)
def verify_deliver_scheduled_message_failure(
self, scheduled_message: ScheduledMessage, expected_failure_message: str
) -> None:
with self.assertLogs(level="INFO") as logs:
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
scheduled_message.refresh_from_db()
self.assertEqual(scheduled_message.failure_message, expected_failure_message)
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})",
f"INFO:root:Failed with message: {scheduled_message.failure_message}",
],
)
self.assertTrue(scheduled_message.failed)
def test_too_late_to_deliver_scheduled_message(self) -> None:
expected_failure_message = "Message could not be sent at the scheduled time."
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
too_late_to_send_message_datetime = scheduled_message.scheduled_timestamp + timedelta(
minutes=SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES + 1
)
with time_machine.travel(too_late_to_send_message_datetime, tick=False):
self.verify_deliver_scheduled_message_failure(
scheduled_message, expected_failure_message
)
# Verify that the user was sent a message informing them about
# the failed scheduled message.
realm = scheduled_message.realm
msg = most_recent_message(scheduled_message.sender)
self.assertEqual(msg.recipient.type, msg.recipient.PERSONAL)
self.assertEqual(msg.sender_id, self.notification_bot(realm).id)
self.assertIn(expected_failure_message, msg.content)
def test_realm_deactivated_failed_to_deliver_scheduled_message(self) -> None:
expected_failure_message = "This organization has been deactivated"
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
# Verify realm isn't deactivated and get user's most recent
# message.
self.assertFalse(scheduled_message.realm.deactivated)
message_before_deactivation = most_recent_message(scheduled_message.sender)
more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
minutes=1
)
with time_machine.travel(more_than_scheduled_delivery_datetime, tick=False):
scheduled_message = self.last_scheduled_message()
scheduled_message.realm.deactivated = True
scheduled_message.realm.save()
self.verify_deliver_scheduled_message_failure(
scheduled_message, expected_failure_message
)
# Verify that no failed scheduled message notification was sent.
self.assertTrue(scheduled_message.realm.deactivated)
message_after_deactivation = most_recent_message(scheduled_message.sender)
self.assertEqual(message_after_deactivation.content, message_before_deactivation.content)
self.assertNotIn(expected_failure_message, message_after_deactivation.content)
def test_sender_deactivated_failed_to_deliver_scheduled_message(self) -> None:
expected_failure_message = "Account is deactivated"
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
# Verify user isn't deactivated and get user's most recent
# message.
self.assertTrue(scheduled_message.sender.is_active)
message_before_deactivation = most_recent_message(scheduled_message.sender)
more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
minutes=1
)
with time_machine.travel(more_than_scheduled_delivery_datetime, tick=False):
scheduled_message = self.last_scheduled_message()
change_user_is_active(scheduled_message.sender, False)
self.verify_deliver_scheduled_message_failure(
scheduled_message, expected_failure_message
)
# Verify that no failed scheduled message notification was sent.
self.assertFalse(scheduled_message.sender.is_active)
message_after_deactivation = most_recent_message(scheduled_message.sender)
self.assertEqual(message_after_deactivation.content, message_before_deactivation.content)
self.assertNotIn(expected_failure_message, message_after_deactivation.content)
def test_failed_to_deliver_scheduled_message_unknown_exception(
self,
) -> None:
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
minutes=1
)
with (
mock.patch(
"zerver.actions.scheduled_messages.send_scheduled_message",
side_effect=Exception(),
),
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
):
scheduled_message = self.last_scheduled_message()
with self.assertLogs(level="INFO") as logs:
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
scheduled_message.refresh_from_db()
self.assert_length(logs.output, 2)
self.assertEqual(
logs.output[0],
f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})",
)
self.assertTrue(
logs.output[1].startswith(
f"ERROR:root:Unexpected error sending scheduled message {scheduled_message.id} (sent: {scheduled_message.delivered})\nTraceback (most recent call last)"
)
)
self.assertTrue(scheduled_message.failed)
# Verify that the user was sent a message informing them about
# the failed scheduled message.
realm = scheduled_message.realm
msg = most_recent_message(scheduled_message.sender)
self.assertEqual(msg.recipient.type, msg.recipient.PERSONAL)
self.assertEqual(msg.sender_id, self.notification_bot(realm).id)
self.assertIn("Internal server error", msg.content)
def test_editing_failed_send_scheduled_message(self) -> None:
expected_failure_message = "Message could not be sent at the scheduled time."
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
too_late_to_send_message_datetime = scheduled_message.scheduled_timestamp + timedelta(
minutes=SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES + 1
)
with time_machine.travel(too_late_to_send_message_datetime, tick=False):
self.verify_deliver_scheduled_message_failure(
scheduled_message, expected_failure_message
)
# After verifying the scheduled message failed to be sent:
# Confirm not updating the scheduled delivery timestamp for
# the scheduled message with that ID returns an error.
payload_without_timestamp = {"topic": "Failed to send"}
response = self.client_patch(
f"/json/scheduled_messages/{scheduled_message.id}", payload_without_timestamp
)
self.assert_json_error(response, "Scheduled delivery time must be in the future.")
# Editing the scheduled message with that ID for a future time is
# successful and resets the `failed` and `failure_message` fields.
new_delivery_datetime = timezone_now() + timedelta(minutes=60)
new_delivery_timestamp = int(new_delivery_datetime.timestamp())
scheduled_message_id = scheduled_message.id
payload_with_timestamp = {
"scheduled_delivery_timestamp": new_delivery_timestamp,
}
response = self.client_patch(
f"/json/scheduled_messages/{scheduled_message.id}", payload_with_timestamp
)
self.assert_json_success(response)
scheduled_message = self.last_scheduled_message()
self.assertEqual(scheduled_message.id, scheduled_message_id)
self.assertFalse(scheduled_message.failed)
self.assertIsNone(scheduled_message.failure_message)
def test_scheduling_in_past(self) -> None:
# Scheduling a message in past should fail.
content = "Test message"
verona_stream_id = self.get_stream_id("Verona")
scheduled_delivery_timestamp = int(time.time() - 86400)
result = self.do_schedule_message(
"channel", verona_stream_id, f"{content} 1", scheduled_delivery_timestamp
)
self.assert_json_error(result, "Scheduled delivery time must be in the future.")
def test_edit_schedule_message(self) -> None:
content = "Original test message"
scheduled_delivery_timestamp = int(time.time() + 86400)
verona_stream_id = self.get_stream_id("Verona")
# Scheduling a message to a stream you are subscribed is successful.
result = self.do_schedule_message(
"channel", verona_stream_id, content, scheduled_delivery_timestamp
)
scheduled_message = self.last_scheduled_message()
self.assert_json_success(result)
self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
self.assertEqual(scheduled_message.content, "Original test message")
self.assertEqual(scheduled_message.topic_name(), "Test topic")
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(scheduled_delivery_timestamp),
)
scheduled_message_id = scheduled_message.id
payload: dict[str, Any]
# Edit message with other stream message type ("stream") and no other changes
# results in no changes to the scheduled message.
payload = {
"type": "stream",
"to": orjson.dumps(verona_stream_id).decode(),
"topic": "Test topic",
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
self.assertEqual(scheduled_message.stream_id, verona_stream_id)
self.assertEqual(scheduled_message.content, "Original test message")
self.assertEqual(scheduled_message.topic_name(), "Test topic")
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(scheduled_delivery_timestamp),
)
# Sending request with only scheduled message ID returns an error
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}")
self.assert_json_error(result, "Nothing to change")
# Trying to edit only message `type` returns an error
payload = {
"type": "direct",
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_error(
result, "Recipient required when updating type of scheduled message."
)
# Edit message `type` with valid `to` parameter succeeds
othello = self.example_user("othello")
to = [othello.id]
payload = {"type": "direct", "to": orjson.dumps(to).decode()}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
self.assertNotEqual(scheduled_message.recipient.type, Recipient.STREAM)
self.assertEqual(scheduled_message.recipient.type, Recipient.PERSONAL)
scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
self.assertEqual(scheduled_message.topic_name(), Message.DM_TOPIC)
# Trying to edit `topic` for direct message is ignored
payload = {
"topic": "Direct message topic",
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
self.assertEqual(scheduled_message.topic_name(), Message.DM_TOPIC)
# Trying to edit `type` to stream message type without a `topic` returns an error
payload = {
"type": "channel",
"to": orjson.dumps(verona_stream_id).decode(),
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_error(
result, "Topic required when updating scheduled message type to channel."
)
# Edit message `type` to stream with valid `to` and `topic` succeeds
payload = {
"type": "channel",
"to": orjson.dumps(verona_stream_id).decode(),
"topic": "New test topic",
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
self.assertEqual(scheduled_message.topic_name(), "New test topic")
# Trying to edit with timestamp in the past returns an error
new_scheduled_delivery_timestamp = int(time.time() - 86400)
payload = {
"scheduled_delivery_timestamp": new_scheduled_delivery_timestamp,
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_error(result, "Scheduled delivery time must be in the future.")
# Edit content and time of scheduled message succeeds
edited_content = "Edited test message"
new_scheduled_delivery_timestamp = scheduled_delivery_timestamp + int(
time.time() + (3 * 86400)
)
payload = {
"content": edited_content,
"scheduled_delivery_timestamp": new_scheduled_delivery_timestamp,
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
self.assertEqual(scheduled_message.content, edited_content)
self.assertEqual(scheduled_message.topic_name(), "New test topic")
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(new_scheduled_delivery_timestamp),
)
# Edit topic and content of scheduled stream message
edited_content = "Final content edit for test"
payload = {
"topic": "Another topic for test",
"content": edited_content,
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message.id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
self.assertEqual(scheduled_message.content, edited_content)
self.assertEqual(scheduled_message.topic_name(), "Another topic for test")
# Edit only topic of scheduled stream message
payload = {
"topic": "Final topic for test",
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message.id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
self.assertEqual(scheduled_message.content, edited_content)
self.assertEqual(scheduled_message.topic_name(), "Final topic for test")
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(new_scheduled_delivery_timestamp),
)
def test_fetch_scheduled_messages(self) -> None:
self.login("hamlet")
# No scheduled message
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
self.assert_length(orjson.loads(result.content)["scheduled_messages"], 0)
verona_stream_id = self.get_stream_id("Verona")
content = "Test message"
scheduled_delivery_timestamp = int(time.time() + 86400)
self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)
# Single scheduled message
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
scheduled_messages = orjson.loads(result.content)["scheduled_messages"]
self.assert_length(scheduled_messages, 1)
self.assertEqual(
scheduled_messages[0]["scheduled_message_id"], self.last_scheduled_message().id
)
self.assertEqual(scheduled_messages[0]["content"], content)
self.assertEqual(scheduled_messages[0]["to"], verona_stream_id)
self.assertEqual(scheduled_messages[0]["type"], "stream")
self.assertEqual(scheduled_messages[0]["topic"], "Test topic")
self.assertEqual(
scheduled_messages[0]["scheduled_delivery_timestamp"], scheduled_delivery_timestamp
)
othello = self.example_user("othello")
result = self.do_schedule_message(
"direct", [othello.id], f"{content} 3", scheduled_delivery_timestamp
)
# Multiple scheduled messages
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
self.assert_length(orjson.loads(result.content)["scheduled_messages"], 2)
# Check if another user can access these scheduled messages.
self.logout()
self.login("othello")
result = self.client_get("/json/scheduled_messages")
self.assert_json_success(result)
self.assert_length(orjson.loads(result.content)["scheduled_messages"], 0)
def test_delete_scheduled_messages(self) -> None:
self.login("hamlet")
content = "Test message"
verona_stream_id = self.get_stream_id("Verona")
scheduled_delivery_timestamp = int(time.time() + 86400)
self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)
scheduled_message = self.last_scheduled_message()
self.logout()
# Other user cannot delete it.
othello = self.example_user("othello")
result = self.api_delete(othello, f"/api/v1/scheduled_messages/{scheduled_message.id}")
self.assert_json_error(result, "Scheduled message does not exist", 404)
self.login("hamlet")
result = self.client_delete(f"/json/scheduled_messages/{scheduled_message.id}")
self.assert_json_success(result)
# Already deleted.
result = self.client_delete(f"/json/scheduled_messages/{scheduled_message.id}")
self.assert_json_error(result, "Scheduled message does not exist", 404)
def test_attachment_handling(self) -> None:
self.login("hamlet")
hamlet = self.example_user("hamlet")
verona_stream_id = self.get_stream_id("Verona")
attachment_file1 = StringIO("zulip!")
attachment_file1.name = "dummy_1.txt"
result = self.client_post("/json/user_uploads", {"file": attachment_file1})
path_id1 = re.sub(r"/user_uploads/", "", result.json()["url"])
attachment_object1 = Attachment.objects.get(path_id=path_id1)
attachment_file2 = StringIO("zulip!")
attachment_file2.name = "dummy_1.txt"
result = self.client_post("/json/user_uploads", {"file": attachment_file2})
path_id2 = re.sub(r"/user_uploads/", "", result.json()["url"])
attachment_object2 = Attachment.objects.get(path_id=path_id2)
content = f"Test [zulip.txt](http://{hamlet.realm.host}/user_uploads/{path_id1})"
scheduled_delivery_timestamp = int(time.time() + 86400)
# Test sending with attachment
self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)
scheduled_message = self.last_scheduled_message()
self.assertEqual(
list(attachment_object1.scheduled_messages.all().values_list("id", flat=True)),
[scheduled_message.id],
)
self.assertEqual(scheduled_message.has_attachment, True)
# Test editing to change attachmment
edited_content = f"Test [zulip.txt](http://{hamlet.realm.host}/user_uploads/{path_id2})"
payload = {
"content": edited_content,
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message.id}", payload)
scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
self.assertEqual(
list(attachment_object1.scheduled_messages.all().values_list("id", flat=True)), []
)
self.assertEqual(
list(attachment_object2.scheduled_messages.all().values_list("id", flat=True)),
[scheduled_message.id],
)
self.assertEqual(scheduled_message.has_attachment, True)
# Test editing to no longer reference any attachments
edited_content = "No more attachments"
payload = {
"content": edited_content,
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message.id}", payload)
scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
self.assertEqual(
list(attachment_object1.scheduled_messages.all().values_list("id", flat=True)), []
)
self.assertEqual(
list(attachment_object2.scheduled_messages.all().values_list("id", flat=True)), []
)
self.assertEqual(scheduled_message.has_attachment, False)
# Test editing to now have an attachment again
edited_content = (
f"Attachment is back! [zulip.txt](http://{hamlet.realm.host}/user_uploads/{path_id2})"
)
payload = {
"content": edited_content,
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message.id}", payload)
scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
self.assertEqual(
list(attachment_object1.scheduled_messages.all().values_list("id", flat=True)), []
)
self.assertEqual(
list(attachment_object2.scheduled_messages.all().values_list("id", flat=True)),
[scheduled_message.id],
)
self.assertEqual(scheduled_message.has_attachment, True)