Read report/label templates in binary mode (#12432)
* Read report/label templates in binary mode file_from_template read the template in text mode and wrapped the string in a ContentFile. The ContentFile size then reflected the character count, which differs from the UTF-8 byte length for any template that contains multi-byte characters. Strict S3 implementations such as Backblaze B2 validate the Content-Length header and reject the upload with an IncompleteBody error. AWS S3 tolerates the mismatch, so the problem is not visible there. inventree_transfer_order_report.html is 1458 characters but 1460 bytes. Its upload fails during create_default_reports, the transaction rolls back, and the default template is recreated and fails again on every startup. Use Path.read_bytes() so the ContentFile size matches the uploaded bytes and the file handle is closed. Add a regression test asserting the size equals the file's byte length. * Test file_from_template across text encodings
This commit is contained in:
parent
b723f5d2ca
commit
ce47cda948
|
|
@ -96,7 +96,7 @@ class ReportConfig(AppConfig):
|
|||
raise FileNotFoundError(
|
||||
'Template file %s does not exist in %s', file_name, file
|
||||
)
|
||||
return ContentFile(file.open('r').read(), os.path.basename(file_name))
|
||||
return ContentFile(file.read_bytes(), os.path.basename(file_name))
|
||||
|
||||
def create_default_labels(self):
|
||||
"""Create default label templates."""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"""Unit testing for the various report models."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.apps import apps
|
||||
|
|
@ -18,6 +20,7 @@ import report.models as report_models
|
|||
from build.models import Build
|
||||
from common.models import Attachment
|
||||
from common.settings import set_global_setting
|
||||
from InvenTree.config import get_base_dir
|
||||
from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase
|
||||
from order.models import PurchaseOrder, ReturnOrder, SalesOrder
|
||||
from part.models import Part
|
||||
|
|
@ -1073,3 +1076,66 @@ class URLFetcherTest(TestCase):
|
|||
with patch('weasyprint.urls.URLFetcher.fetch', return_value={}):
|
||||
self.fetcher.fetch('data:image/png;base64,abc123')
|
||||
self.fetcher.fetch('data:text/css;base64,abc123')
|
||||
|
||||
|
||||
class DefaultTemplateFileTest(TestCase):
|
||||
"""Unit tests for building the default report and label template files."""
|
||||
|
||||
def test_file_size_matches_bytes(self):
|
||||
"""file_from_template must size the ContentFile by byte length.
|
||||
|
||||
Reading the template in text mode makes the size a character count,
|
||||
which is smaller than the byte length for multi-byte content and breaks
|
||||
uploads to S3 backends that enforce Content-Length.
|
||||
"""
|
||||
config = apps.get_app_config('report')
|
||||
filename = 'inventree_transfer_order_report.html'
|
||||
content_file = config.file_from_template('report', filename)
|
||||
path = get_base_dir().joinpath('report', 'templates', 'report', filename)
|
||||
self.assertEqual(content_file.size, path.stat().st_size)
|
||||
|
||||
def test_file_from_template_is_encoding_agnostic(self):
|
||||
"""file_from_template must preserve the exact bytes for any encoding.
|
||||
|
||||
A text-mode read sizes the ContentFile by character count and needs the
|
||||
correct decoder, so non-English content is both mis-sized and at risk of
|
||||
corruption. Reading the raw bytes keeps the file identical to what is on
|
||||
disk, so the size always matches the Content-Length used for S3 uploads.
|
||||
The samples below cover non-ASCII text in a few encodings, including
|
||||
multi-byte content where a character count would not equal the byte
|
||||
length.
|
||||
"""
|
||||
config = apps.get_app_config('report')
|
||||
|
||||
# (encoding, text, whether the encoding uses multi-byte characters).
|
||||
# The utf cases hold characters that span more than one byte, so a
|
||||
# character count would not equal the byte length; latin-1 stays
|
||||
# single-byte and is included to show the read is not utf-8 specific.
|
||||
cases = [
|
||||
('utf-8', 'Système de café ≤ 你好 в наявності 📦', True),
|
||||
('utf-16', 'Système de café ≤ 你好 в наявності 📦', True),
|
||||
('latin-1', 'Système de café àéîõü', False),
|
||||
]
|
||||
|
||||
for encoding, text, multibyte in cases:
|
||||
with self.subTest(encoding=encoding):
|
||||
body = f'<html><body>{text}</body></html>'
|
||||
raw = body.encode(encoding)
|
||||
|
||||
if multibyte:
|
||||
# A character count would differ from the byte length here
|
||||
self.assertNotEqual(len(raw), len(body))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base = Path(tmp)
|
||||
template_dir = base.joinpath('report', 'templates', 'report')
|
||||
template_dir.mkdir(parents=True)
|
||||
filename = f'encoding_{encoding}.html'
|
||||
template_dir.joinpath(filename).write_bytes(raw)
|
||||
|
||||
with patch('report.apps.get_base_dir', return_value=base):
|
||||
content_file = config.file_from_template('report', filename)
|
||||
|
||||
# Size and content must match the raw bytes exactly
|
||||
self.assertEqual(content_file.size, len(raw))
|
||||
self.assertEqual(content_file.read(), raw)
|
||||
|
|
|
|||
Loading…
Reference in New Issue