2024-06-21 21:02:36 +02:00
|
|
|
import re
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
import pyvips
|
|
|
|
|
|
|
|
from zerver.actions.message_delete import do_delete_messages
|
2024-07-31 18:07:10 +02:00
|
|
|
from zerver.actions.message_send import check_message, do_send_messages
|
|
|
|
from zerver.lib.addressee import Addressee
|
2024-06-21 21:02:36 +02:00
|
|
|
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 get_test_image_file, read_test_image_file
|
|
|
|
from zerver.lib.thumbnail import ThumbnailFormat
|
|
|
|
from zerver.lib.upload import upload_message_attachment
|
2024-07-31 18:07:10 +02:00
|
|
|
from zerver.models import (
|
|
|
|
ArchivedAttachment,
|
|
|
|
ArchivedMessage,
|
|
|
|
Attachment,
|
|
|
|
Client,
|
|
|
|
ImageAttachment,
|
|
|
|
Message,
|
|
|
|
)
|
2024-06-21 21:02:36 +02:00
|
|
|
from zerver.models.clients import get_client
|
|
|
|
from zerver.worker.thumbnail import ensure_thumbnails
|
|
|
|
|
|
|
|
|
|
|
|
class MarkdownThumbnailTest(ZulipTestCase):
|
|
|
|
def upload_image(self, image_name: str) -> str:
|
|
|
|
self.login("othello")
|
|
|
|
with get_test_image_file(image_name) as image_file:
|
|
|
|
response = self.assert_json_success(
|
|
|
|
self.client_post("/json/user_uploads", {"file": image_file})
|
|
|
|
)
|
|
|
|
return re.sub(r"/user_uploads/", "", response["url"])
|
|
|
|
|
|
|
|
def upload_and_thumbnail_image(self, image_name: str) -> str:
|
|
|
|
with self.captureOnCommitCallbacks(execute=True):
|
|
|
|
# Running captureOnCommitCallbacks includes inserting into
|
|
|
|
# the Rabbitmq queue, which in testing means we
|
|
|
|
# immediately run the worker for it, producing the thumbnails.
|
|
|
|
return self.upload_image(image_name)
|
|
|
|
|
|
|
|
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,
|
|
|
|
(
|
|
|
|
"<p>Test 1<br>\n"
|
|
|
|
f'<a href="/user_uploads/{path_ids[0]}">{image_names[0]}</a> </p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_ids[0]}" title="{image_names[0]}">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_ids[0]}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
"<p>Next image<br>\n"
|
|
|
|
f'<a href="/user_uploads/{path_ids[1]}">{image_names[1]}</a> </p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_ids[1]}" title="{image_names[1]}">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_ids[1]}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
"<p>Another screenshot<br>\n"
|
|
|
|
f'<a href="/user_uploads/{path_ids[2]}">{image_names[2]}</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_ids[2]}" title="{image_names[2]}">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_ids[2]}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_thumbnail_code_block(self) -> None:
|
|
|
|
url = "http://example.com/image.png"
|
|
|
|
path_id = self.upload_and_thumbnail_image("img.png")
|
|
|
|
# We have a path_id of an image in the message content, so we
|
|
|
|
# will prefetch the thumbnail metadata -- but not insert it.
|
|
|
|
|
|
|
|
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, f"{url}\n```\n/user_uploads/{path_id}\n```")
|
|
|
|
self.assertEqual(
|
|
|
|
converted.rendered_content,
|
|
|
|
(
|
|
|
|
f'<div class="message_inline_image"><a href="{url}"><img src="{get_camo_url(url)}"></a></div>'
|
|
|
|
f'<div class="codehilite"><pre><span></span><code>/user_uploads/{path_id}\n'
|
|
|
|
"</code></pre></div>"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_thumbnail_after_send(self) -> None:
|
|
|
|
with self.captureOnCommitCallbacks(execute=True):
|
|
|
|
path_id = self.upload_image("img.png")
|
|
|
|
content = f"[image](/user_uploads/{path_id})"
|
|
|
|
expected = (
|
|
|
|
f'<p><a href="/user_uploads/{path_id}">image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
2024-09-04 18:01:56 +02:00
|
|
|
'<img class="image-loading-placeholder" data-original-dimensions="128x128" src="/static/images/loading/loader-black.svg"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
message_id = self.send_message_content(content)
|
|
|
|
self.assert_message_content_is(message_id, expected)
|
|
|
|
|
|
|
|
# Exit the block and run thumbnailing
|
|
|
|
expected = (
|
|
|
|
f'<p><a href="/user_uploads/{path_id}">image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_id}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
self.assert_message_content_is(message_id, expected)
|
|
|
|
|
|
|
|
def test_thumbnail_escaping(self) -> None:
|
|
|
|
self.login("othello")
|
|
|
|
with self.captureOnCommitCallbacks(execute=True):
|
|
|
|
url = upload_message_attachment(
|
|
|
|
"I am 95% ± 5% certain!",
|
|
|
|
"image/png",
|
|
|
|
read_test_image_file("img.png"),
|
|
|
|
self.example_user("othello"),
|
2024-08-30 04:13:01 +02:00
|
|
|
)[0]
|
2024-06-21 21:02:36 +02:00
|
|
|
path_id = re.sub(r"/user_uploads/", "", url)
|
|
|
|
self.assertTrue(ImageAttachment.objects.filter(path_id=path_id).exists())
|
|
|
|
message_id = self.send_message_content(f"[I am 95% ± 5% certain!](/user_uploads/{path_id})")
|
|
|
|
expected = (
|
2024-09-05 22:21:51 +02:00
|
|
|
f'<p><a href="/user_uploads/{path_id}">I am 95% ± 5% certain!</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="I am 95% ± 5% certain!">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_id}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
self.assert_message_content_is(message_id, expected)
|
|
|
|
|
|
|
|
def test_thumbnail_repeated(self) -> None:
|
|
|
|
# We currently have no way to generate a thumbnailing event
|
|
|
|
# for the worker except during upload, meaning that we will
|
|
|
|
# never repeat a ImageAttachment thumbnailing. However, the
|
|
|
|
# code supports it, so test it.
|
|
|
|
|
|
|
|
# Thumbnail with one set of sizes
|
|
|
|
with self.thumbnail_formats(
|
|
|
|
ThumbnailFormat("webp", 100, 75, animated=True),
|
|
|
|
ThumbnailFormat("webp", 100, 75, animated=False),
|
|
|
|
):
|
2024-07-22 23:16:03 +02:00
|
|
|
path_id = self.upload_and_thumbnail_image("animated_unequal_img.gif")
|
|
|
|
content = f"[animated_unequal_img.gif](/user_uploads/{path_id})"
|
2024-06-21 21:02:36 +02:00
|
|
|
expected = (
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<p><a href="/user_uploads/{path_id}">animated_unequal_img.gif</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="animated_unequal_img.gif">'
|
|
|
|
'<img data-animated="true" data-original-dimensions="128x56"'
|
|
|
|
f' src="/user_uploads/thumbnail/{path_id}/100x75-anim.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
message_id = self.send_message_content(content, do_thumbnail=True)
|
|
|
|
self.assert_message_content_is(message_id, expected)
|
|
|
|
self.assert_length(ImageAttachment.objects.get(path_id=path_id).thumbnail_metadata, 2)
|
|
|
|
|
|
|
|
# Re-thumbnail with a non-overlapping set of sizes
|
|
|
|
with self.thumbnail_formats(ThumbnailFormat("jpg", 100, 75, animated=False)):
|
|
|
|
ensure_thumbnails(ImageAttachment.objects.get(path_id=path_id))
|
|
|
|
|
|
|
|
# We generate a new size but leave the old ones
|
|
|
|
self.assert_length(ImageAttachment.objects.get(path_id=path_id).thumbnail_metadata, 3)
|
|
|
|
|
|
|
|
# And the contents are not updated to the new size
|
|
|
|
self.assert_message_content_is(message_id, expected)
|
|
|
|
|
|
|
|
def test_thumbnail_sequential_edits(self) -> None:
|
|
|
|
first_path_id = self.upload_image("img.png")
|
|
|
|
second_path_id = self.upload_image("img.jpg")
|
|
|
|
|
|
|
|
message_id = self.send_message_content(
|
|
|
|
f"[first image](/user_uploads/{first_path_id})\n[second image](/user_uploads/{second_path_id})",
|
|
|
|
do_thumbnail=False,
|
|
|
|
)
|
|
|
|
self.assert_message_content_is(
|
|
|
|
message_id,
|
|
|
|
(
|
|
|
|
f'<p><a href="/user_uploads/{first_path_id}">first image</a><br>\n'
|
|
|
|
f'<a href="/user_uploads/{second_path_id}">second image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{first_path_id}" title="first image">'
|
2024-09-04 18:01:56 +02:00
|
|
|
'<img class="image-loading-placeholder" data-original-dimensions="128x128" src="/static/images/loading/loader-black.svg"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{second_path_id}" title="second image">'
|
2024-09-04 18:01:56 +02:00
|
|
|
'<img class="image-loading-placeholder" data-original-dimensions="128x128" src="/static/images/loading/loader-black.svg"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Complete thumbnailing the second image first -- replacing only that spinner
|
|
|
|
ensure_thumbnails(ImageAttachment.objects.get(path_id=second_path_id))
|
|
|
|
self.assert_message_content_is(
|
|
|
|
message_id,
|
|
|
|
(
|
|
|
|
f'<p><a href="/user_uploads/{first_path_id}">first image</a><br>\n'
|
|
|
|
f'<a href="/user_uploads/{second_path_id}">second image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{first_path_id}" title="first image">'
|
2024-09-04 18:01:56 +02:00
|
|
|
'<img class="image-loading-placeholder" data-original-dimensions="128x128" src="/static/images/loading/loader-black.svg"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{second_path_id}" title="second image">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{second_path_id}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Finish the other thumbnail
|
|
|
|
ensure_thumbnails(ImageAttachment.objects.get(path_id=first_path_id))
|
|
|
|
self.assert_message_content_is(
|
|
|
|
message_id,
|
|
|
|
(
|
|
|
|
f'<p><a href="/user_uploads/{first_path_id}">first image</a><br>\n'
|
|
|
|
f'<a href="/user_uploads/{second_path_id}">second image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{first_path_id}" title="first image">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{first_path_id}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{second_path_id}" title="second image">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{second_path_id}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_thumbnail_of_deleted(self) -> None:
|
|
|
|
sender_user_profile = self.example_user("othello")
|
|
|
|
path_id = self.upload_image("img.png")
|
|
|
|
message_id = self.send_message_content(f"[image](/user_uploads/{path_id})")
|
|
|
|
|
|
|
|
# Delete the message
|
|
|
|
do_delete_messages(
|
|
|
|
sender_user_profile.realm, [Message.objects.get(id=message_id)], acting_user=None
|
|
|
|
)
|
|
|
|
|
|
|
|
# There is still an ImageAttachment row
|
|
|
|
self.assertFalse(Attachment.objects.filter(path_id=path_id).exists())
|
|
|
|
self.assertTrue(ArchivedAttachment.objects.filter(path_id=path_id).exists())
|
|
|
|
self.assertTrue(ImageAttachment.objects.filter(path_id=path_id).exists())
|
|
|
|
|
|
|
|
# Completing rendering after it is deleted should work, and
|
|
|
|
# update the rendered content in the archived message
|
|
|
|
ensure_thumbnails(ImageAttachment.objects.get(path_id=path_id))
|
|
|
|
expected = (
|
|
|
|
f'<p><a href="/user_uploads/{path_id}">image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_id}/840x560.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
ArchivedMessage.objects.get(id=message_id).rendered_content,
|
|
|
|
expected,
|
|
|
|
)
|
|
|
|
# See test_delete_unclaimed_attachments for tests of the
|
|
|
|
# archiving process itself, and how it interacts with
|
|
|
|
# thumbnails.
|
|
|
|
|
|
|
|
def test_thumbnail_bad_image(self) -> None:
|
|
|
|
"""Test what happens if the file looks fine, but resizing later fails"""
|
|
|
|
path_id = self.upload_image("img.png")
|
|
|
|
message_id = self.send_message_content(f"[image](/user_uploads/{path_id})")
|
|
|
|
self.assert_length(ImageAttachment.objects.get(path_id=path_id).thumbnail_metadata, 0)
|
|
|
|
|
|
|
|
# If the image is found to be bad, we remove all trace of the preview
|
|
|
|
with (
|
|
|
|
patch.object(
|
|
|
|
pyvips.Image, "thumbnail_buffer", side_effect=pyvips.Error("some bad error")
|
|
|
|
) as thumb_mock,
|
|
|
|
self.assertLogs("zerver.worker.thumbnail", "ERROR") as thumbnail_logs,
|
|
|
|
):
|
|
|
|
ensure_thumbnails(ImageAttachment.objects.get(path_id=path_id))
|
|
|
|
thumb_mock.assert_called_once()
|
|
|
|
self.assert_length(thumbnail_logs.output, 1)
|
|
|
|
self.assertTrue(
|
|
|
|
thumbnail_logs.output[0].startswith("ERROR:zerver.worker.thumbnail:some bad error")
|
|
|
|
)
|
|
|
|
self.assertFalse(ImageAttachment.objects.filter(path_id=path_id).exists())
|
|
|
|
self.assert_message_content_is(
|
|
|
|
message_id, f'<p><a href="/user_uploads/{path_id}">image</a></p>'
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_thumbnail_multiple_messages(self) -> None:
|
|
|
|
sender_user_profile = self.example_user("othello")
|
|
|
|
path_id = self.upload_image("img.png")
|
|
|
|
channel_message_id = self.send_message_content(f"A public [image](/user_uploads/{path_id})")
|
|
|
|
private_message_id = self.send_personal_message(
|
|
|
|
from_user=sender_user_profile,
|
|
|
|
to_user=self.example_user("hamlet"),
|
|
|
|
content=f"This [image](/user_uploads/{path_id}) is private",
|
thumbnail: Enqueue thumbnails when we render a spinner.
Thumbnails are usually enqueued in the worker when the image is
uploaded. However, for images which were uploaded before the
existence of the thumbnailing worker, and whose metadata was
backfilled (see previous commit) this leaves a permanent spinner,
since nothing triggers the thumbnail worker for them.
Enqueue a thumbnail worker for every spinner which we render into
Markdown. This ensures that _something_ is attempting to resolve the
spinner which the user sees. In the case of freshly-uploaded images
which are still in the queue, this results in a duplicate entry in the
thumbnailing queue -- this is harmless, since the worker determines
that all of the thumbnails we need have already been generated, and it
does no further work. However, in the case of historical uploads, it
properly kicks off the thumbnailing process and results in a
subsequent message update to include the freshly-generated thumbnail.
While specifically useful for backfilled uploads, this is also
generally a good safety step for a good user experience, as it also
prevents dropped events in the queue from unknown causes from leaving
perpetual spinners in the message feed.
Because `get_user_upload_previews` is potentially called twice for
every message with spinners (see 6f20c15ae9e5), we add an additional
flag to `get_user_upload_previews` to suppress a _second_ event from
being enqueued for every spinner generated.
2024-08-28 16:45:35 +02:00
|
|
|
skip_capture_on_commit_callbacks=True,
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
placeholder = (
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
2024-09-04 18:01:56 +02:00
|
|
|
'<img class="image-loading-placeholder" data-original-dimensions="128x128" src="/static/images/loading/loader-black.svg"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
self.assert_message_content_is(
|
|
|
|
channel_message_id,
|
|
|
|
f'<p>A public <a href="/user_uploads/{path_id}">image</a></p>\n{placeholder}',
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_message_content_is(
|
|
|
|
private_message_id,
|
|
|
|
f'<p>This <a href="/user_uploads/{path_id}">image</a> is private</p>\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'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
2024-07-22 23:16:03 +02:00
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_id}/100x75.webp"></a></div>'
|
2024-06-21 21:02:36 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_message_content_is(
|
|
|
|
channel_message_id,
|
|
|
|
f'<p>A public <a href="/user_uploads/{path_id}">image</a></p>\n{rendered_thumb}',
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_message_content_is(
|
|
|
|
private_message_id,
|
|
|
|
f'<p>This <a href="/user_uploads/{path_id}">image</a> is private</p>\n{rendered_thumb}',
|
|
|
|
)
|
2024-07-31 18:07:10 +02:00
|
|
|
|
|
|
|
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'<p><a href="/user_uploads/{path_id}">image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
2024-09-04 18:01:56 +02:00
|
|
|
'<img class="image-loading-placeholder" data-original-dimensions="128x128" src="/static/images/loading/loader-black.svg"></a></div>'
|
2024-07-31 18:07:10 +02:00
|
|
|
)
|
|
|
|
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'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_id}/840x560.webp"></a></div>'
|
|
|
|
)
|
|
|
|
self.assert_message_content_is(
|
|
|
|
message_id, f'<p><a href="/user_uploads/{path_id}">image</a></p>\n{rendered_thumb}'
|
|
|
|
)
|
thumbnail: Enqueue thumbnails when we render a spinner.
Thumbnails are usually enqueued in the worker when the image is
uploaded. However, for images which were uploaded before the
existence of the thumbnailing worker, and whose metadata was
backfilled (see previous commit) this leaves a permanent spinner,
since nothing triggers the thumbnail worker for them.
Enqueue a thumbnail worker for every spinner which we render into
Markdown. This ensures that _something_ is attempting to resolve the
spinner which the user sees. In the case of freshly-uploaded images
which are still in the queue, this results in a duplicate entry in the
thumbnailing queue -- this is harmless, since the worker determines
that all of the thumbnails we need have already been generated, and it
does no further work. However, in the case of historical uploads, it
properly kicks off the thumbnailing process and results in a
subsequent message update to include the freshly-generated thumbnail.
While specifically useful for backfilled uploads, this is also
generally a good safety step for a good user experience, as it also
prevents dropped events in the queue from unknown causes from leaving
perpetual spinners in the message feed.
Because `get_user_upload_previews` is potentially called twice for
every message with spinners (see 6f20c15ae9e5), we add an additional
flag to `get_user_upload_previews` to suppress a _second_ event from
being enqueued for every spinner generated.
2024-08-28 16:45:35 +02:00
|
|
|
|
|
|
|
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'<p><a href="/user_uploads/{path_id}">image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
2024-09-04 18:01:56 +02:00
|
|
|
'<img class="image-loading-placeholder" data-original-dimensions="128x128" src="/static/images/loading/loader-black.svg"></a></div>'
|
thumbnail: Enqueue thumbnails when we render a spinner.
Thumbnails are usually enqueued in the worker when the image is
uploaded. However, for images which were uploaded before the
existence of the thumbnailing worker, and whose metadata was
backfilled (see previous commit) this leaves a permanent spinner,
since nothing triggers the thumbnail worker for them.
Enqueue a thumbnail worker for every spinner which we render into
Markdown. This ensures that _something_ is attempting to resolve the
spinner which the user sees. In the case of freshly-uploaded images
which are still in the queue, this results in a duplicate entry in the
thumbnailing queue -- this is harmless, since the worker determines
that all of the thumbnails we need have already been generated, and it
does no further work. However, in the case of historical uploads, it
properly kicks off the thumbnailing process and results in a
subsequent message update to include the freshly-generated thumbnail.
While specifically useful for backfilled uploads, this is also
generally a good safety step for a good user experience, as it also
prevents dropped events in the queue from unknown causes from leaving
perpetual spinners in the message feed.
Because `get_user_upload_previews` is potentially called twice for
every message with spinners (see 6f20c15ae9e5), we add an additional
flag to `get_user_upload_previews` to suppress a _second_ event from
being enqueued for every spinner generated.
2024-08-28 16:45:35 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
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'<p><a href="/user_uploads/{path_id}">image</a></p>\n'
|
|
|
|
f'<div class="message_inline_image"><a href="/user_uploads/{path_id}" title="image">'
|
|
|
|
f'<img data-original-dimensions="128x128" src="/user_uploads/thumbnail/{path_id}/840x560.webp"></a></div>'
|
|
|
|
)
|
|
|
|
self.assert_message_content_is(message_id, expected)
|