# -*- coding: utf-8 -*- from __future__ import print_function from __future__ import absolute_import from django.conf import settings from django.test import TestCase from zerver.lib import bugdown from zerver.lib.actions import ( check_add_realm_emoji, do_remove_realm_emoji, do_set_alert_words, get_realm, ) from zerver.lib.alert_words import alert_words_in_realm from zerver.lib.camo import get_camo_url from zerver.lib.message import render_markdown from zerver.lib.request import ( JsonableError, ) from zerver.lib.test_helpers import ( ZulipTestCase, ) from zerver.models import ( domain_in_local_realm_filters_cache, flush_per_request_caches, flush_realm_filter, get_client, get_user_profile_by_email, realm_filters_for_domain, Message, Realm, RealmFilter, Recipient, ) import mock import os import ujson import six from six import text_type from zerver.lib.str_utils import NonBinaryStr from typing import Any, AnyStr, Dict, List, Optional, Tuple class FencedBlockPreprocessorTest(TestCase): def test_simple_quoting(self): # type: () -> None processor = bugdown.fenced_code.FencedBlockPreprocessor(None) markdown = [ '~~~ quote', 'hi', 'bye', '', '' ] expected = [ '', '> hi', '> bye', '', '', '' ] lines = processor.run(markdown) self.assertEqual(lines, expected) def test_serial_quoting(self): # type: () -> None processor = bugdown.fenced_code.FencedBlockPreprocessor(None) markdown = [ '~~~ quote', 'hi', '~~~', '', '~~~ quote', 'bye', '', '' ] expected = [ '', '> hi', '', '', '', '> bye', '', '', '' ] lines = processor.run(markdown) self.assertEqual(lines, expected) def test_serial_code(self): # type: () -> None processor = bugdown.fenced_code.FencedBlockPreprocessor(None) # Simulate code formatting. processor.format_code = lambda lang, code: lang + ':' + code # type: ignore # mypy doesn't allow monkey-patching functions processor.placeholder = lambda s: '**' + s.strip('\n') + '**' # type: ignore # https://github.com/python/mypy/issues/708 markdown = [ '``` .py', 'hello()', '```', '', '``` .py', 'goodbye()', '```', '', '' ] expected = [ '', '**py:hello()**', '', '', '', '**py:goodbye()**', '', '', '' ] lines = processor.run(markdown) self.assertEqual(lines, expected) def test_nested_code(self): # type: () -> None processor = bugdown.fenced_code.FencedBlockPreprocessor(None) # Simulate code formatting. processor.format_code = lambda lang, code: lang + ':' + code # type: ignore # mypy doesn't allow monkey-patching functions processor.placeholder = lambda s: '**' + s.strip('\n') + '**' # type: ignore # https://github.com/python/mypy/issues/708 markdown = [ '~~~ quote', 'hi', '``` .py', 'hello()', '```', '', '' ] expected = [ '', '> hi', '', '> **py:hello()**', '', '', '' ] lines = processor.run(markdown) self.assertEqual(lines, expected) def bugdown_convert(text): # type: (text_type) -> text_type return bugdown.convert(text, "zulip.com") class BugdownTest(TestCase): def common_bugdown_test(self, text, expected): # type: (text_type, text_type) -> None converted = bugdown_convert(text) self.assertEqual(converted, expected) def load_bugdown_tests(self): # type: () -> Tuple[Dict[text_type, Any], List[List[text_type]]] test_fixtures = {} data_file = open(os.path.join(os.path.dirname(__file__), '../fixtures/bugdown-data.json'), 'r') data = ujson.loads('\n'.join(data_file.readlines())) for test in data['regular_tests']: test_fixtures[test['name']] = test return test_fixtures, data['linkify_tests'] def test_bugdown_fixtures(self): # type: () -> None format_tests, linkify_tests = self.load_bugdown_tests() self.maxDiff = None # type: Optional[int] for name, test in six.iteritems(format_tests): converted = bugdown_convert(test['input']) print("Running Bugdown test %s" % (name,)) self.assertEqual(converted, test['expected_output']) def replaced(payload, url, phrase=''): # type: (text_type, text_type, text_type) -> text_type target = " target=\"_blank\"" if url[:4] == 'http': href = url elif '@' in url: href = 'mailto:' + url target = "" else: href = 'http://' + url return payload % ("%s" % (href, target, href, url),) print("Running Bugdown Linkify tests") self.maxDiff = None # type: Optional[int] for inline_url, reference, url in linkify_tests: try: match = replaced(reference, url, phrase=inline_url) except TypeError: match = reference converted = bugdown_convert(inline_url) self.assertEqual(match, converted) def test_inline_youtube(self): # type: () -> None msg = 'Check out the debate: http://www.youtube.com/watch?v=hx1mjT73xYE' converted = bugdown_convert(msg) self.assertEqual(converted, '

