zulip/zerver/tests/test_reactions.py

1126 lines
42 KiB
Python

from typing import Any, Dict, List, Mapping
from unittest import mock
import orjson
from django.http import HttpResponse
from zerver.lib.actions import do_change_stream_permission, notify_reaction_update
from zerver.lib.cache import cache_get, to_dict_cache_key_id
from zerver.lib.emoji import emoji_name_to_emoji_code
from zerver.lib.exceptions import JsonableError
from zerver.lib.message import extract_message_dict
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import zulip_reaction_info
from zerver.models import Message, Reaction, RealmEmoji, UserMessage, get_realm
class ReactionEmojiTest(ZulipTestCase):
def test_missing_emoji(self) -> None:
"""
Sending reaction without emoji fails
"""
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "",
}
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assertEqual(result.status_code, 400)
def test_add_invalid_emoji(self) -> None:
"""
Sending invalid emoji fails
"""
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "foo",
}
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_error(result, "Emoji 'foo' does not exist")
def test_add_deactivated_realm_emoji(self) -> None:
"""
Sending deactivated realm emoji fails.
"""
emoji = RealmEmoji.objects.get(name="green_tick")
emoji.deactivated = True
emoji.save(update_fields=["deactivated"])
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "green_tick",
"reaction_type": "realm_emoji",
}
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_error(result, "Emoji 'green_tick' does not exist")
def test_valid_emoji(self) -> None:
"""
Reacting with valid emoji succeeds
"""
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "smile",
}
base_query = Reaction.objects.filter(
user_profile=sender,
message=Message.objects.get(id=1),
)
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
self.assertTrue(base_query.filter(emoji_name=reaction_info["emoji_name"]).exists())
reaction_info["emoji_name"] = "green_tick"
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
self.assertTrue(base_query.filter(emoji_name=reaction_info["emoji_name"]).exists())
def test_cached_reaction_data(self) -> None:
"""
Formatted reactions data is saved in cache.
"""
senders = [self.example_user("hamlet"), self.example_user("cordelia")]
emojis = ["smile", "tada"]
expected_emoji_codes = ["1f642", "1f389"]
for sender, emoji in zip(senders, emojis):
reaction_info = {
"emoji_name": emoji,
}
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
key = to_dict_cache_key_id(1)
message = extract_message_dict(cache_get(key)[0])
expected_reaction_data = [
{
"emoji_name": emoji,
"emoji_code": emoji_code,
"reaction_type": "unicode_emoji",
"user": {
"email": f"user{sender.id}@zulip.testserver",
"id": sender.id,
"full_name": sender.full_name,
},
"user_id": sender.id,
}
# It's important that we preserve the loop order in this
# test, since this is our test to verify that we're
# returning reactions in chronological order.
for sender, emoji, emoji_code in zip(senders, emojis, expected_emoji_codes)
]
self.assertEqual(expected_reaction_data, message["reactions"])
def test_zulip_emoji(self) -> None:
"""
Reacting with zulip emoji succeeds
"""
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "zulip",
"reaction_type": "zulip_extra_emoji",
}
base_query = Reaction.objects.filter(
user_profile=sender, emoji_name=reaction_info["emoji_name"]
)
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
self.assertTrue(base_query.filter(message=Message.objects.get(id=1)).exists())
reaction_info.pop("reaction_type")
result = self.api_post(sender, "/api/v1/messages/2/reactions", reaction_info)
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
self.assertTrue(base_query.filter(message=Message.objects.get(id=2)).exists())
def test_valid_emoji_react_historical(self) -> None:
"""
Reacting with valid emoji on a historical message succeeds
"""
stream_name = "Saxony"
self.subscribe(self.example_user("cordelia"), stream_name)
message_id = self.send_stream_message(self.example_user("cordelia"), stream_name)
user_profile = self.example_user("hamlet")
sender = user_profile
# Verify that hamlet did not receive the message.
self.assertFalse(
UserMessage.objects.filter(user_profile=user_profile, message_id=message_id).exists()
)
# Have hamlet react to the message
reaction_info = {
"emoji_name": "smile",
}
result = self.api_post(sender, f"/api/v1/messages/{message_id}/reactions", reaction_info)
self.assert_json_success(result)
# Fetch the now-created UserMessage object to confirm it exists and is historical
user_message = UserMessage.objects.get(user_profile=user_profile, message_id=message_id)
self.assertTrue(user_message.flags.historical)
self.assertTrue(user_message.flags.read)
self.assertFalse(user_message.flags.starred)
def test_valid_realm_emoji(self) -> None:
"""
Reacting with valid realm emoji succeeds
"""
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "green_tick",
"reaction_type": "realm_emoji",
}
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
def test_emoji_name_to_emoji_code(self) -> None:
"""
An emoji name is mapped canonically to emoji code.
"""
realm = get_realm("zulip")
realm_emoji = RealmEmoji.objects.get(name="green_tick")
# Test active realm emoji.
emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "green_tick")
self.assertEqual(emoji_code, str(realm_emoji.id))
self.assertEqual(reaction_type, "realm_emoji")
# Test deactivated realm emoji.
realm_emoji.deactivated = True
realm_emoji.save(update_fields=["deactivated"])
with self.assertRaises(JsonableError) as exc:
emoji_name_to_emoji_code(realm, "green_tick")
self.assertEqual(str(exc.exception), "Emoji 'green_tick' does not exist")
# Test ':zulip:' emoji.
emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "zulip")
self.assertEqual(emoji_code, "zulip")
self.assertEqual(reaction_type, "zulip_extra_emoji")
# Test Unicode emoji.
emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "astonished")
self.assertEqual(emoji_code, "1f632")
self.assertEqual(reaction_type, "unicode_emoji")
# Test override Unicode emoji.
overriding_emoji = RealmEmoji.objects.create(
name="astonished", realm=realm, file_name="astonished"
)
emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "astonished")
self.assertEqual(emoji_code, str(overriding_emoji.id))
self.assertEqual(reaction_type, "realm_emoji")
# Test deactivate over-ridding realm emoji.
overriding_emoji.deactivated = True
overriding_emoji.save(update_fields=["deactivated"])
emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "astonished")
self.assertEqual(emoji_code, "1f632")
self.assertEqual(reaction_type, "unicode_emoji")
# Test override `:zulip:` emoji.
overriding_emoji = RealmEmoji.objects.create(name="zulip", realm=realm, file_name="zulip")
emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "zulip")
self.assertEqual(emoji_code, str(overriding_emoji.id))
self.assertEqual(reaction_type, "realm_emoji")
# Test non-existent emoji.
with self.assertRaises(JsonableError) as exc:
emoji_name_to_emoji_code(realm, "invalid_emoji")
self.assertEqual(str(exc.exception), "Emoji 'invalid_emoji' does not exist")
class ReactionMessageIDTest(ZulipTestCase):
def test_missing_message_id(self) -> None:
"""
Reacting without a message_id fails
"""
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "smile",
}
result = self.api_post(sender, "/api/v1/messages//reactions", reaction_info)
self.assertEqual(result.status_code, 404)
def test_invalid_message_id(self) -> None:
"""
Reacting to an invalid message id fails
"""
sender = self.example_user("hamlet")
reaction_info = {
"emoji_name": "smile",
}
result = self.api_post(sender, "/api/v1/messages/-1/reactions", reaction_info)
self.assertEqual(result.status_code, 404)
def test_inaccessible_message_id(self) -> None:
"""
Reacting to a inaccessible (for instance, private) message fails
"""
pm_sender = self.example_user("hamlet")
pm_recipient = self.example_user("othello")
reaction_sender = self.example_user("iago")
result = self.api_post(
pm_sender,
"/api/v1/messages",
{"type": "private", "content": "Test message", "to": pm_recipient.email},
)
self.assert_json_success(result)
pm_id = result.json()["id"]
reaction_info = {
"emoji_name": "smile",
}
result = self.api_post(
reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info
)
self.assert_json_error(result, "Invalid message(s)")
class ReactionTest(ZulipTestCase):
def test_add_existing_reaction(self) -> None:
"""
Creating the same reaction twice fails
"""
pm_sender = self.example_user("hamlet")
pm_recipient = self.example_user("othello")
reaction_sender = pm_recipient
pm = self.api_post(
pm_sender,
"/api/v1/messages",
{"type": "private", "content": "Test message", "to": pm_recipient.email},
)
self.assert_json_success(pm)
content = orjson.loads(pm.content)
pm_id = content["id"]
reaction_info = {
"emoji_name": "smile",
}
first = self.api_post(reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info)
self.assert_json_success(first)
second = self.api_post(
reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info
)
self.assert_json_error(second, "Reaction already exists.")
def test_remove_nonexisting_reaction(self) -> None:
"""
Removing a reaction twice fails
"""
pm_sender = self.example_user("hamlet")
pm_recipient = self.example_user("othello")
reaction_sender = pm_recipient
pm = self.api_post(
pm_sender,
"/api/v1/messages",
{"type": "private", "content": "Test message", "to": pm_recipient.email},
)
self.assert_json_success(pm)
content = orjson.loads(pm.content)
pm_id = content["id"]
reaction_info = {
"emoji_name": "smile",
}
add = self.api_post(reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info)
self.assert_json_success(add)
first = self.api_delete(
reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info
)
self.assert_json_success(first)
second = self.api_delete(
reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info
)
self.assert_json_error(second, "Reaction doesn't exist.")
def test_remove_existing_reaction_with_renamed_emoji(self) -> None:
"""
Removes an old existing reaction but the name of emoji got changed during
various emoji infra changes.
"""
realm = get_realm("zulip")
sender = self.example_user("hamlet")
emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "smile")
reaction_info = {
"emoji_name": "smile",
"emoji_code": emoji_code,
"reaction_type": reaction_type,
}
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
with mock.patch("zerver.lib.emoji.name_to_codepoint", name_to_codepoint={}):
result = self.api_delete(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
def test_remove_existing_reaction_with_deactivated_realm_emoji(self) -> None:
"""
Removes an old existing reaction but the realm emoji used there has been deactivated.
"""
sender = self.example_user("hamlet")
emoji = RealmEmoji.objects.get(name="green_tick")
reaction_info = {
"emoji_name": "green_tick",
"emoji_code": str(emoji.id),
"reaction_type": "realm_emoji",
}
result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
# Deactivate realm emoji.
emoji.deactivated = True
emoji.save(update_fields=["deactivated"])
result = self.api_delete(sender, "/api/v1/messages/1/reactions", reaction_info)
self.assert_json_success(result)
class ReactionEventTest(ZulipTestCase):
def test_add_event(self) -> None:
"""
Recipients of the message receive the reaction event
and event contains relevant data
"""
pm_sender = self.example_user("hamlet")
pm_recipient = self.example_user("othello")
reaction_sender = pm_recipient
result = self.api_post(
pm_sender,
"/api/v1/messages",
{"type": "private", "content": "Test message", "to": pm_recipient.email},
)
self.assert_json_success(result)
pm_id = result.json()["id"]
expected_recipient_ids = {pm_sender.id, pm_recipient.id}
reaction_info = {
"emoji_name": "smile",
}
events: List[Mapping[str, Any]] = []
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_post(
reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
event_user_ids = set(events[0]["users"])
self.assertEqual(expected_recipient_ids, event_user_ids)
self.assertEqual(event["user"]["email"], reaction_sender.email)
self.assertEqual(event["type"], "reaction")
self.assertEqual(event["op"], "add")
self.assertEqual(event["emoji_name"], "smile")
self.assertEqual(event["message_id"], pm_id)
def test_remove_event(self) -> None:
"""
Recipients of the message receive the reaction event
and event contains relevant data
"""
pm_sender = self.example_user("hamlet")
pm_recipient = self.example_user("othello")
reaction_sender = pm_recipient
result = self.api_post(
pm_sender,
"/api/v1/messages",
{"type": "private", "content": "Test message", "to": pm_recipient.email},
)
self.assert_json_success(result)
content = result.json()
pm_id = content["id"]
expected_recipient_ids = {pm_sender.id, pm_recipient.id}
reaction_info = {
"emoji_name": "smile",
}
add = self.api_post(reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info)
self.assert_json_success(add)
events: List[Mapping[str, Any]] = []
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_delete(
reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
event_user_ids = set(events[0]["users"])
self.assertEqual(expected_recipient_ids, event_user_ids)
self.assertEqual(event["user"]["email"], reaction_sender.email)
self.assertEqual(event["type"], "reaction")
self.assertEqual(event["op"], "remove")
self.assertEqual(event["emoji_name"], "smile")
self.assertEqual(event["message_id"], pm_id)
def test_reaction_event_scope(self) -> None:
iago = self.example_user("iago")
hamlet = self.example_user("hamlet")
polonius = self.example_user("polonius")
reaction_info = {
"emoji_name": "smile",
}
# Test `invite_only` streams with `!history_public_to_subscribers` and `!is_web_public`
stream = self.make_stream(
"test_reactions_stream", invite_only=True, history_public_to_subscribers=False
)
self.subscribe(iago, stream.name)
message_before_id = self.send_stream_message(
iago, "test_reactions_stream", "before subscription history private"
)
self.subscribe(hamlet, stream.name)
self.subscribe(polonius, stream.name)
# Hamlet and Polonius joined after the message was sent, and
# so only Iago should receive the event.
events: List[Mapping[str, Any]] = []
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_post(
iago, f"/api/v1/messages/{message_before_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
self.assertEqual(event["type"], "reaction")
event_user_ids = set(events[0]["users"])
self.assertEqual(event_user_ids, {iago.id})
remove = self.api_delete(
iago, f"/api/v1/messages/{message_before_id}/reactions", reaction_info
)
self.assert_json_success(remove)
# Reaction to a Message sent after subscription, should
# trigger events for all subscribers (Iago, Hamlet and Polonius).
message_after_id = self.send_stream_message(
iago, "test_reactions_stream", "after subscription history private"
)
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_post(
iago, f"/api/v1/messages/{message_after_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
self.assertEqual(event["type"], "reaction")
event_user_ids = set(events[0]["users"])
self.assertEqual(event_user_ids, {iago.id, hamlet.id, polonius.id})
remove = self.api_delete(
iago, f"/api/v1/messages/{message_after_id}/reactions", reaction_info
)
self.assert_json_success(remove)
# Make stream history public to subscribers
do_change_stream_permission(
stream, invite_only=False, history_public_to_subscribers=True, acting_user=iago
)
# Since stream history is public to subscribers, reacting to
# message_before_id should notify all subscribers:
# Iago and Hamlet.
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_post(
iago, f"/api/v1/messages/{message_before_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
self.assertEqual(event["type"], "reaction")
event_user_ids = set(events[0]["users"])
self.assertEqual(event_user_ids, {iago.id, hamlet.id, polonius.id})
remove = self.api_delete(
iago, f"/api/v1/messages/{message_before_id}/reactions", reaction_info
)
self.assert_json_success(remove)
# Make stream web_public as well.
do_change_stream_permission(stream, is_web_public=True, acting_user=iago)
# For is_web_public streams, events even on old messages
# should go to all subscribers, including guests like polonius.
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_post(
iago, f"/api/v1/messages/{message_before_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
self.assertEqual(event["type"], "reaction")
event_user_ids = set(events[0]["users"])
self.assertEqual(event_user_ids, {iago.id, hamlet.id, polonius.id})
remove = self.api_delete(
iago, f"/api/v1/messages/{message_before_id}/reactions", reaction_info
)
self.assert_json_success(remove)
# Private message, event should go to both participants.
private_message_id = self.send_personal_message(
iago,
hamlet,
"hello to single receiver",
)
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_post(
hamlet, f"/api/v1/messages/{private_message_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
self.assertEqual(event["type"], "reaction")
event_user_ids = set(events[0]["users"])
self.assertEqual(event_user_ids, {iago.id, hamlet.id})
# Group private message; event should go to all participants.
huddle_message_id = self.send_huddle_message(
hamlet,
[polonius, iago],
"hello message to multiple receiver",
)
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_post(
polonius, f"/api/v1/messages/{huddle_message_id}/reactions", reaction_info
)
self.assert_json_success(result)
event = events[0]["event"]
self.assertEqual(event["type"], "reaction")
event_user_ids = set(events[0]["users"])
self.assertEqual(event_user_ids, {iago.id, hamlet.id, polonius.id})
class EmojiReactionBase(ZulipTestCase):
"""Reusable testing functions for emoji reactions tests. Be careful when
changing this: It's used in test_retention.py as well."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
def post_reaction(self, reaction_info: Dict[str, str]) -> HttpResponse:
message_id = 1
result = self.api_post(
self.example_user("hamlet"), f"/api/v1/messages/{message_id}/reactions", reaction_info
)
return result
def post_other_reaction(self, reaction_info: Dict[str, str]) -> HttpResponse:
message_id = 1
result = self.api_post(
self.example_user("AARON"), f"/api/v1/messages/{message_id}/reactions", reaction_info
)
return result
def delete_reaction(self, reaction_info: Dict[str, str]) -> HttpResponse:
message_id = 1
result = self.api_delete(
self.example_user("hamlet"), f"/api/v1/messages/{message_id}/reactions", reaction_info
)
return result
def get_message_reactions(
self, message_id: int, emoji_code: str, reaction_type: str
) -> List[Reaction]:
message = Message.objects.get(id=message_id)
reactions = Reaction.objects.filter(
message=message, emoji_code=emoji_code, reaction_type=reaction_type
)
return list(reactions)
class DefaultEmojiReactionTests(EmojiReactionBase):
def setUp(self) -> None:
super().setUp()
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "hamburger",
"emoji_code": "1f354",
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
def test_add_default_emoji_reaction(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "thumbs_up",
"emoji_code": "1f44d",
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
def test_add_default_emoji_invalid_code(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "hamburger",
"emoji_code": "TBD",
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, "Invalid emoji code.")
def test_add_default_emoji_invalid_name(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "non-existent",
"emoji_code": "1f44d",
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, "Invalid emoji name.")
def test_add_to_existing_renamed_default_emoji_reaction(self) -> None:
hamlet = self.example_user("hamlet")
message = Message.objects.get(id=1)
reaction = Reaction.objects.create(
user_profile=hamlet,
message=message,
emoji_name="old_name",
emoji_code="1f603",
reaction_type="unicode_emoji",
)
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "smiley",
"emoji_code": "1f603",
}
result = self.post_other_reaction(reaction_info)
self.assert_json_success(result)
reactions = self.get_message_reactions(1, "1f603", "unicode_emoji")
for reaction in reactions:
self.assertEqual(reaction.emoji_name, "old_name")
def test_add_duplicate_reaction(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "non-existent",
"emoji_code": "1f354",
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, "Reaction already exists.")
def test_add_reaction_by_name(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "+1",
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
hamlet = self.example_user("hamlet")
message = Message.objects.get(id=1)
self.assertTrue(
Reaction.objects.filter(
user_profile=hamlet,
message=message,
emoji_name=reaction_info["emoji_name"],
emoji_code="1f44d",
reaction_type="unicode_emoji",
).exists(),
)
def test_preserve_non_canonical_name(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "+1",
"emoji_code": "1f44d",
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
reactions = self.get_message_reactions(1, "1f44d", "unicode_emoji")
for reaction in reactions:
self.assertEqual(reaction.emoji_name, "+1")
def test_reaction_name_collapse(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "+1",
"emoji_code": "1f44d",
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
reaction_info["emoji_name"] = "thumbs_up"
result = self.post_other_reaction(reaction_info)
self.assert_json_success(result)
reactions = self.get_message_reactions(1, "1f44d", "unicode_emoji")
for reaction in reactions:
self.assertEqual(reaction.emoji_name, "+1")
def test_delete_default_emoji_reaction(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "hamburger",
"emoji_code": "1f354",
}
result = self.delete_reaction(reaction_info)
self.assert_json_success(result)
def test_delete_insufficient_arguments_reaction(self) -> None:
result = self.delete_reaction({})
self.assert_json_error(
result,
"At least one of the following arguments must be present: emoji_name, emoji_code",
)
def test_delete_non_existing_emoji_reaction(self) -> None:
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "thumbs_up",
"emoji_code": "1f44d",
}
result = self.delete_reaction(reaction_info)
self.assert_json_error(result, "Reaction doesn't exist.")
def test_delete_renamed_default_emoji(self) -> None:
hamlet = self.example_user("hamlet")
message = Message.objects.get(id=1)
Reaction.objects.create(
user_profile=hamlet,
message=message,
emoji_name="old_name",
emoji_code="1f44f",
reaction_type="unicode_emoji",
)
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "new_name",
"emoji_code": "1f44f",
}
result = self.delete_reaction(reaction_info)
self.assert_json_success(result)
def test_delete_reaction_by_name(self) -> None:
hamlet = self.example_user("hamlet")
message = Message.objects.get(id=1)
Reaction.objects.create(
user_profile=hamlet,
message=message,
emoji_name="+1",
emoji_code="1f44d",
reaction_type="unicode_emoji",
)
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "+1",
}
result = self.delete_reaction(reaction_info)
self.assert_json_success(result)
self.assertFalse(
Reaction.objects.filter(
user_profile=hamlet,
message=message,
emoji_name=reaction_info["emoji_name"],
emoji_code="1f44d",
reaction_type="unicode_emoji",
).exists(),
)
def test_react_historical(self) -> None:
"""
Reacting with valid emoji on a historical message succeeds.
"""
stream_name = "Saxony"
self.subscribe(self.example_user("cordelia"), stream_name)
message_id = self.send_stream_message(self.example_user("cordelia"), stream_name)
user_profile = self.example_user("hamlet")
# Verify that hamlet did not receive the message.
self.assertFalse(
UserMessage.objects.filter(user_profile=user_profile, message_id=message_id).exists()
)
# Have hamlet react to the message
reaction_info = {
"reaction_type": "unicode_emoji",
"emoji_name": "hamburger",
"emoji_code": "1f354",
}
result = self.api_post(
user_profile, f"/api/v1/messages/{message_id}/reactions", reaction_info
)
self.assert_json_success(result)
# Fetch the now-created UserMessage object to confirm it exists and is historical
user_message = UserMessage.objects.get(user_profile=user_profile, message_id=message_id)
self.assertTrue(user_message.flags.historical)
self.assertTrue(user_message.flags.read)
self.assertFalse(user_message.flags.starred)
class ZulipExtraEmojiReactionTest(EmojiReactionBase):
def test_add_zulip_emoji_reaction(self) -> None:
result = self.post_reaction(zulip_reaction_info())
self.assert_json_success(result)
def test_add_duplicate_zulip_reaction(self) -> None:
result = self.post_reaction(zulip_reaction_info())
self.assert_json_success(result)
result = self.post_reaction(zulip_reaction_info())
self.assert_json_error(result, "Reaction already exists.")
def test_add_invalid_extra_emoji(self) -> None:
reaction_info = {
"emoji_name": "extra_emoji",
"emoji_code": "extra_emoji",
"reaction_type": "zulip_extra_emoji",
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, "Invalid emoji code.")
def test_add_invalid_emoji_name(self) -> None:
reaction_info = {
"emoji_name": "zulip_invalid",
"emoji_code": "zulip",
"reaction_type": "zulip_extra_emoji",
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, "Invalid emoji name.")
def test_delete_zulip_emoji(self) -> None:
result = self.post_reaction(zulip_reaction_info())
self.assert_json_success(result)
result = self.delete_reaction(zulip_reaction_info())
self.assert_json_success(result)
def test_delete_non_existent_zulip_reaction(self) -> None:
result = self.delete_reaction(zulip_reaction_info())
self.assert_json_error(result, "Reaction doesn't exist.")
class RealmEmojiReactionTests(EmojiReactionBase):
def setUp(self) -> None:
super().setUp()
green_tick_emoji = RealmEmoji.objects.get(name="green_tick")
self.default_reaction_info = {
"reaction_type": "realm_emoji",
"emoji_name": "green_tick",
"emoji_code": str(green_tick_emoji.id),
}
def test_add_realm_emoji(self) -> None:
result = self.post_reaction(self.default_reaction_info)
self.assert_json_success(result)
def test_add_realm_emoji_invalid_code(self) -> None:
reaction_info = {
"reaction_type": "realm_emoji",
"emoji_name": "green_tick",
"emoji_code": "9999",
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, "Invalid custom emoji.")
def test_add_realm_emoji_invalid_name(self) -> None:
green_tick_emoji = RealmEmoji.objects.get(name="green_tick")
reaction_info = {
"reaction_type": "realm_emoji",
"emoji_name": "bogus_name",
"emoji_code": str(green_tick_emoji.id),
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, "Invalid custom emoji name.")
def test_add_deactivated_realm_emoji(self) -> None:
emoji = RealmEmoji.objects.get(name="green_tick")
emoji.deactivated = True
emoji.save(update_fields=["deactivated"])
result = self.post_reaction(self.default_reaction_info)
self.assert_json_error(result, "This custom emoji has been deactivated.")
def test_add_to_existing_deactivated_realm_emoji_reaction(self) -> None:
result = self.post_reaction(self.default_reaction_info)
self.assert_json_success(result)
emoji = RealmEmoji.objects.get(name="green_tick")
emoji.deactivated = True
emoji.save(update_fields=["deactivated"])
result = self.post_other_reaction(self.default_reaction_info)
self.assert_json_success(result)
reactions = self.get_message_reactions(
1, self.default_reaction_info["emoji_code"], "realm_emoji"
)
self.assert_length(reactions, 2)
def test_remove_realm_emoji_reaction(self) -> None:
result = self.post_reaction(self.default_reaction_info)
self.assert_json_success(result)
result = self.delete_reaction(self.default_reaction_info)
self.assert_json_success(result)
def test_remove_deactivated_realm_emoji_reaction(self) -> None:
result = self.post_reaction(self.default_reaction_info)
self.assert_json_success(result)
emoji = RealmEmoji.objects.get(name="green_tick")
emoji.deactivated = True
emoji.save(update_fields=["deactivated"])
result = self.delete_reaction(self.default_reaction_info)
self.assert_json_success(result)
def test_remove_non_existent_realm_emoji_reaction(self) -> None:
reaction_info = {
"reaction_type": "realm_emoji",
"emoji_name": "non_existent",
"emoji_code": "TBD",
}
result = self.delete_reaction(reaction_info)
self.assert_json_error(result, "Reaction doesn't exist.")
def test_invalid_reaction_type(self) -> None:
reaction_info = {
"emoji_name": "zulip",
"emoji_code": "zulip",
"reaction_type": "nonexistent_emoji_type",
}
sender = self.example_user("hamlet")
message_id = 1
result = self.api_post(sender, f"/api/v1/messages/{message_id}/reactions", reaction_info)
self.assert_json_error(result, "Invalid emoji type.")
class ReactionAPIEventTest(EmojiReactionBase):
def test_add_event(self) -> None:
"""
Recipients of the message receive the reaction event
and event contains relevant data
"""
pm_sender = self.example_user("hamlet")
pm_recipient = self.example_user("othello")
reaction_sender = pm_recipient
pm_id = self.send_personal_message(pm_sender, pm_recipient)
expected_recipient_ids = {pm_sender.id, pm_recipient.id}
reaction_info = {
"emoji_name": "hamburger",
"emoji_code": "1f354",
"reaction_type": "unicode_emoji",
}
events: List[Mapping[str, Any]] = []
with self.tornado_redirected_to_list(events, expected_num_events=1):
with mock.patch("zerver.lib.actions.send_event") as m:
m.side_effect = AssertionError(
"Events should be sent only after the transaction commits!"
)
self.api_post(reaction_sender, f"/api/v1/messages/{pm_id}/reactions", reaction_info)
event = events[0]["event"]
event_user_ids = set(events[0]["users"])
self.assertEqual(expected_recipient_ids, event_user_ids)
self.assertEqual(event["user"]["user_id"], reaction_sender.id)
self.assertEqual(event["user"]["email"], reaction_sender.email)
self.assertEqual(event["user"]["full_name"], reaction_sender.full_name)
self.assertEqual(event["type"], "reaction")
self.assertEqual(event["op"], "add")
self.assertEqual(event["message_id"], pm_id)
self.assertEqual(event["emoji_name"], reaction_info["emoji_name"])
self.assertEqual(event["emoji_code"], reaction_info["emoji_code"])
self.assertEqual(event["reaction_type"], reaction_info["reaction_type"])
def test_remove_event(self) -> None:
"""
Recipients of the message receive the reaction event
and event contains relevant data
"""
pm_sender = self.example_user("hamlet")
pm_recipient = self.example_user("othello")
reaction_sender = pm_recipient
pm_id = self.send_personal_message(pm_sender, pm_recipient)
expected_recipient_ids = {pm_sender.id, pm_recipient.id}
reaction_info = {
"emoji_name": "hamburger",
"emoji_code": "1f354",
"reaction_type": "unicode_emoji",
}
add = self.api_post(
reaction_sender,
f"/api/v1/messages/{pm_id}/reactions",
reaction_info,
)
self.assert_json_success(add)
events: List[Mapping[str, Any]] = []
with self.tornado_redirected_to_list(events, expected_num_events=1):
result = self.api_delete(
reaction_sender,
f"/api/v1/messages/{pm_id}/reactions",
reaction_info,
)
self.assert_json_success(result)
event = events[0]["event"]
event_user_ids = set(events[0]["users"])
self.assertEqual(expected_recipient_ids, event_user_ids)
self.assertEqual(event["user"]["user_id"], reaction_sender.id)
self.assertEqual(event["user"]["email"], reaction_sender.email)
self.assertEqual(event["user"]["full_name"], reaction_sender.full_name)
self.assertEqual(event["type"], "reaction")
self.assertEqual(event["op"], "remove")
self.assertEqual(event["message_id"], pm_id)
self.assertEqual(event["emoji_name"], reaction_info["emoji_name"])
self.assertEqual(event["emoji_code"], reaction_info["emoji_code"])
self.assertEqual(event["reaction_type"], reaction_info["reaction_type"])
def test_events_sent_after_transaction_commits(self) -> None:
"""
Tests that `send_event` is hooked to `transaction.on_commit`. This is important, because
we don't want to end up holding locks on message rows for too long if the event queue runs
into a problem.
"""
hamlet = self.example_user("hamlet")
self.send_stream_message(hamlet, "Denmark")
message = self.get_last_message()
reaction = Reaction(
user_profile=hamlet,
message=message,
emoji_name="whatever",
emoji_code="whatever",
reaction_type="whatever",
)
with self.tornado_redirected_to_list([], expected_num_events=1):
with mock.patch("zerver.lib.actions.send_event") as m:
m.side_effect = AssertionError(
"Events should be sent only after the transaction commits."
)
notify_reaction_update(hamlet, message, reaction, "stuff")