# -*- coding: utf-8 -*- # See https://zulip.readthedocs.io/en/latest/subsystems/events-system.html for # high-level documentation on how this system works. from typing import Any, Callable, Dict, List, Optional, Set, Text, Tuple, Union import os import shutil import sys from django.conf import settings from django.http import HttpRequest, HttpResponse from django.test import TestCase from django.utils.timezone import now as timezone_now from zerver.models import ( get_client, get_realm, get_stream_recipient, get_stream, get_user, Message, RealmDomain, Recipient, UserMessage, UserPresence, UserProfile, Realm, Subscription, Stream, flush_per_request_caches, UserGroup, Service, ) from zerver.lib.actions import ( try_update_realm_custom_profile_field, bulk_add_subscriptions, bulk_remove_subscriptions, check_add_realm_emoji, check_send_message, check_send_typing_notification, do_add_alert_words, do_add_default_stream, do_add_reaction, do_add_reaction_legacy, do_add_realm_domain, do_add_realm_filter, do_add_streams_to_default_stream_group, do_change_avatar_fields, do_change_bot_owner, do_change_default_all_public_streams, do_change_default_events_register_stream, do_change_default_sending_stream, do_change_default_stream_group_description, do_change_default_stream_group_name, do_change_full_name, do_change_icon_source, do_change_is_admin, do_change_notification_settings, do_change_realm_domain, do_change_stream_description, do_change_subscription_property, do_create_user, do_create_default_stream_group, do_deactivate_stream, do_deactivate_user, do_delete_message, do_mark_hotspot_as_read, do_mute_topic, do_reactivate_user, do_regenerate_api_key, do_remove_alert_words, do_remove_default_stream, do_remove_default_stream_group, do_remove_reaction, do_remove_reaction_legacy, do_remove_realm_domain, do_remove_realm_emoji, do_remove_realm_filter, do_remove_streams_from_default_stream_group, do_rename_stream, do_set_realm_authentication_methods, do_set_realm_message_editing, do_set_realm_property, do_set_user_display_setting, do_set_realm_notifications_stream, do_set_realm_signup_notifications_stream, do_unmute_topic, do_update_embedded_data, do_update_message, do_update_message_flags, do_update_outgoing_webhook_service, do_update_pointer, do_update_user_presence, log_event, lookup_default_stream_groups, notify_realm_custom_profile_fields, check_add_user_group, do_update_user_group_name, do_update_user_group_description, bulk_add_members_to_user_group, remove_members_from_user_group, check_delete_user_group, ) from zerver.lib.events import ( apply_events, fetch_initial_state_data, ) from zerver.lib.message import ( aggregate_unread_data, get_raw_unread_data, render_markdown, UnreadMessagesResult, ) from zerver.lib.test_helpers import POSTRequestMock, get_subscription, \ get_test_image_file, stub_event_queue_user_events, queries_captured from zerver.lib.test_classes import ( ZulipTestCase, ) from zerver.lib.test_runner import slow from zerver.lib.topic_mutes import ( add_topic_mute, ) from zerver.lib.validator import ( check_bool, check_dict, check_dict_only, check_float, check_int, check_list, check_string, equals, check_none_or, Validator, check_url ) from zerver.views.events_register import _default_all_public_streams, _default_narrow from zerver.views.users import add_service from zerver.tornado.event_queue import ( allocate_client_descriptor, clear_client_event_queues_for_testing, get_client_info_for_message_event, process_message_event, EventQueue, ) from zerver.tornado.views import get_events_backend from collections import OrderedDict import mock import time import ujson class LogEventsTest(ZulipTestCase): def test_with_missing_event_log_dir_setting(self) -> None: with self.settings(EVENT_LOG_DIR=None): log_event(dict()) def test_log_event_mkdir(self) -> None: dir_name = 'var/test-log-dir' try: shutil.rmtree(dir_name) except OSError: # nocoverage # assume it doesn't exist already pass self.assertFalse(os.path.exists(dir_name)) with self.settings(EVENT_LOG_DIR=dir_name): event = {} # type: Dict[str, int] log_event(event) self.assertTrue(os.path.exists(dir_name)) class EventsEndpointTest(ZulipTestCase): def test_events_register_endpoint(self) -> None: # This test is intended to get minimal coverage on the # events_register code paths email = self.example_email("hamlet") with mock.patch('zerver.views.events_register.do_events_register', return_value={}): result = self.api_post(email, '/json/register') self.assert_json_success(result) with mock.patch('zerver.lib.events.request_event_queue', return_value=None): result = self.api_post(email, '/json/register') self.assert_json_error(result, "Could not allocate event queue") return_event_queue = '15:11' return_user_events = [] # type: (List[Any]) # Test that call is made to deal with a returning soft deactivated user. with mock.patch('zerver.lib.events.maybe_catch_up_soft_deactivated_user') as fa: with stub_event_queue_user_events(return_event_queue, return_user_events): result = self.api_post(email, '/json/register', dict(event_types=ujson.dumps(['pointer']))) self.assertEqual(fa.call_count, 1) with stub_event_queue_user_events(return_event_queue, return_user_events): result = self.api_post(email, '/json/register', dict(event_types=ujson.dumps(['pointer']))) self.assert_json_success(result) result_dict = result.json() self.assertEqual(result_dict['last_event_id'], -1) self.assertEqual(result_dict['queue_id'], '15:11') return_event_queue = '15:12' return_user_events = [ { 'id': 6, 'type': 'pointer', 'pointer': 15, } ] with stub_event_queue_user_events(return_event_queue, return_user_events): result = self.api_post(email, '/json/register', dict(event_types=ujson.dumps(['pointer']))) self.assert_json_success(result) result_dict = result.json() self.assertEqual(result_dict['last_event_id'], 6) self.assertEqual(result_dict['pointer'], 15) self.assertEqual(result_dict['queue_id'], '15:12') # Now test with `fetch_event_types` not matching the event return_event_queue = '15:13' with stub_event_queue_user_events(return_event_queue, return_user_events): result = self.api_post(email, '/json/register', dict(event_types=ujson.dumps(['pointer']), fetch_event_types=ujson.dumps(['message']))) self.assert_json_success(result) result_dict = result.json() self.assertEqual(result_dict['last_event_id'], 6) # Check that the message event types data is in there self.assertIn('max_message_id', result_dict) # Check that the pointer event types data is not in there self.assertNotIn('pointer', result_dict) self.assertEqual(result_dict['queue_id'], '15:13') # Now test with `fetch_event_types` matching the event with stub_event_queue_user_events(return_event_queue, return_user_events): result = self.api_post(email, '/json/register', dict(fetch_event_types=ujson.dumps(['pointer']), event_types=ujson.dumps(['message']))) self.assert_json_success(result) result_dict = result.json() self.assertEqual(result_dict['last_event_id'], 6) # Check that we didn't fetch the messages data self.assertNotIn('max_message_id', result_dict) # Check that the pointer data is in there, and is correctly # updated (presering our atomicity guaranteed), though of # course any future pointer events won't be distributed self.assertIn('pointer', result_dict) self.assertEqual(result_dict['pointer'], 15) self.assertEqual(result_dict['queue_id'], '15:13') def test_tornado_endpoint(self) -> None: # This test is mostly intended to get minimal coverage on # the /notify_tornado endpoint, so we can have 100% URL coverage, # but it does exercise a little bit of the codepath. post_data = dict( data=ujson.dumps( dict( event=dict( type='other' ), users=[self.example_user('hamlet').id], ), ), ) req = POSTRequestMock(post_data, user_profile=None) req.META['REMOTE_ADDR'] = '127.0.0.1' result = self.client_post_request('/notify_tornado', req) self.assert_json_error(result, 'Access denied', status_code=403) post_data['secret'] = settings.SHARED_SECRET req = POSTRequestMock(post_data, user_profile=None) req.META['REMOTE_ADDR'] = '127.0.0.1' result = self.client_post_request('/notify_tornado', req) self.assert_json_success(result) class GetEventsTest(ZulipTestCase): def tornado_call(self, view_func: Callable[[HttpRequest, UserProfile], HttpResponse], user_profile: UserProfile, post_data: Dict[str, Any]) -> HttpResponse: request = POSTRequestMock(post_data, user_profile) return view_func(request, user_profile) def test_get_events(self) -> None: user_profile = self.example_user('hamlet') email = user_profile.email recipient_user_profile = self.example_user('othello') recipient_email = recipient_user_profile.email self.login(email) result = self.tornado_call(get_events_backend, user_profile, {"apply_markdown": ujson.dumps(True), "client_gravatar": ujson.dumps(True), "event_types": ujson.dumps(["message"]), "user_client": "website", "dont_block": ujson.dumps(True), }) self.assert_json_success(result) queue_id = ujson.loads(result.content)["queue_id"] recipient_result = self.tornado_call(get_events_backend, recipient_user_profile, {"apply_markdown": ujson.dumps(True), "client_gravatar": ujson.dumps(True), "event_types": ujson.dumps(["message"]), "user_client": "website", "dont_block": ujson.dumps(True), }) self.assert_json_success(recipient_result) recipient_queue_id = ujson.loads(recipient_result.content)["queue_id"] result = self.tornado_call(get_events_backend, user_profile, {"queue_id": queue_id, "user_client": "website", "last_event_id": -1, "dont_block": ujson.dumps(True), }) events = ujson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 0) local_id = '10.01' check_send_message( sender=user_profile, client=get_client('whatever'), message_type_name='private', message_to=[recipient_email], topic_name=None, message_content='hello', local_id=local_id, sender_queue_id=queue_id, ) result = self.tornado_call(get_events_backend, user_profile, {"queue_id": queue_id, "user_client": "website", "last_event_id": -1, "dont_block": ujson.dumps(True), }) events = ujson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 1) self.assertEqual(events[0]["type"], "message") self.assertEqual(events[0]["message"]["sender_email"], email) self.assertEqual(events[0]["local_message_id"], local_id) self.assertEqual(events[0]["message"]["display_recipient"][0]["is_mirror_dummy"], False) self.assertEqual(events[0]["message"]["display_recipient"][1]["is_mirror_dummy"], False) last_event_id = events[0]["id"] local_id = '10.02' check_send_message( sender=user_profile, client=get_client('whatever'), message_type_name='private', message_to=[recipient_email], topic_name=None, message_content='hello', local_id=local_id, sender_queue_id=queue_id, ) result = self.tornado_call(get_events_backend, user_profile, {"queue_id": queue_id, "user_client": "website", "last_event_id": last_event_id, "dont_block": ujson.dumps(True), }) events = ujson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 1) self.assertEqual(events[0]["type"], "message") self.assertEqual(events[0]["message"]["sender_email"], email) self.assertEqual(events[0]["local_message_id"], local_id) # Test that the received message in the receiver's event queue # exists and does not contain a local id recipient_result = self.tornado_call(get_events_backend, recipient_user_profile, {"queue_id": recipient_queue_id, "user_client": "website", "last_event_id": -1, "dont_block": ujson.dumps(True), }) recipient_events = ujson.loads(recipient_result.content)["events"] self.assert_json_success(recipient_result) self.assertEqual(len(recipient_events), 2) self.assertEqual(recipient_events[0]["type"], "message") self.assertEqual(recipient_events[0]["message"]["sender_email"], email) self.assertTrue("local_message_id" not in recipient_events[0]) self.assertEqual(recipient_events[1]["type"], "message") self.assertEqual(recipient_events[1]["message"]["sender_email"], email) self.assertTrue("local_message_id" not in recipient_events[1]) def test_get_events_narrow(self) -> None: user_profile = self.example_user('hamlet') email = user_profile.email self.login(email) def get_message(apply_markdown: bool, client_gravatar: bool) -> Dict[str, Any]: result = self.tornado_call( get_events_backend, user_profile, dict( apply_markdown=ujson.dumps(apply_markdown), client_gravatar=ujson.dumps(client_gravatar), event_types=ujson.dumps(["message"]), narrow=ujson.dumps([["stream", "denmark"]]), user_client="website", dont_block=ujson.dumps(True), ) ) self.assert_json_success(result) queue_id = ujson.loads(result.content)["queue_id"] result = self.tornado_call(get_events_backend, user_profile, {"queue_id": queue_id, "user_client": "website", "last_event_id": -1, "dont_block": ujson.dumps(True), }) events = ujson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 0) self.send_personal_message(email, self.example_email("othello"), "hello") self.send_stream_message(email, "Denmark", "**hello**") result = self.tornado_call(get_events_backend, user_profile, {"queue_id": queue_id, "user_client": "website", "last_event_id": -1, "dont_block": ujson.dumps(True), }) events = ujson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 1) self.assertEqual(events[0]["type"], "message") return events[0]['message'] message = get_message(apply_markdown=False, client_gravatar=False) self.assertEqual(message["display_recipient"], "Denmark") self.assertEqual(message["content"], "**hello**") self.assertIn('gravatar.com', message["avatar_url"]) message = get_message(apply_markdown=True, client_gravatar=False) self.assertEqual(message["display_recipient"], "Denmark") self.assertEqual(message["content"], "
hello
") self.assertIn('gravatar.com', message["avatar_url"]) message = get_message(apply_markdown=False, client_gravatar=True) self.assertEqual(message["display_recipient"], "Denmark") self.assertEqual(message["content"], "**hello**") self.assertEqual(message["avatar_url"], None) message = get_message(apply_markdown=True, client_gravatar=True) self.assertEqual(message["display_recipient"], "Denmark") self.assertEqual(message["content"], "hello
") self.assertEqual(message["avatar_url"], None) class EventsRegisterTest(ZulipTestCase): def setUp(self) -> None: super().setUp() self.user_profile = self.example_user('hamlet') def create_bot(self, email: str, **extras: Any) -> Optional[UserProfile]: return self.create_test_bot(email, self.user_profile, **extras) def realm_bot_schema(self, field_name: str, check: Validator) -> Validator: return self.check_events_dict([ ('type', equals('realm_bot')), ('op', equals('update')), ('bot', check_dict_only([ ('email', check_string), ('user_id', check_int), (field_name, check), ])), ]) def do_test(self, action: Callable[[], Any], event_types: Optional[List[str]]=None, include_subscribers: bool=True, state_change_expected: bool=True, client_gravatar: bool=False, num_events: int=1) -> List[Dict[str, Any]]: ''' Make sure we have a clean slate of client descriptors for these tests. If we don't do this, then certain failures will only manifest when you run multiple tests. ''' clear_client_event_queues_for_testing() client = allocate_client_descriptor( dict(user_profile_id = self.user_profile.id, user_profile_email = self.user_profile.email, realm_id = self.user_profile.realm_id, event_types = event_types, client_type_name = "website", apply_markdown = True, client_gravatar = client_gravatar, all_public_streams = False, queue_timeout = 600, last_connection_time = time.time(), narrow = []) ) # hybrid_state = initial fetch state + re-applying events triggered by our action # normal_state = do action then fetch at the end (the "normal" code path) hybrid_state = fetch_initial_state_data( self.user_profile, event_types, "", client_gravatar=True, include_subscribers=include_subscribers ) action() events = client.event_queue.contents() self.assertTrue(len(events) == num_events) before = ujson.dumps(hybrid_state) apply_events(hybrid_state, events, self.user_profile, client_gravatar=True, include_subscribers=include_subscribers) after = ujson.dumps(hybrid_state) if state_change_expected: if before == after: print(events) # nocoverage raise AssertionError('Test does not exercise enough code -- events do not change state.') else: if before != after: raise AssertionError('Test is invalid--state actually does change here.') normal_state = fetch_initial_state_data( self.user_profile, event_types, "", client_gravatar=True, include_subscribers=include_subscribers ) self.match_states(hybrid_state, normal_state, events) return events def assert_on_error(self, error: Optional[str]) -> None: if error: raise AssertionError(error) def match_states(self, state1: Dict[str, Any], state2: Dict[str, Any], events: List[Dict[str, Any]]) -> None: def normalize(state: Dict[str, Any]) -> None: for u in state['never_subscribed']: if 'subscribers' in u: u['subscribers'].sort() for u in state['subscriptions']: if 'subscribers' in u: u['subscribers'].sort() state['subscriptions'] = {u['name']: u for u in state['subscriptions']} state['unsubscribed'] = {u['name']: u for u in state['unsubscribed']} if 'realm_bots' in state: state['realm_bots'] = {u['email']: u for u in state['realm_bots']} normalize(state1) normalize(state2) # If this assertions fails, we have unusual problems. self.assertEqual(state1.keys(), state2.keys()) # The far more likely scenario is that some section of # our enormous payload does not get updated properly. We # want the diff here to be developer-friendly, hence # the somewhat tedious code to provide useful output. if state1 != state2: # nocoverage print('\n---States DO NOT MATCH---') print('\nEVENTS:\n') # Printing out the events is a big help to # developers. import json for event in events: print(json.dumps(event, indent=4)) print('\nMISMATCHES:\n') for k in state1: if state1[k] != state2[k]: print('\nkey = ' + k) try: self.assertEqual({k: state1[k]}, {k: state2[k]}) except AssertionError as e: print(e) print(''' NOTE: This is an advanced test that verifies how we apply events after fetching data. If you do not know how to debug it, you can ask for help on chat. ''') sys.stdout.flush() raise AssertionError('Mismatching states') def check_events_dict(self, required_keys: List[Tuple[str, Validator]]) -> Validator: required_keys.append(('id', check_int)) return check_dict_only(required_keys) def test_mentioned_send_message_events(self) -> None: user = self.example_user('hamlet') for i in range(3): content = 'mentioning... @**' + user.full_name + '** hello ' + str(i) self.do_test( lambda: self.send_stream_message(self.example_email('cordelia'), "Verona", content) ) def test_pm_send_message_events(self) -> None: self.do_test( lambda: self.send_personal_message(self.example_email('cordelia'), self.example_email('hamlet'), 'hola') ) def test_huddle_send_message_events(self) -> None: huddle = [ self.example_email('hamlet'), self.example_email('othello'), ] self.do_test( lambda: self.send_huddle_message(self.example_email('cordelia'), huddle, 'hola') ) def test_stream_send_message_events(self) -> None: def check_none(var_name: str, val: Any) -> Optional[str]: assert(val is None) return None def get_checker(check_gravatar: Validator) -> Validator: schema_checker = self.check_events_dict([ ('type', equals('message')), ('flags', check_list(None)), ('message', self.check_events_dict([ ('avatar_url', check_gravatar), ('client', check_string), ('content', check_string), ('content_type', equals('text/html')), ('display_recipient', check_string), ('is_me_message', check_bool), ('reactions', check_list(None)), ('recipient_id', check_int), ('sender_realm_str', check_string), ('sender_email', check_string), ('sender_full_name', check_string), ('sender_id', check_int), ('sender_short_name', check_string), ('stream_id', check_int), ('subject', check_string), ('subject_links', check_list(None)), ('timestamp', check_int), ('type', check_string), ])), ]) return schema_checker events = self.do_test( lambda: self.send_stream_message(self.example_email("hamlet"), "Verona", "hello"), client_gravatar=False, ) schema_checker = get_checker(check_gravatar=check_string) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test( lambda: self.send_stream_message(self.example_email("hamlet"), "Verona", "hello"), client_gravatar=True, ) schema_checker = get_checker(check_gravatar=check_none) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) # Verify message editing schema_checker = self.check_events_dict([ ('type', equals('update_message')), ('flags', check_list(None)), ('content', check_string), ('edit_timestamp', check_int), ('flags', check_list(None)), ('message_id', check_int), ('message_ids', check_list(check_int)), ('prior_mention_user_ids', check_list(check_int)), ('mention_user_ids', check_list(check_int)), ('presence_idle_user_ids', check_list(check_int)), ('stream_push_user_ids', check_list(check_int)), ('push_notify_user_ids', check_list(check_int)), ('orig_content', check_string), ('orig_rendered_content', check_string), ('orig_subject', check_string), ('prev_rendered_content_version', check_int), ('propagate_mode', check_string), ('rendered_content', check_string), ('sender', check_string), ('stream_id', check_int), ('stream_name', check_string), ('subject', check_string), ('subject_links', check_list(None)), ('user_id', check_int), ('is_me_message', check_bool), ]) message = Message.objects.order_by('-id')[0] topic = 'new_topic' propagate_mode = 'change_all' content = 'new content' rendered_content = render_markdown(message, content) prior_mention_user_ids = set() # type: Set[int] mentioned_user_ids = set() # type: Set[int] events = self.do_test( lambda: do_update_message(self.user_profile, message, topic, propagate_mode, content, rendered_content, prior_mention_user_ids, mentioned_user_ids), state_change_expected=True, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) # Verify do_update_embedded_data schema_checker = self.check_events_dict([ ('type', equals('update_message')), ('flags', check_list(None)), ('content', check_string), ('flags', check_list(None)), ('message_id', check_int), ('message_ids', check_list(check_int)), ('rendered_content', check_string), ('sender', check_string), ]) events = self.do_test( lambda: do_update_embedded_data(self.user_profile, message, u"embed_content", "embed_content
"), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_update_message_flags(self) -> None: # Test message flag update events schema_checker = self.check_events_dict([ ('all', check_bool), ('type', equals('update_message_flags')), ('flag', check_string), ('messages', check_list(check_int)), ('operation', equals("add")), ]) message = self.send_personal_message( self.example_email("cordelia"), self.example_email("hamlet"), "hello", ) user_profile = self.example_user('hamlet') events = self.do_test( lambda: do_update_message_flags(user_profile, 'add', 'starred', [message]), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) schema_checker = self.check_events_dict([ ('all', check_bool), ('type', equals('update_message_flags')), ('flag', check_string), ('messages', check_list(check_int)), ('operation', equals("remove")), ]) events = self.do_test( lambda: do_update_message_flags(user_profile, 'remove', 'starred', [message]), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_update_read_flag_removes_unread_msg_ids(self) -> None: user_profile = self.example_user('hamlet') mention = '@**' + user_profile.full_name + '**' for content in ['hello', mention]: message = self.send_stream_message( self.example_email('cordelia'), "Verona", content ) self.do_test( lambda: do_update_message_flags(user_profile, 'add', 'read', [message]), state_change_expected=True, ) def test_send_message_to_existing_recipient(self) -> None: self.send_stream_message( self.example_email('cordelia'), "Verona", "hello 1" ) self.do_test( lambda: self.send_stream_message("cordelia@zulip.com", "Verona", "hello 2"), state_change_expected=True, ) def test_add_reaction_legacy(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('reaction')), ('op', equals('add')), ('message_id', check_int), ('emoji_name', check_string), ('emoji_code', check_string), ('reaction_type', check_string), ('user', check_dict_only([ ('email', check_string), ('full_name', check_string), ('user_id', check_int) ])), ]) message_id = self.send_stream_message(self.example_email("hamlet"), "Verona", "hello") message = Message.objects.get(id=message_id) events = self.do_test( lambda: do_add_reaction_legacy( self.user_profile, message, "tada"), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_remove_reaction_legacy(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('reaction')), ('op', equals('remove')), ('message_id', check_int), ('emoji_name', check_string), ('emoji_code', check_string), ('reaction_type', check_string), ('user', check_dict_only([ ('email', check_string), ('full_name', check_string), ('user_id', check_int) ])), ]) message_id = self.send_stream_message(self.example_email("hamlet"), "Verona", "hello") message = Message.objects.get(id=message_id) do_add_reaction_legacy(self.user_profile, message, "tada") events = self.do_test( lambda: do_remove_reaction_legacy( self.user_profile, message, "tada"), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_add_reaction(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('reaction')), ('op', equals('add')), ('message_id', check_int), ('emoji_name', check_string), ('emoji_code', check_string), ('reaction_type', check_string), ('user', check_dict_only([ ('email', check_string), ('full_name', check_string), ('user_id', check_int) ])), ]) message_id = self.send_stream_message(self.example_email("hamlet"), "Verona", "hello") message = Message.objects.get(id=message_id) events = self.do_test( lambda: do_add_reaction( self.user_profile, message, "tada", "1f389", "unicode_emoji"), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_remove_reaction(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('reaction')), ('op', equals('remove')), ('message_id', check_int), ('emoji_name', check_string), ('emoji_code', check_string), ('reaction_type', check_string), ('user', check_dict_only([ ('email', check_string), ('full_name', check_string), ('user_id', check_int) ])), ]) message_id = self.send_stream_message(self.example_email("hamlet"), "Verona", "hello") message = Message.objects.get(id=message_id) do_add_reaction(self.user_profile, message, "tada", "1f389", "unicode_emoji") events = self.do_test( lambda: do_remove_reaction( self.user_profile, message, "1f389", "unicode_emoji"), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_typing_events(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('typing')), ('op', equals('start')), ('sender', check_dict_only([ ('email', check_string), ('user_id', check_int)])), ('recipients', check_list(check_dict_only([ ('email', check_string), ('user_id', check_int), ]))), ]) events = self.do_test( lambda: check_send_typing_notification( self.user_profile, [self.example_email("cordelia")], "start"), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_custom_profile_fields_events(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('custom_profile_fields')), ('op', equals('add')), ('fields', check_list(check_dict_only([ ('id', check_int), ('type', check_int), ('name', check_string), ('hint', check_string), ]))), ]) events = self.do_test( lambda: notify_realm_custom_profile_fields( self.user_profile.realm, 'add'), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) realm = self.user_profile.realm field = realm.customprofilefield_set.get(realm=realm, name='Biography') name = field.name hint = 'Biography of the user' try_update_realm_custom_profile_field(realm, field, name, hint=hint) events = self.do_test( lambda: notify_realm_custom_profile_fields( self.user_profile.realm, 'add'), state_change_expected=False, ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_presence_events(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('presence')), ('email', check_string), ('server_timestamp', check_float), ('presence', check_dict_only([ ('website', check_dict_only([ ('status', equals('active')), ('timestamp', check_int), ('client', check_string), ('pushable', check_bool), ])), ])), ]) events = self.do_test(lambda: do_update_user_presence( self.user_profile, get_client("website"), timezone_now(), UserPresence.ACTIVE)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_presence_events_multiple_clients(self) -> None: schema_checker_android = self.check_events_dict([ ('type', equals('presence')), ('email', check_string), ('server_timestamp', check_float), ('presence', check_dict_only([ ('ZulipAndroid/1.0', check_dict_only([ ('status', equals('idle')), ('timestamp', check_int), ('client', check_string), ('pushable', check_bool), ])), ])), ]) self.api_post(self.user_profile.email, "/api/v1/users/me/presence", {'status': 'idle'}, HTTP_USER_AGENT="ZulipAndroid/1.0") self.do_test(lambda: do_update_user_presence( self.user_profile, get_client("website"), timezone_now(), UserPresence.ACTIVE)) events = self.do_test(lambda: do_update_user_presence( self.user_profile, get_client("ZulipAndroid/1.0"), timezone_now(), UserPresence.IDLE)) error = schema_checker_android('events[0]', events[0]) self.assert_on_error(error) def test_pointer_events(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('pointer')), ('pointer', check_int) ]) events = self.do_test(lambda: do_update_pointer(self.user_profile, 1500)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_register_events(self) -> None: realm_user_add_checker = self.check_events_dict([ ('type', equals('realm_user')), ('op', equals('add')), ('person', check_dict_only([ ('user_id', check_int), ('email', check_string), ('avatar_url', check_none_or(check_string)), ('full_name', check_string), ('is_admin', check_bool), ('is_bot', check_bool), ('timezone', check_string), ])), ]) events = self.do_test(lambda: self.register("test1@zulip.com", "test1")) self.assert_length(events, 1) error = realm_user_add_checker('events[0]', events[0]) self.assert_on_error(error) def test_alert_words_events(self) -> None: alert_words_checker = self.check_events_dict([ ('type', equals('alert_words')), ('alert_words', check_list(check_string)), ]) events = self.do_test(lambda: do_add_alert_words(self.user_profile, ["alert_word"])) error = alert_words_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test(lambda: do_remove_alert_words(self.user_profile, ["alert_word"])) error = alert_words_checker('events[0]', events[0]) self.assert_on_error(error) def test_user_group_events(self) -> None: user_group_add_checker = self.check_events_dict([ ('type', equals('user_group')), ('op', equals('add')), ('group', check_dict_only([ ('id', check_int), ('name', check_string), ('members', check_list(check_int)), ('description', check_string), ])), ]) othello = self.example_user('othello') zulip = get_realm('zulip') events = self.do_test(lambda: check_add_user_group(zulip, 'backend', [othello], 'Backend team')) error = user_group_add_checker('events[0]', events[0]) self.assert_on_error(error) # Test name update user_group_update_checker = self.check_events_dict([ ('type', equals('user_group')), ('op', equals('update')), ('group_id', check_int), ('data', check_dict_only([ ('name', check_string), ])), ]) backend = UserGroup.objects.get(name='backend') events = self.do_test(lambda: do_update_user_group_name(backend, 'backendteam')) error = user_group_update_checker('events[0]', events[0]) self.assert_on_error(error) # Test description update user_group_update_checker = self.check_events_dict([ ('type', equals('user_group')), ('op', equals('update')), ('group_id', check_int), ('data', check_dict_only([ ('description', check_string), ])), ]) description = "Backend team to deal with backend code." events = self.do_test(lambda: do_update_user_group_description(backend, description)) error = user_group_update_checker('events[0]', events[0]) self.assert_on_error(error) # Test add members user_group_add_member_checker = self.check_events_dict([ ('type', equals('user_group')), ('op', equals('add_members')), ('group_id', check_int), ('user_ids', check_list(check_int)), ]) hamlet = self.example_user('hamlet') events = self.do_test(lambda: bulk_add_members_to_user_group(backend, [hamlet])) error = user_group_add_member_checker('events[0]', events[0]) self.assert_on_error(error) # Test remove members user_group_remove_member_checker = self.check_events_dict([ ('type', equals('user_group')), ('op', equals('remove_members')), ('group_id', check_int), ('user_ids', check_list(check_int)), ]) hamlet = self.example_user('hamlet') events = self.do_test(lambda: remove_members_from_user_group(backend, [hamlet])) error = user_group_remove_member_checker('events[0]', events[0]) self.assert_on_error(error) # Test delete event user_group_remove_checker = self.check_events_dict([ ('type', equals('user_group')), ('op', equals('remove')), ('group_id', check_int), ]) events = self.do_test(lambda: check_delete_user_group(backend.id, othello)) error = user_group_remove_checker('events[0]', events[0]) self.assert_on_error(error) def test_default_stream_groups_events(self) -> None: default_stream_groups_checker = self.check_events_dict([ ('type', equals('default_stream_groups')), ('default_stream_groups', check_list(check_dict_only([ ('name', check_string), ('id', check_int), ('description', check_string), ('streams', check_list(check_dict_only([ ('description', check_string), ('invite_only', check_bool), ('name', check_string), ('stream_id', check_int)]))), ]))), ]) streams = [] for stream_name in ["Scotland", "Verona", "Denmark"]: streams.append(get_stream(stream_name, self.user_profile.realm)) events = self.do_test(lambda: do_create_default_stream_group( self.user_profile.realm, "group1", "This is group1", streams)) error = default_stream_groups_checker('events[0]', events[0]) self.assert_on_error(error) group = lookup_default_stream_groups(["group1"], self.user_profile.realm)[0] venice_stream = get_stream("Venice", self.user_profile.realm) events = self.do_test(lambda: do_add_streams_to_default_stream_group(self.user_profile.realm, group, [venice_stream])) error = default_stream_groups_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test(lambda: do_remove_streams_from_default_stream_group(self.user_profile.realm, group, [venice_stream])) error = default_stream_groups_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test(lambda: do_change_default_stream_group_description(self.user_profile.realm, group, "New description")) error = default_stream_groups_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test(lambda: do_change_default_stream_group_name(self.user_profile.realm, group, "New Group Name")) error = default_stream_groups_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test(lambda: do_remove_default_stream_group(self.user_profile.realm, group)) error = default_stream_groups_checker('events[0]', events[0]) self.assert_on_error(error) def test_default_streams_events(self) -> None: default_streams_checker = self.check_events_dict([ ('type', equals('default_streams')), ('default_streams', check_list(check_dict_only([ ('description', check_string), ('invite_only', check_bool), ('name', check_string), ('stream_id', check_int), ]))), ]) stream = get_stream("Scotland", self.user_profile.realm) events = self.do_test(lambda: do_add_default_stream(stream)) error = default_streams_checker('events[0]', events[0]) events = self.do_test(lambda: do_remove_default_stream(stream)) error = default_streams_checker('events[0]', events[0]) self.assert_on_error(error) def test_muted_topics_events(self) -> None: muted_topics_checker = self.check_events_dict([ ('type', equals('muted_topics')), ('muted_topics', check_list(check_list(check_string, 2))), ]) stream = get_stream('Denmark', self.user_profile.realm) recipient = get_stream_recipient(stream.id) events = self.do_test(lambda: do_mute_topic( self.user_profile, stream, recipient, "topic")) error = muted_topics_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test(lambda: do_unmute_topic( self.user_profile, stream, "topic")) error = muted_topics_checker('events[0]', events[0]) self.assert_on_error(error) def test_change_avatar_fields(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm_user')), ('op', equals('update')), ('person', check_dict_only([ ('email', check_string), ('user_id', check_int), ('avatar_url', check_string), ('avatar_url_medium', check_string), ('avatar_source', check_string), ])), ]) events = self.do_test( lambda: do_change_avatar_fields(self.user_profile, UserProfile.AVATAR_FROM_USER), ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) schema_checker = self.check_events_dict([ ('type', equals('realm_user')), ('op', equals('update')), ('person', check_dict_only([ ('email', check_string), ('user_id', check_int), ('avatar_url', check_none_or(check_string)), ('avatar_url_medium', check_none_or(check_string)), ('avatar_source', check_string), ])), ]) events = self.do_test( lambda: do_change_avatar_fields(self.user_profile, UserProfile.AVATAR_FROM_GRAVATAR), ) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_change_full_name(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm_user')), ('op', equals('update')), ('person', check_dict_only([ ('email', check_string), ('full_name', check_string), ('user_id', check_int), ])), ]) events = self.do_test(lambda: do_change_full_name(self.user_profile, 'Sir Hamlet', self.user_profile)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def do_set_realm_property_test(self, name: str) -> None: bool_tests = [True, False, True] # type: List[bool] test_values = dict( default_language=[u'es', u'de', u'en'], description=[u'Realm description', u'New description'], message_retention_days=[10, 20], name=[u'Zulip', u'New Name'], waiting_period_threshold=[10, 20], bot_creation_policy=[Realm.BOT_CREATION_EVERYONE], ) # type: Dict[str, Any] vals = test_values.get(name) property_type = Realm.property_types[name] if property_type is bool: validator = check_bool vals = bool_tests elif property_type is Text: validator = check_string elif property_type is int: validator = check_int elif property_type == (int, type(None)): validator = check_int else: raise AssertionError("Unexpected property type %s" % (property_type,)) schema_checker = self.check_events_dict([ ('type', equals('realm')), ('op', equals('update')), ('property', equals(name)), ('value', validator), ]) if vals is None: raise AssertionError('No test created for %s' % (name)) do_set_realm_property(self.user_profile.realm, name, vals[0]) for val in vals[1:]: events = self.do_test( lambda: do_set_realm_property(self.user_profile.realm, name, val)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) @slow("Actually runs several full-stack fetching tests") def test_change_realm_property(self) -> None: for prop in Realm.property_types: self.do_set_realm_property_test(prop) @slow("Runs a large matrix of tests") def test_change_realm_authentication_methods(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm')), ('op', equals('update_dict')), ('property', equals('default')), ('data', check_dict_only([ ('authentication_methods', check_dict([])) ])), ]) def fake_backends() -> Any: backends = ( 'zproject.backends.DevAuthBackend', 'zproject.backends.EmailAuthBackend', 'zproject.backends.GitHubAuthBackend', 'zproject.backends.GoogleMobileOauth2Backend', 'zproject.backends.ZulipLDAPAuthBackend', ) return self.settings(AUTHENTICATION_BACKENDS=backends) # Test transitions; any new backends should be tested with T/T/T/F/T for (auth_method_dict) in \ ({'Google': True, 'Email': True, 'GitHub': True, 'LDAP': False, 'Dev': False}, {'Google': True, 'Email': True, 'GitHub': False, 'LDAP': False, 'Dev': False}, {'Google': True, 'Email': False, 'GitHub': False, 'LDAP': False, 'Dev': False}, {'Google': True, 'Email': False, 'GitHub': True, 'LDAP': False, 'Dev': False}, {'Google': False, 'Email': False, 'GitHub': False, 'LDAP': False, 'Dev': True}, {'Google': False, 'Email': False, 'GitHub': True, 'LDAP': False, 'Dev': True}, {'Google': False, 'Email': True, 'GitHub': True, 'LDAP': True, 'Dev': False}): with fake_backends(): events = self.do_test( lambda: do_set_realm_authentication_methods( self.user_profile.realm, auth_method_dict)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_change_pin_stream(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('subscription')), ('op', equals('update')), ('property', equals('pin_to_top')), ('stream_id', check_int), ('value', check_bool), ('name', check_string), ('email', check_string), ]) stream = get_stream("Denmark", self.user_profile.realm) sub = get_subscription(stream.name, self.user_profile) do_change_subscription_property(self.user_profile, sub, stream, "pin_to_top", False) for pinned in (True, False): events = self.do_test(lambda: do_change_subscription_property(self.user_profile, sub, stream, "pin_to_top", pinned)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) @slow("Runs a matrix of 6 queries to the /home view") def test_change_realm_message_edit_settings(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm')), ('op', equals('update_dict')), ('property', equals('default')), ('data', check_dict_only([ ('allow_message_editing', check_bool), ('message_content_edit_limit_seconds', check_int), ('allow_community_topic_editing', check_bool), ])), ]) # Test every transition among the four possibilities {T,F} x {0, non-0} for (allow_message_editing, message_content_edit_limit_seconds) in \ ((True, 0), (False, 0), (False, 1234), (True, 600), (False, 0), (True, 1234)): events = self.do_test( lambda: do_set_realm_message_editing(self.user_profile.realm, allow_message_editing, message_content_edit_limit_seconds, False)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_change_realm_notifications_stream(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm')), ('op', equals('update')), ('property', equals('notifications_stream_id')), ('value', check_int), ]) stream = get_stream("Rome", self.user_profile.realm) for notifications_stream, notifications_stream_id in ((stream, stream.id), (None, -1)): events = self.do_test( lambda: do_set_realm_notifications_stream(self.user_profile.realm, notifications_stream, notifications_stream_id)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_change_realm_signup_notifications_stream(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm')), ('op', equals('update')), ('property', equals('signup_notifications_stream_id')), ('value', check_int), ]) stream = get_stream("Rome", self.user_profile.realm) for signup_notifications_stream, signup_notifications_stream_id in ((stream, stream.id), (None, -1)): events = self.do_test( lambda: do_set_realm_signup_notifications_stream(self.user_profile.realm, signup_notifications_stream, signup_notifications_stream_id)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_change_is_admin(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm_user')), ('op', equals('update')), ('person', check_dict_only([ ('email', check_string), ('is_admin', check_bool), ('user_id', check_int), ])), ]) do_change_is_admin(self.user_profile, False) for is_admin in [True, False]: events = self.do_test(lambda: do_change_is_admin(self.user_profile, is_admin)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def do_set_user_display_settings_test(self, setting_name: str) -> None: """Test updating each setting in UserProfile.property_types dict.""" bool_change = [True, False, True] # type: List[bool] test_changes = dict( emojiset = [u'apple', u'twitter'], default_language = [u'es', u'de', u'en'], timezone = [u'US/Mountain', u'US/Samoa', u'Pacific/Galapogos', u''] ) # type: Dict[str, Any] property_type = UserProfile.property_types[setting_name] if property_type is bool: validator = check_bool elif property_type is Text: validator = check_string else: raise AssertionError("Unexpected property type %s" % (property_type,)) num_events = 1 if setting_name == "timezone": num_events = 2 values = test_changes.get(setting_name) if property_type is bool: values = bool_change if values is None: raise AssertionError('No test created for %s' % (setting_name)) for value in values: events = self.do_test(lambda: do_set_user_display_setting( self.user_profile, setting_name, value), num_events=num_events) schema_checker = self.check_events_dict([ ('type', equals('update_display_settings')), ('setting_name', equals(setting_name)), ('user', check_string), ('setting', validator), ]) language_schema_checker = self.check_events_dict([ ('type', equals('update_display_settings')), ('language_name', check_string), ('setting_name', equals(setting_name)), ('user', check_string), ('setting', validator), ]) if setting_name == "default_language": error = language_schema_checker('events[0]', events[0]) else: error = schema_checker('events[0]', events[0]) self.assert_on_error(error) timezone_schema_checker = self.check_events_dict([ ('type', equals('realm_user')), ('op', equals('update')), ('person', check_dict_only([ ('email', check_string), ('user_id', check_int), ('timezone', check_string), ])), ]) if setting_name == "timezone": error = timezone_schema_checker('events[1]', events[1]) @slow("Actually runs several full-stack fetching tests") def test_set_user_display_settings(self) -> None: for prop in UserProfile.property_types: self.do_set_user_display_settings_test(prop) @slow("Actually runs several full-stack fetching tests") def test_change_notification_settings(self) -> None: for notification_setting, v in self.user_profile.notification_setting_types.items(): schema_checker = self.check_events_dict([ ('type', equals('update_global_notifications')), ('notification_name', equals(notification_setting)), ('user', check_string), ('setting', check_bool), ]) do_change_notification_settings(self.user_profile, notification_setting, False) for setting_value in [True, False]: events = self.do_test(lambda: do_change_notification_settings( self.user_profile, notification_setting, setting_value, log=False)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_realm_emoji_events(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm_emoji')), ('op', equals('update')), ('realm_emoji', check_dict([])), ]) author = self.example_user('iago') with get_test_image_file('img.png') as img_file: events = self.do_test(lambda: check_add_realm_emoji(get_realm("zulip"), "my_emoji", author, img_file)) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) events = self.do_test(lambda: do_remove_realm_emoji(get_realm("zulip"), "my_emoji")) error = schema_checker('events[0]', events[0]) self.assert_on_error(error) def test_realm_filter_events(self) -> None: schema_checker = self.check_events_dict([ ('type', equals('realm_filters')), ('realm_filters', check_list(None)), # TODO: validate tuples in the list ]) events = self.do_test(lambda: do_add_realm_filter(get_realm("zulip"), "#(?P