Check out the debate: http://www.youtube.com/watch?v=hx1mjT73xYE

\n
') def test_inline_dropbox(self): # type: () -> None msg = 'Look at how hilarious our old office was: https://www.dropbox.com/s/ymdijjcg67hv2ta/IMG_0923.JPG' image_info = {'image': 'https://photos-4.dropbox.com/t/2/AABIre1oReJgPYuc_53iv0IHq1vUzRaDg2rrCfTpiWMccQ/12/129/jpeg/1024x1024/2/_/0/4/IMG_0923.JPG/CIEBIAEgAiAHKAIoBw/ymdijjcg67hv2ta/AABz2uuED1ox3vpWWvMpBxu6a/IMG_0923.JPG', 'desc': 'Shared with Dropbox', 'title': 'IMG_0923.JPG'} with mock.patch('zerver.lib.bugdown.fetch_open_graph_image', return_value=image_info): converted = bugdown_convert(msg) self.assertEqual(converted, '

Look at how hilarious our old office was: https://www.dropbox.com/s/ymdijjcg67hv2ta/IMG_0923.JPG

\n
') msg = 'Look at my hilarious drawing folder: https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl=' image_info = {'image': 'https://cf.dropboxstatic.com/static/images/icons128/folder_dropbox.png', 'desc': 'Shared with Dropbox', 'title': 'Saves'} with mock.patch('zerver.lib.bugdown.fetch_open_graph_image', return_value=image_info): converted = bugdown_convert(msg) self.assertEqual(converted, '

Look at my hilarious drawing folder: https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl=

\n
Saves
') def test_inline_dropbox_preview(self): # type: () -> None # Test photo album previews msg = 'https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5' image_info = {'image': 'https://photos-6.dropbox.com/t/2/AAAlawaeD61TyNewO5vVi-DGf2ZeuayfyHFdNTNzpGq-QA/12/271544745/jpeg/1024x1024/2/_/0/5/baby-piglet.jpg/CKnjvYEBIAIgBygCKAc/tditp9nitko60n5/AADX03VAIrQlTl28CtujDcMla/0', 'desc': 'Shared with Dropbox', 'title': '1 photo'} with mock.patch('zerver.lib.bugdown.fetch_open_graph_image', return_value=image_info): converted = bugdown_convert(msg) self.assertEqual(converted, '

https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5

