import re from unittest.mock import patch import pyvips from typing_extensions import override from zerver.actions.message_delete import do_delete_messages from zerver.actions.message_edit import re_thumbnail from zerver.actions.message_send import check_message, do_send_messages from zerver.lib.addressee import Addressee from zerver.lib.camo import get_camo_url from zerver.lib.markdown import render_message_markdown from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import read_test_image_file from zerver.lib.thumbnail import ThumbnailFormat from zerver.lib.upload import upload_message_attachment from zerver.models import ( ArchivedAttachment, ArchivedMessage, Attachment, Client, ImageAttachment, Message, ) from zerver.models.clients import get_client from zerver.models.realms import get_realm from zerver.worker.thumbnail import ensure_thumbnails class MarkdownThumbnailTest(ZulipTestCase): @override def setUp(self) -> None: self.login("othello") super().setUp() def assert_message_content_is( self, message_id: int, rendered_content: str, user_name: str = "othello" ) -> None: sender_user_profile = self.example_user(user_name) result = self.assert_json_success( self.api_get(sender_user_profile, f"/api/v1/messages/{message_id}") ) self.assertEqual(result["message"]["content"], rendered_content) def send_message_content( self, content: str, do_thumbnail: bool = False, user_name: str = "othello" ) -> int: sender_user_profile = self.example_user(user_name) return self.send_stream_message( sender=sender_user_profile, stream_name="Verona", content=content, skip_capture_on_commit_callbacks=not do_thumbnail, ) def test_uploads_preview_order(self) -> None: image_names = ["img.jpg", "img.png", "img.gif"] path_ids = [self.upload_and_thumbnail_image(image_name) for image_name in image_names] content = ( f"Test 1\n[{image_names[0]}](/user_uploads/{path_ids[0]}) \n\n" f"Next image\n[{image_names[1]}](/user_uploads/{path_ids[1]}) \n\n" f"Another screenshot\n[{image_names[2]}](/user_uploads/{path_ids[2]})" ) sender_user_profile = self.example_user("othello") msg = Message( sender=sender_user_profile, sending_client=get_client("test"), realm=sender_user_profile.realm, ) converted = render_message_markdown(msg, content) self.assertEqual( converted.rendered_content, ( "
Test 1
\n"
f'{image_names[0]}
Next image
\n"
f'{image_names[1]}
Another screenshot
\n"
f'{image_names[2]}
/user_uploads/{path_id}\n'
"first image
\n'
f'second image
first image
\n'
f'second image
first image
\n'
f'second image
A public image
\n{placeholder}', ) self.assert_message_content_is( private_message_id, f'This image is private
\n{placeholder}', ) with ( patch.object( pyvips.Image, "thumbnail_buffer", wraps=pyvips.Image.thumbnail_buffer ) as thumb_mock, self.thumbnail_formats( ThumbnailFormat("webp", 100, 75, animated=False), ThumbnailFormat("webp", 200, 150, animated=False), ), ): ensure_thumbnails(ImageAttachment.objects.get(path_id=path_id)) # Called once per format self.assertEqual(thumb_mock.call_count, 2) rendered_thumb = ( f'' ) self.assert_message_content_is( channel_message_id, f'A public image
\n{rendered_thumb}', ) self.assert_message_content_is( private_message_id, f'This image is private
\n{rendered_thumb}', ) def test_thumbnail_race(self) -> None: """Test what happens when thumbnailing happens between rendering and sending""" path_id = self.upload_image("img.png") self.assert_length(ImageAttachment.objects.get(path_id=path_id).thumbnail_metadata, 0) # Render, but do not send, the message referencing the image. # This will render as a spinner, since the thumbnail has not # been generated yet. send_request = check_message( self.example_user("othello"), Client.objects.get_or_create(name="test suite")[0], Addressee.for_stream_name("Verona", "test"), f"[image](/user_uploads/{path_id})", ) expected = ( f'\n' f'' ) self.assertEqual(send_request.message.rendered_content, expected) # Thumbnail the image. The message does not exist yet, so # nothing is re-written. ensure_thumbnails(ImageAttachment.objects.get(path_id=path_id)) # Send the message; this should re-check the ImageAttachment # data, find the thumbnails, and update the rendered_content # to no longer contain a spinner. message_id = do_send_messages([send_request])[0].message_id rendered_thumb = ( f'' ) self.assert_message_content_is( message_id, f'\n{rendered_thumb}' ) def test_thumbnail_historical_image(self) -> None: # Note that this is outside the captureOnCommitCallbacks, so # we don't actually run thumbnailing for it. This results in # a ImageAttachment row but no thumbnails, which matches the # state of backfilled previously-uploaded images. path_id = self.upload_image("img.png") with self.captureOnCommitCallbacks(execute=True): message_id = self.send_message_content(f"An [image](/user_uploads/{path_id})") content = f"[image](/user_uploads/{path_id})" expected = ( f'\n' f'' ) message_id = self.send_message_content(content) self.assert_message_content_is(message_id, expected) # Exiting the block should have run the thumbnailing that was # enqueued when rendering the message. expected = ( f'\n' f'' ) self.assert_message_content_is(message_id, expected) def test_thumbnail_transcode(self) -> None: path_id = self.upload_image("img.tif") message_id = self.send_message_content( f"An [image](/user_uploads/{path_id})", do_thumbnail=True ) expected = ( f'An image
\n' f'' ) self.assert_message_content_is(message_id, expected) def test_re_thumbnail_stuck(self) -> None: # For the case when we generated a thumbnail, but there was a # race condition in updating the message, which left it with a # permanent spinner. No new thumbnailing is enqueued. with self.captureOnCommitCallbacks(execute=True): path_id = self.upload_image("img.png") content = f"[image](/user_uploads/{path_id})" expected = ( f'\n' f'' ) message_id = self.send_message_content(content) self.assert_message_content_is(message_id, expected) # Exit the block and run thumbnailing # Set the rendered content back to the spinner version, so # simulate one of the race condition bugs we have had in the # past self.assertEqual( Message.objects.filter( realm_id=get_realm("zulip").id, has_image=True, rendered_content__contains='class="image-loading-placeholder"', ).count(), 0, ) message = Message.objects.get(id=message_id) message.rendered_content = expected message.save() self.assertEqual( Message.objects.filter( realm_id=get_realm("zulip").id, has_image=True, rendered_content__contains='class="image-loading-placeholder"', ).count(), 1, ) # Trigger the re-thumbnailing codepath re_thumbnail(Message, message_id, enqueue=False) expected = ( f'\n' f'' ) self.assert_message_content_is(message_id, expected) def test_re_thumbnail_historical(self) -> None: # Note that this is all outside the captureOnCommitCallbacks, # so we don't actually run thumbnailing for it. This results # in a ImageAttachment row but no thumbnails, which matches # the state of backfilled previously-uploaded images. path_id = self.upload_image("img.png") # We don't execute callbacks at all, to drop thumbnailing # which would have been done content = f"[image](/user_uploads/{path_id})" expected = ( f'\n' f'' ) message_id = self.send_message_content(content) self.assert_message_content_is(message_id, expected) # Force-update to the version without thumbnails self.assertEqual( Message.objects.filter( realm_id=get_realm("zulip").id, has_image=True, rendered_content__contains='