Files
old-saburly-wagtail-web/env/lib/python3.10/site-packages/wagtail/documents/tests/test_views.py
2024-08-27 20:33:44 +02:00

415 lines
14 KiB
Python

import os.path
import unittest
import urllib
from unittest import mock
from django.conf import settings
from django.core.files.base import ContentFile
from django.test import TestCase, override_settings
from django.urls import reverse
from wagtail.documents import models
@override_settings(WAGTAILDOCS_SERVE_METHOD=None)
class TestServeView(TestCase):
def setUp(self):
self.document = models.Document(title="Test document", file_hash="123456")
self.document.file.save(
"serve_view.doc", ContentFile(b"A boring example document")
)
self.pdf_document = models.Document(title="Test document", file_hash="123456")
self.pdf_document.file.save(
"serve_view.pdf", ContentFile(b"A boring example document")
)
def tearDown(self):
if hasattr(self, "response"):
# Make sure the response is fully read before deleting the document so
# that the file is closed by the view.
# This is required on Windows as the below line that deletes the file
# will crash if the file is still open.
b"".join(self.response.streaming_content)
# delete the FieldFile directly because the TestCase does not commit
# transactions to trigger transaction.on_commit() in the signal handler
self.document.file.delete()
self.pdf_document.file.delete()
def get(self, document=None):
document = document or self.document
self.response = self.client.get(
reverse("wagtaildocs_serve", args=(document.id, document.filename))
)
return self.response
def test_response_code(self):
self.assertEqual(self.get().status_code, 200)
def test_content_disposition_header(self):
self.assertEqual(
self.get(self.document)["Content-Disposition"],
f'attachment; filename="{self.document.filename}"',
)
def test_inline_content_disposition_header(self):
self.assertEqual(
self.get(self.pdf_document)["Content-Disposition"],
f'inline; filename="{self.pdf_document.filename}"',
)
@mock.patch("wagtail.documents.views.serve.hooks")
@mock.patch("wagtail.documents.views.serve.get_object_or_404")
def test_non_local_filesystem_content_disposition_header(
self, mock_get_object_or_404, mock_hooks
):
"""
Tests the 'Content-Disposition' header in a response when using a
storage backend that doesn't expose filesystem paths.
"""
# Create a mock document with no local file to hit the correct code path
mock_doc = mock.Mock()
mock_doc.filename = self.document.filename
mock_doc.content_type = self.document.content_type
mock_doc.content_disposition = self.document.content_disposition
mock_doc.file = ContentFile(b"file-like object" * 10)
mock_doc.file.path = None
mock_doc.file.url = None
mock_get_object_or_404.return_value = mock_doc
# Bypass 'before_serve_document' hooks
mock_hooks.get_hooks.return_value = []
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertEqual(
response["Content-Disposition"],
"attachment; filename={0}; filename*=UTF-8''{0}".format(
urllib.parse.quote(self.document.filename)
),
)
@mock.patch("wagtail.documents.views.serve.hooks")
@mock.patch("wagtail.documents.views.serve.get_object_or_404")
def test_non_local_filesystem_inline_content_disposition_header(
self, mock_get_object_or_404, mock_hooks
):
"""
Tests the 'Content-Disposition' header in a response when using a
storage backend that doesn't expose filesystem paths.
"""
# Create a mock document with no local file to hit the correct code path
mock_doc = mock.Mock()
mock_doc.filename = self.pdf_document.filename
mock_doc.content_type = self.pdf_document.content_type
mock_doc.content_disposition = self.pdf_document.content_disposition
mock_doc.file = ContentFile(b"file-like object" * 10)
mock_doc.file.path = None
mock_doc.file.url = None
mock_get_object_or_404.return_value = mock_doc
# Bypass 'before_serve_document' hooks
mock_hooks.get_hooks.return_value = []
response = self.get(self.pdf_document)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Disposition"], "inline")
def test_content_length_header(self):
self.assertEqual(self.get()["Content-Length"], "25")
def test_content_type_header(self):
self.assertEqual(self.get()["Content-Type"], "application/msword")
def test_is_streaming_response(self):
self.assertTrue(self.get().streaming)
def test_content(self):
self.assertEqual(
b"".join(self.get().streaming_content), b"A boring example document"
)
def test_document_served_fired(self):
mock_handler = mock.MagicMock()
models.document_served.connect(mock_handler)
try:
self.get()
self.assertEqual(mock_handler.call_count, 1)
self.assertEqual(mock_handler.mock_calls[0][2]["sender"], models.Document)
self.assertEqual(mock_handler.mock_calls[0][2]["instance"], self.document)
finally:
models.document_served.disconnect(mock_handler)
def test_with_nonexistent_document(self):
response = self.client.get(
reverse(
"wagtaildocs_serve",
args=(
1000,
"blahblahblah",
),
)
)
self.assertEqual(response.status_code, 404)
def test_with_incorrect_filename(self):
response = self.client.get(
reverse("wagtaildocs_serve", args=(self.document.id, "incorrectfilename"))
)
self.assertEqual(response.status_code, 404)
def test_has_etag_header(self):
self.assertEqual(self.get()["ETag"], '"123456"')
def clear_sendfile_cache(self):
from wagtail.utils.sendfile import _get_sendfile
_get_sendfile.clear()
@override_settings(WAGTAILDOCS_SERVE_METHOD="redirect")
class TestServeViewWithRedirect(TestCase):
def setUp(self):
self.document = models.Document(title="Test document")
self.document.file.save(
"serve_view_with_redirect.doc",
ContentFile("A boring example document"),
)
self.serve_view_url = reverse(
"wagtaildocs_serve", args=(self.document.id, self.document.filename)
)
def tearDown(self):
self.document.delete()
def get(self):
return self.client.get(
reverse(
"wagtaildocs_serve", args=(self.document.id, self.document.filename)
)
)
def test_document_url_should_point_to_serve_view(self):
self.assertEqual(self.document.url, self.serve_view_url)
def test_redirect(self):
response = self.get()
self.assertRedirects(
response, self.document.file.url, fetch_redirect_response=False
)
@override_settings(WAGTAILDOCS_SERVE_METHOD="direct")
class TestDirectDocumentUrls(TestCase):
def setUp(self):
self.document = models.Document(title="Test document")
self.document.file.save(
"direct_document_urls.doc",
ContentFile("A boring example document"),
)
def tearDown(self):
self.document.delete()
def get(self):
return self.client.get(
reverse(
"wagtaildocs_serve", args=(self.document.id, self.document.filename)
)
)
def test_url_should_point_directly_to_file_storage_url(self):
self.assertEqual(self.document.url, self.document.file.url)
def test_redirect(self):
# The serve view will not normally be linked to in 'direct' mode, but we should ensure it
# still works by redirecting
response = self.get()
self.assertRedirects(
response, self.document.file.url, fetch_redirect_response=False
)
@override_settings(
WAGTAILDOCS_SERVE_METHOD=None,
STORAGES={
**settings.STORAGES,
"default": {
"BACKEND": "wagtail.test.dummy_external_storage.DummyExternalStorage"
},
},
)
class TestServeWithExternalStorage(TestCase):
"""
Test the behaviour of the default serve method when used with a remote storage backend
(i.e. one that throws NotImplementedError for the path() method).
"""
def setUp(self):
self.document = models.Document(title="Test document")
self.document.file.save(
"serve_with_external_storage.doc",
ContentFile("A boring example document"),
)
self.serve_view_url = reverse(
"wagtaildocs_serve", args=(self.document.id, self.document.filename)
)
def tearDown(self):
self.document.delete()
def test_document_url_should_point_to_serve_view(self):
self.assertEqual(self.document.url, self.serve_view_url)
def test_redirect(self):
# serve view should redirect to the remote URL
response = self.client.get(self.serve_view_url)
self.assertRedirects(
response, self.document.file.url, fetch_redirect_response=False
)
@override_settings(WAGTAILDOCS_SERVE_METHOD=None)
class TestServeViewWithSendfile(TestCase):
def setUp(self):
# Import using a try-catch block to prevent crashes if the
# django-sendfile module is not installed
try:
import sendfile # noqa: F401
except ImportError:
raise unittest.SkipTest("django-sendfile not installed")
self.document = models.Document(title="Test document")
self.document.file.save(
"serve_view_with_sendfile.doc",
ContentFile("A boring example document"),
)
def tearDown(self):
# delete the FieldFile directly because the TestCase does not commit
# transactions to trigger transaction.on_commit() in the signal handler
self.document.file.delete()
def get(self):
return self.client.get(
reverse(
"wagtaildocs_serve", args=(self.document.id, self.document.filename)
)
)
def clear_sendfile_cache(self):
from wagtail.utils.sendfile import _get_sendfile
_get_sendfile.clear()
@override_settings(SENDFILE_BACKEND="sendfile.backends.xsendfile")
def test_sendfile_xsendfile_backend(self):
self.clear_sendfile_cache()
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertEqual(response["X-Sendfile"], self.document.file.path)
@override_settings(
SENDFILE_BACKEND="sendfile.backends.mod_wsgi",
SENDFILE_ROOT=settings.MEDIA_ROOT,
SENDFILE_URL=settings.MEDIA_URL[:-1],
)
def test_sendfile_mod_wsgi_backend(self):
self.clear_sendfile_cache()
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertEqual(
response["Location"],
os.path.join(settings.MEDIA_URL, self.document.file.name),
)
@override_settings(
SENDFILE_BACKEND="sendfile.backends.nginx",
SENDFILE_ROOT=settings.MEDIA_ROOT,
SENDFILE_URL=settings.MEDIA_URL[:-1],
)
def test_sendfile_nginx_backend(self):
self.clear_sendfile_cache()
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertEqual(
response["X-Accel-Redirect"],
os.path.join(settings.MEDIA_URL, self.document.file.name),
)
@override_settings(WAGTAILDOCS_SERVE_METHOD=None)
class TestServeWithUnicodeFilename(TestCase):
def setUp(self):
self.document = models.Document(title="Test document")
self.filename = "docs\u0627\u0644\u0643\u0627\u062a\u062f\u0631\u0627"
"\u064a\u064a\u0629_\u0648\u0627\u0644\u0633\u0648\u0642"
try:
self.document.file.save(
self.filename, ContentFile("A boring example document")
)
except UnicodeEncodeError:
raise unittest.SkipTest("Filesystem doesn't support unicode filenames")
def tearDown(self):
# delete the FieldFile directly because the TestCase does not commit
# transactions to trigger transaction.on_commit() in the signal handler
self.document.file.delete()
def test_response_code(self):
response = self.client.get(
reverse("wagtaildocs_serve", args=(self.document.id, self.filename))
)
self.assertEqual(response.status_code, 200)
@mock.patch("wagtail.documents.views.serve.hooks")
@mock.patch("wagtail.documents.views.serve.get_object_or_404")
def test_non_local_filesystem_unicode_content_disposition_header(
self, mock_get_object_or_404, mock_hooks
):
"""
Tests that a unicode 'Content-Disposition' header (for a response using
a storage backend that doesn't expose filesystem paths) doesn't cause an
error if encoded differently.
"""
# Create a mock document to hit the correct code path.
mock_doc = mock.Mock()
mock_doc.filename = "TÈST.doc"
mock_doc.file = ContentFile(b"file-like object" * 10)
mock_doc.file.path = None
mock_doc.file.url = None
mock_get_object_or_404.return_value = mock_doc
# Bypass 'before_serve_document' hooks
mock_hooks.get_hooks.return_value = []
response = self.client.get(
reverse("wagtaildocs_serve", args=(self.document.id, mock_doc.filename))
)
self.assertEqual(response.status_code, 200)
try:
response["Content-Disposition"].encode("ascii")
except UnicodeDecodeError:
self.fail(
"Content-Disposition with unicode characters failed ascii encoding."
)
try:
response["Content-Disposition"].encode("latin-1")
except UnicodeDecodeError:
self.fail(
"Content-Disposition with unicode characters failed latin-1 encoding."
)