\n
') def test_inline_dropbox_negative(self): # type: () -> None # Make sure we're not overzealous in our conversion: msg = 'Look at the new dropbox logo: https://www.dropbox.com/static/images/home_logo.png' with mock.patch('zerver.lib.bugdown.fetch_open_graph_image', return_value=None): converted = bugdown_convert(msg) self.assertEqual(converted, '

Look at the new dropbox logo: https://www.dropbox.com/static/images/home_logo.png

\n
') def test_inline_dropbox_bad(self): # type: () -> None # Don't fail on bad dropbox links msg = "https://zulip-test.dropbox.com/photos/cl/ROmr9K1XYtmpneM" with mock.patch('zerver.lib.bugdown.fetch_open_graph_image', return_value=None): converted = bugdown_convert(msg) self.assertEqual(converted, '

https://zulip-test.dropbox.com/photos/cl/ROmr9K1XYtmpneM

') def test_twitter_id_extraction(self): # type: () -> None self.assertEqual(bugdown.get_tweet_id('http://twitter.com/#!/VizzQuotes/status/409030735191097344'), '409030735191097344') self.assertEqual(bugdown.get_tweet_id('http://twitter.com/VizzQuotes/status/409030735191097344'), '409030735191097344') self.assertEqual(bugdown.get_tweet_id('http://twitter.com/VizzQuotes/statuses/409030735191097344'), '409030735191097344') self.assertEqual(bugdown.get_tweet_id('https://twitter.com/wdaher/status/1017581858'), '1017581858') self.assertEqual(bugdown.get_tweet_id('https://twitter.com/wdaher/status/1017581858/'), '1017581858') self.assertEqual(bugdown.get_tweet_id('https://twitter.com/windyoona/status/410766290349879296/photo/1'), '410766290349879296') self.assertEqual(bugdown.get_tweet_id('https://twitter.com/windyoona/status/410766290349879296/'), '410766290349879296') def test_inline_interesting_links(self): # type: () -> None def make_link(url): # type: (text_type) -> text_type return '%s' % (url, url, url) normal_tweet_html = ('@twitter ' 'meets @seepicturely at #tcdisrupt cc.' '@boscomonkey ' '@episod ' 'http://instagram.com/p/MuW67/') mention_in_link_tweet_html = """http://foo.com""" media_tweet_html = ('' 'http://twitter.com/NEVNBoston/status/421654515616849920/photo/1') def make_inline_twitter_preview(url, tweet_html, image_html=''): # type: (text_type, text_type, text_type) -> text_type ## As of right now, all previews are mocked to be the exact same tweet return ('
' '
' '' '' '' '

%s

' '- Eoin McMillan (@imeoin)' '%s' '
' '
') % (url, tweet_html, image_html) msg = 'http://www.twitter.com' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

' % make_link('http://www.twitter.com')) msg = 'http://www.twitter.com/wdaher/' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

' % make_link('http://www.twitter.com/wdaher/')) msg = 'http://www.twitter.com/wdaher/status/3' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

' % make_link('http://www.twitter.com/wdaher/status/3')) # id too long msg = 'http://www.twitter.com/wdaher/status/2879779692873154569' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

' % make_link('http://www.twitter.com/wdaher/status/2879779692873154569')) # id too large (i.e. tweet doesn't exist) msg = 'http://www.twitter.com/wdaher/status/999999999999999999' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

' % make_link('http://www.twitter.com/wdaher/status/999999999999999999')) msg = 'http://www.twitter.com/wdaher/status/287977969287315456' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

\n%s' % ( make_link('http://www.twitter.com/wdaher/status/287977969287315456'), make_inline_twitter_preview('http://www.twitter.com/wdaher/status/287977969287315456', normal_tweet_html))) msg = 'https://www.twitter.com/wdaher/status/287977969287315456' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

\n%s' % ( make_link('https://www.twitter.com/wdaher/status/287977969287315456'), make_inline_twitter_preview('https://www.twitter.com/wdaher/status/287977969287315456', normal_tweet_html))) msg = 'http://twitter.com/wdaher/status/287977969287315456' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

\n%s' % ( make_link('http://twitter.com/wdaher/status/287977969287315456'), make_inline_twitter_preview('http://twitter.com/wdaher/status/287977969287315456', normal_tweet_html))) # A max of 3 will be converted msg = ('http://twitter.com/wdaher/status/287977969287315456 ' 'http://twitter.com/wdaher/status/287977969287315457 ' 'http://twitter.com/wdaher/status/287977969287315457 ' 'http://twitter.com/wdaher/status/287977969287315457') converted = bugdown_convert(msg) self.assertEqual(converted, '

%s %s %s %s

\n%s%s%s' % ( make_link('http://twitter.com/wdaher/status/287977969287315456'), make_link('http://twitter.com/wdaher/status/287977969287315457'), make_link('http://twitter.com/wdaher/status/287977969287315457'), make_link('http://twitter.com/wdaher/status/287977969287315457'), make_inline_twitter_preview('http://twitter.com/wdaher/status/287977969287315456', normal_tweet_html), make_inline_twitter_preview('http://twitter.com/wdaher/status/287977969287315457', normal_tweet_html), make_inline_twitter_preview('http://twitter.com/wdaher/status/287977969287315457', normal_tweet_html))) # Tweet has a mention in a URL, only the URL is linked msg = 'http://twitter.com/wdaher/status/287977969287315458' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

\n%s' % ( make_link('http://twitter.com/wdaher/status/287977969287315458'), make_inline_twitter_preview('http://twitter.com/wdaher/status/287977969287315458', mention_in_link_tweet_html))) # Tweet with an image msg = 'http://twitter.com/wdaher/status/287977969287315459' converted = bugdown_convert(msg) self.assertEqual(converted, '

%s

\n%s' % ( make_link('http://twitter.com/wdaher/status/287977969287315459'), make_inline_twitter_preview('http://twitter.com/wdaher/status/287977969287315459', media_tweet_html, ('
' '' '' '' '
')))) def test_fetch_tweet_data_settings_validation(self): # type: () -> None with self.settings(TEST_SUITE=False, TWITTER_CONSUMER_KEY=None): self.assertIs(None, bugdown.fetch_tweet_data('287977969287315459')) def test_realm_emoji(self): # type: () -> None def emoji_img(name, url): # type: (text_type, text_type) -> text_type return '%s' % (name, get_camo_url(url), name) zulip_realm = get_realm('zulip.com') url = "https://zulip.com/test_realm_emoji.png" check_add_realm_emoji(zulip_realm, "test", url) # Needs to mock an actual message because that's how bugdown obtains the realm msg = Message(sender=get_user_profile_by_email("hamlet@zulip.com")) converted = bugdown.convert(":test:", "zulip.com", msg) self.assertEqual(converted, '

%s

' %(emoji_img(':test:', url))) do_remove_realm_emoji(zulip_realm, 'test') converted = bugdown.convert(":test:", "zulip.com", msg) self.assertEqual(converted, '

:test:

') def test_unicode_emoji(self): # type: () -> None msg = u'\u2615' # ☕ converted = bugdown_convert(msg) self.assertEqual(converted, u'

\u2615

') msg = u'\u2615\u2615' # ☕☕ converted = bugdown_convert(msg) self.assertEqual(converted, u'

\u2615\u2615

') def test_realm_patterns(self): # type: () -> None realm = get_realm('zulip.com') url_format_string = r"https://trac.zulip.net/ticket/%(id)s" realm_filter = RealmFilter(realm=realm, pattern=r"#(?P[0-9]{2,8})", url_format_string=url_format_string) realm_filter.save() self.assertEqual( str(realm_filter), '[0-9]{2,8})' ' https://trac.zulip.net/ticket/%(id)s>') msg = Message(sender=get_user_profile_by_email("othello@zulip.com"), subject="#444") flush_per_request_caches() content = "We should fix #224 and #115, but not issue#124 or #1124z or [trac #15](https://trac.zulip.net/ticket/16) today." converted = bugdown.convert(content, realm_domain='zulip.com', message=msg) converted_subject = bugdown.subject_links(realm.domain.lower(), msg.subject) self.assertEqual(converted, '

We should fix #224 and #115, but not issue#124 or #1124z or trac #15 today.

') self.assertEqual(converted_subject, [u'https://trac.zulip.net/ticket/444']) def test_maybe_update_realm_filters(self): # type: () -> None realm = get_realm('zulip.com') url_format_string = r"https://trac.zulip.net/ticket/%(id)s" realm_filter = RealmFilter(realm=realm, pattern=r"#(?P[0-9]{2,8})", url_format_string=url_format_string) realm_filter.save() bugdown.realm_filter_data = {} bugdown.maybe_update_realm_filters(domain=None) all_filters = bugdown.realm_filter_data zulip_filters = all_filters['zulip.com'] self.assertEqual(len(zulip_filters), 1) self.assertEqual(zulip_filters[0], (u'#(?P[0-9]{2,8})', u'https://trac.zulip.net/ticket/%(id)s')) def test_flush_realm_filter(self): # type: () -> None domain = 'zulip.com' realm = get_realm(domain) def flush(): # type: () -> None ''' flush_realm_filter is a post-save hook, so calling it directly for testing is kind of awkward ''' class Instance(object): realm = None # type: Optional[Realm] instance = Instance() instance.realm = realm flush_realm_filter(sender=None, instance=instance) def save_new_realm_filter(): # type: () -> None realm_filter = RealmFilter(realm=realm, pattern=r"whatever", url_format_string='whatever') realm_filter.save() # start fresh for our domain flush() self.assertFalse(domain_in_local_realm_filters_cache(domain)) # call this just for side effects of populating the cache realm_filters_for_domain(domain=domain) self.assertTrue(domain_in_local_realm_filters_cache(domain)) # Saving a new RealmFilter should have the side effect of # flushing the cache. save_new_realm_filter() self.assertFalse(domain_in_local_realm_filters_cache(domain)) # and flush it one more time, to make sure we don't get a KeyError flush() self.assertFalse(domain_in_local_realm_filters_cache(domain)) def test_realm_patterns_negative(self): # type: () -> None realm = get_realm('zulip.com') RealmFilter(realm=realm, pattern=r"#(?P[0-9]{2,8})", url_format_string=r"https://trac.zulip.net/ticket/%(id)s").save() boring_msg = Message(sender=get_user_profile_by_email("othello@zulip.com"), subject=u"no match here") converted_boring_subject = bugdown.subject_links(realm.domain.lower(), boring_msg.subject) self.assertEqual(converted_boring_subject, []) def test_is_status_message(self): # type: () -> None user_profile = get_user_profile_by_email("othello@zulip.com") msg = Message(sender=user_profile, sending_client=get_client("test")) content = '/me makes a list\n* one\n* two' rendered_content = render_markdown(msg, content) self.assertEqual( rendered_content, '

/me makes a list

\n
    \n
  • one
  • \n
  • two
  • \n
' ) self.assertFalse(Message.is_status_message(content, rendered_content)) content = '/me takes a walk' rendered_content = render_markdown(msg, content) self.assertEqual( rendered_content, '

/me takes a walk

' ) self.assertTrue(Message.is_status_message(content, rendered_content)) def test_alert_words(self): # type: () -> None user_profile = get_user_profile_by_email("othello@zulip.com") do_set_alert_words(user_profile, ["ALERTWORD", "scaryword"]) msg = Message(sender=user_profile, sending_client=get_client("test")) realm_alert_words = alert_words_in_realm(user_profile.realm) def render(msg, content): # type: (Message, text_type) -> text_type return render_markdown(msg, content, realm_alert_words=realm_alert_words, message_users={user_profile}) content = "We have an ALERTWORD day today!" self.assertEqual(render(msg, content), "

We have an ALERTWORD day today!

") self.assertEqual(msg.user_ids_with_alert_words, set([user_profile.id])) msg = Message(sender=user_profile, sending_client=get_client("test")) content = "We have a NOTHINGWORD day today!" self.assertEqual(render(msg, content), "

We have a NOTHINGWORD day today!

") self.assertEqual(msg.user_ids_with_alert_words, set()) def test_mention_wildcard(self): # type: () -> None user_profile = get_user_profile_by_email("othello@zulip.com") msg = Message(sender=user_profile, sending_client=get_client("test")) content = "@all test" self.assertEqual(render_markdown(msg, content), '

@all test

') self.assertTrue(msg.mentions_wildcard) def test_mention_everyone(self): # type: () -> None user_profile = get_user_profile_by_email("othello@zulip.com") msg = Message(sender=user_profile, sending_client=get_client("test")) content = "@everyone test" self.assertEqual(render_markdown(msg, content), '

@everyone test

') self.assertTrue(msg.mentions_wildcard) def test_mention_single(self): # type: () -> None sender_user_profile = get_user_profile_by_email("othello@zulip.com") user_profile = get_user_profile_by_email("hamlet@zulip.com") msg = Message(sender=sender_user_profile, sending_client=get_client("test")) content = "@**King Hamlet**" self.assertEqual(render_markdown(msg, content), '

@King Hamlet

') self.assertEqual(msg.mentions_user_ids, set([user_profile.id])) def test_mention_shortname(self): # type: () -> None sender_user_profile = get_user_profile_by_email("othello@zulip.com") user_profile = get_user_profile_by_email("hamlet@zulip.com") msg = Message(sender=sender_user_profile, sending_client=get_client("test")) content = "@**hamlet**" self.assertEqual(render_markdown(msg, content), '

@King Hamlet

') self.assertEqual(msg.mentions_user_ids, set([user_profile.id])) def test_mention_multiple(self): # type: () -> None sender_user_profile = get_user_profile_by_email("othello@zulip.com") hamlet = get_user_profile_by_email("hamlet@zulip.com") cordelia = get_user_profile_by_email("cordelia@zulip.com") msg = Message(sender=sender_user_profile, sending_client=get_client("test")) content = "@**King Hamlet** and @**cordelia**, check this out" self.assertEqual(render_markdown(msg, content), '

' '@King Hamlet and ' '@Cordelia Lear, ' 'check this out

') self.assertEqual(msg.mentions_user_ids, set([hamlet.id, cordelia.id])) def test_mention_invalid(self): # type: () -> None sender_user_profile = get_user_profile_by_email("othello@zulip.com") msg = Message(sender=sender_user_profile, sending_client=get_client("test")) content = "Hey @**Nonexistent User**" self.assertEqual(render_markdown(msg, content), '

Hey @Nonexistent User

') self.assertEqual(msg.mentions_user_ids, set()) def test_stream_subscribe_button_simple(self): # type: () -> None msg = '!_stream_subscribe_button(simple)' converted = bugdown_convert(msg) self.assertEqual( converted, '

' '' '' '' '' '

' ) def test_stream_subscribe_button_in_name(self): # type: () -> None msg = '!_stream_subscribe_button(simple (not\\))' converted = bugdown_convert(msg) self.assertEqual( converted, '

' '' '' '' '' '

' ) def test_stream_subscribe_button_after_name(self): # type: () -> None msg = '!_stream_subscribe_button(simple) (not)' converted = bugdown_convert(msg) self.assertEqual( converted, '

' '' '' '' '' ' (not)

' ) def test_stream_subscribe_button_slash(self): # type: () -> None msg = '!_stream_subscribe_button(simple\\\\)' converted = bugdown_convert(msg) self.assertEqual( converted, '

' '' '' '' '' '

' ) def test_in_app_modal_link(self): # type: () -> None msg = '!modal_link(#settings, Settings page)' converted = bugdown_convert(msg) self.assertEqual( converted, '

' 'Settings page' '

' ) def test_image_preview_title(self): # type: () -> None msg = '[My favorite image](https://example.com/testimage.png)' converted = bugdown_convert(msg) self.assertEqual( converted, '

' 'My favorite image' '

\n' '
' '' '' '' '
' ) def test_mit_rendering(self): # type: () -> None """Test the markdown configs for the MIT Zephyr mirroring system; verifies almost all inline patterns are disabled, but inline_interesting_links is still enabled""" msg = "**test**" converted = bugdown.convert(msg, "zephyr_mirror") self.assertEqual( converted, "

**test**

", ) msg = "* test" converted = bugdown.convert(msg, "zephyr_mirror") self.assertEqual( converted, "

* test

", ) msg = "https://lists.debian.org/debian-ctte/2014/02/msg00173.html" converted = bugdown.convert(msg, "zephyr_mirror") self.assertEqual( converted, '

https://lists.debian.org/debian-ctte/2014/02/msg00173.html

', ) class BugdownApiTests(ZulipTestCase): def test_render_message_api(self): # type: () -> None content = 'That is a **bold** statement' result = self.client_get( '/api/v1/messages/render', dict(content=content), **self.api_auth('othello@zulip.com') ) self.assert_json_success(result) data = ujson.loads(result.content) self.assertEqual(data['rendered'], u'

That is a bold statement

') class BugdownErrorTests(ZulipTestCase): def test_bugdown_error_handling(self): # type: () -> None with self.simulated_markdown_failure(): with self.assertRaises(bugdown.BugdownRenderingException): bugdown.convert('', 'zulip.com') def test_send_message_errors(self): # type: () -> None message = 'whatever' with self.simulated_markdown_failure(): # We don't use assertRaisesRegexp because it seems to not # handle i18n properly here on some systems. with self.assertRaises(JsonableError): self.send_message("othello@zulip.com", "Denmark", Recipient.STREAM, message)