2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-12 06:18:48 +00:00
Files
InvenTree/src/backend/InvenTree/report/tests.py
Matthias Mair 64b9365947 chore: bump pre commit (#8904)
* bump pre-commit

* auto-fixes

* ignore error

* fix a few more issues

* fix pattern
2025-01-18 09:38:00 +11:00

697 lines
24 KiB
Python

"""Unit testing for the various report models."""
from io import StringIO
from zoneinfo import ZoneInfo
from django.apps import apps
from django.conf import settings
from django.core.cache import cache
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import SafeString
from PIL import Image
import report.models as report_models
from build.models import Build
from common.models import Attachment, InvenTreeSetting
from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase
from order.models import ReturnOrder, SalesOrder
from part.models import Part
from plugin.registry import registry
from report.models import LabelTemplate, ReportTemplate
from report.templatetags import barcode as barcode_tags
from report.templatetags import report as report_tags
from stock.models import StockItem
class ReportTagTest(TestCase):
"""Unit tests for the report template tags."""
def debug_mode(self, value: bool):
"""Enable or disable debug mode for reports."""
InvenTreeSetting.set_setting('REPORT_DEBUG_MODE', value, change_user=None)
def test_getindex(self):
"""Tests for the 'getindex' template tag."""
fn = report_tags.getindex
data = [1, 2, 3, 4, 5, 6]
# Out of bounds or invalid
self.assertEqual(fn(data, -1), None)
self.assertEqual(fn(data, 99), None)
self.assertEqual(fn(data, 'xx'), None)
for idx in range(len(data)):
self.assertEqual(fn(data, idx), data[idx])
def test_getkey(self):
"""Tests for the 'getkey' template tag."""
data = {'hello': 'world', 'foo': 'bar', 'with spaces': 'withoutspaces', 1: 2}
for k, v in data.items():
self.assertEqual(report_tags.getkey(data, k), v)
def test_asset(self):
"""Tests for asset files."""
# Test that an error is raised if the file does not exist
for b in [True, False]:
self.debug_mode(b)
with self.assertRaises(FileNotFoundError):
report_tags.asset('bad_file.txt')
# Create an asset file
asset_dir = settings.MEDIA_ROOT.joinpath('report', 'assets')
asset_dir.mkdir(parents=True, exist_ok=True)
asset_path = asset_dir.joinpath('test.txt')
asset_path.write_text('dummy data')
self.debug_mode(True)
asset = report_tags.asset('test.txt')
self.assertEqual(asset, '/media/report/assets/test.txt')
# Ensure that a 'safe string' also works
asset = report_tags.asset(SafeString('test.txt'))
self.assertEqual(asset, '/media/report/assets/test.txt')
self.debug_mode(False)
asset = report_tags.asset('test.txt')
self.assertEqual(asset, f'file://{asset_dir}/test.txt')
def test_uploaded_image(self):
"""Tests for retrieving uploaded images."""
# Test for a missing image
for b in [True, False]:
self.debug_mode(b)
with self.assertRaises(FileNotFoundError):
report_tags.uploaded_image(
'/part/something/test.png', replace_missing=False
)
img = str(report_tags.uploaded_image('/part/something/other.png'))
if b:
self.assertIn('blank_image.png', img)
else:
self.assertIn('data:image/png;charset=utf-8;base64,', img)
# Create a dummy image
img_path = 'part/images/'
img_path = settings.MEDIA_ROOT.joinpath(img_path)
img_file = img_path.joinpath('test.jpg')
img_path.mkdir(parents=True, exist_ok=True)
img_file.write_text('dummy data')
# Test in debug mode. Returns blank image as dummy file is not a valid image
self.debug_mode(True)
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, '/static/img/blank_image.png')
# Now, let's create a proper image
img = Image.new('RGB', (128, 128), color='RED')
img.save(img_file)
# Try again
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, '/media/part/images/test.jpg')
# Ensure that a 'safe string' also works
img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
self.assertEqual(img, '/media/part/images/test.jpg')
self.debug_mode(False)
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,'))
img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
self.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,'))
def test_part_image(self):
"""Unit tests for the 'part_image' tag."""
with self.assertRaises(TypeError):
report_tags.part_image(None)
def test_company_image(self):
"""Unit tests for the 'company_image' tag."""
with self.assertRaises(TypeError):
report_tags.company_image(None)
def test_logo_image(self):
"""Unit tests for the 'logo_image' tag."""
# By default, should return the core InvenTree logo
for b in [True, False]:
self.debug_mode(b)
logo = report_tags.logo_image()
self.assertIn('inventree.png', logo)
def test_maths_tags(self):
"""Simple tests for mathematical operator tags."""
self.assertEqual(report_tags.add(1, 2), 3)
self.assertEqual(report_tags.subtract(10, 4.2), 5.8)
self.assertEqual(report_tags.multiply(2.3, 4), 9.2)
self.assertEqual(report_tags.divide(100, 5), 20)
def test_number_tags(self):
"""Simple tests for number formatting tags."""
fn = report_tags.format_number
self.assertEqual(fn(1234), '1234')
self.assertEqual(fn(1234.5678, decimal_places=2), '1234.57')
self.assertEqual(fn(1234.5678, decimal_places=3), '1234.568')
self.assertEqual(fn(-9999.5678, decimal_places=2, separator=','), '-9,999.57')
self.assertEqual(
fn(9988776655.4321, integer=True, separator=' '), '9 988 776 655'
)
@override_settings(TIME_ZONE='America/New_York')
def test_date_tags(self):
"""Test for date formatting tags.
- Source timezone is Australia/Sydney
- Server timezone is America/New York
"""
time = timezone.datetime(
year=2024,
month=3,
day=13,
hour=12,
minute=30,
second=0,
tzinfo=ZoneInfo('Australia/Sydney'),
)
# Format a set of tests: timezone, format, expected
tests = [
(None, None, '2024-03-12T21:30:00-04:00'),
(None, '%d-%m-%y', '12-03-24'),
('UTC', None, '2024-03-13T01:30:00+00:00'),
('UTC', '%d-%B-%Y', '13-March-2024'),
('Europe/Amsterdam', None, '2024-03-13T02:30:00+01:00'),
('Europe/Amsterdam', '%y-%m-%d %H:%M', '24-03-13 02:30'),
]
for tz, fmt, expected in tests:
result = report_tags.format_datetime(time, tz, fmt)
self.assertEqual(result, expected)
def test_icon(self):
"""Test the icon template tag."""
for icon in [None, '', 'not:the-correct-format', 'any-non-existent-icon']:
self.assertEqual(report_tags.icon(icon), '')
self.assertEqual(
report_tags.icon('ti:package:outline'),
f'<i class="icon " style="font-family: inventree-icon-font-ti">{chr(int("eaff", 16))}</i>',
)
self.assertEqual(
report_tags.icon(
'ti:package:outline', **{'class': 'my-custom-class my-seconds-class'}
),
f'<i class="icon my-custom-class my-seconds-class" style="font-family: inventree-icon-font-ti">{chr(int("eaff", 16))}</i>',
)
def test_include_icon_fonts(self):
"""Test the include_icon_fonts template tag."""
style = report_tags.include_icon_fonts()
self.assertIn('@font-face {', style)
self.assertIn("font-family: 'inventree-icon-font-ti';", style)
self.assertIn('tabler-icons/tabler-icons.ttf', style)
self.assertIn('.icon {', style)
class BarcodeTagTest(TestCase):
"""Unit tests for the barcode template tags."""
def test_barcode(self):
"""Test the barcode generation tag."""
barcode = barcode_tags.barcode('12345')
self.assertIsInstance(barcode, str)
self.assertTrue(barcode.startswith('data:image/png;'))
# Try with a different format
barcode = barcode_tags.barcode('99999', format='BMP')
self.assertIsInstance(barcode, str)
self.assertTrue(barcode.startswith('data:image/bmp;'))
# Test empty tag
with self.assertRaises(ValueError):
barcode_tags.barcode('')
def test_qrcode(self):
"""Test the qrcode generation tag."""
# Test with default settings
qrcode = barcode_tags.qrcode('hello world')
self.assertIsInstance(qrcode, str)
self.assertTrue(qrcode.startswith('data:image/png;'))
self.assertEqual(len(qrcode), 700)
# Generate a much larger qrcode
qrcode = barcode_tags.qrcode(
'hello_world', version=2, box_size=50, format='BMP'
)
self.assertIsInstance(qrcode, str)
self.assertTrue(qrcode.startswith('data:image/bmp;'))
self.assertEqual(len(qrcode), 309720)
# Test empty tag
with self.assertRaises(ValueError):
barcode_tags.qrcode('')
def test_datamatrix(self):
"""Test the datamatrix generation tag."""
# Test with default settings
datamatrix = barcode_tags.datamatrix('hello world')
self.assertEqual(
datamatrix,
'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAAlElEQVR4nJ1TQQ7AIAgri///cncw6wroEseBgEFbCgZJnNsFICKOPAAIjeSM5T11IznK5f5WRMgnkhP9JfCcTC/MxFZ5hxLOgqrn3o/z/OqtsNpdSL31Iu9W4Dq8Sulu+q5Nuqa3XYOdnuidlICPpXhZVBruyzAKSZehT+yNlzvZQcq6JiW7Ni592swf/43kdlDfdgMk1eOtR7kWpAAAAABJRU5ErkJggg==',
)
datamatrix = barcode_tags.datamatrix(
'hello world', border=3, fill_color='red', back_color='blue'
)
self.assertEqual(
datamatrix,
'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAIAAABL1vtsAAAAqElEQVR4nN1UQQ6AMAgrxv9/GQ9mpJYSY/QkBxM3KLUUA0i8i+1l/dcQiXj09CwSEU2aQJ7nE8ou2faVUXoPZSEkq+dZKVxWg4UqxUHnVdkp6IdwMXMulGvzNBDMk4WwPSrUF3LNnQNZBJmOsZaVXa44QSEKnvWb5mIgKon1E1H6aPyOcIa15uhONP9aR4hSCiGmYAoYpj4uO+vK4+ybMhr8Nkjmn/z4Dvoldi8uJu4iAAAAAElFTkSuQmCC',
)
# Test empty tag
with self.assertRaises(ValueError):
barcode_tags.datamatrix('')
class ReportTest(InvenTreeAPITestCase):
"""Base class for unit testing reporting models."""
fixtures = [
'category',
'part',
'company',
'location',
'test_templates',
'supplier_part',
'stock',
'stock_tests',
'bom',
'build',
'order',
'return_order',
'sales_order',
]
superuser = True
def setUp(self):
"""Ensure cache is cleared as part of test setup."""
cache.clear()
apps.get_app_config('report').create_default_reports()
return super().setUp()
def test_list_endpoint(self):
"""Test that the LIST endpoint works for each report."""
url = reverse('api-report-template-list')
response = self.get(url)
self.assertEqual(response.status_code, 200)
reports = ReportTemplate.objects.all()
n = len(reports)
# API endpoint must return correct number of reports
self.assertEqual(len(response.data), n)
# Filter by "enabled" status
response = self.get(url, {'enabled': True})
self.assertEqual(len(response.data), n)
response = self.get(url, {'enabled': False})
self.assertEqual(len(response.data), 0)
# Disable each report
for report in reports:
report.enabled = False
report.save()
# Filter by "enabled" status
response = self.get(url, {'enabled': True})
self.assertEqual(len(response.data), 0)
response = self.get(url, {'enabled': False})
self.assertEqual(len(response.data), n)
# Filter by items
part_pk = Part.objects.first().pk
report = ReportTemplate.objects.filter(model_type='part').first()
return
# TODO @matmair re-enable this (in GitHub Actions) flaky test
response = self.get(url, {'model_type': 'part', 'items': part_pk})
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['pk'], report.pk)
self.assertEqual(response.data[0]['name'], report.name)
def test_create_endpoint(self):
"""Test that creating a new report works for each report."""
url = reverse('api-report-template-list')
# Create a new report
# Django REST API "APITestCase" does not work like requests - to send a file without it existing on disk,
# create it as a StringIO object, and upload it under parameter template
filestr = StringIO(
'{% extends "label/report_base.html" %}{% block content %}<pre>TEST REPORT</pre>{% endblock content %}'
)
filestr.name = 'ExampleTemplate.html'
data = {
'name': 'New report',
'description': 'A fancy new report created through API test',
'template': filestr,
'model_type': 'part2',
}
# Test with invalid model type
response = self.post(url, data=data, expected_code=400)
self.assertIn('"part2" is not a valid choice', str(response.data['model_type']))
# With valid model type
data['model_type'] = 'part'
filestr.seek(0)
response = self.post(url, data=data, format=None, expected_code=201)
# Make sure the expected keys are in the response
self.assertIn('pk', response.data)
self.assertIn('name', response.data)
self.assertIn('description', response.data)
self.assertIn('template', response.data)
self.assertIn('filters', response.data)
self.assertIn('enabled', response.data)
self.assertEqual(response.data['name'], 'New report')
self.assertEqual(
response.data['description'], 'A fancy new report created through API test'
)
self.assertTrue(response.data['template'].endswith('ExampleTemplate.html'))
def test_detail_endpoint(self):
"""Test that the DETAIL endpoint works for each report."""
reports = ReportTemplate.objects.all()
n = len(reports)
# Make sure at least one report defined
self.assertGreaterEqual(n, 1)
# Check detail page for first report
response = self.get(
reverse('api-report-template-detail', kwargs={'pk': reports[0].pk}),
expected_code=200,
)
# Make sure the expected keys are in the response
self.assertIn('pk', response.data)
self.assertIn('name', response.data)
self.assertIn('description', response.data)
self.assertIn('template', response.data)
self.assertIn('filters', response.data)
self.assertIn('enabled', response.data)
filestr = StringIO(
'{% extends "label/report_base.html" %}{% block content %}<pre>TEST REPORT VERSION 2</pre>{% endblock content %}'
)
filestr.name = 'ExampleTemplate_Updated.html'
# Check PATCH method
response = self.patch(
reverse('api-report-template-detail', kwargs={'pk': reports[0].pk}),
{
'name': 'Changed name during test',
'description': 'New version of the template',
'template': filestr,
},
format=None,
expected_code=200,
)
# Make sure the expected keys are in the response
self.assertIn('pk', response.data)
self.assertIn('name', response.data)
self.assertIn('description', response.data)
self.assertIn('template', response.data)
self.assertIn('filters', response.data)
self.assertIn('enabled', response.data)
self.assertEqual(response.data['name'], 'Changed name during test')
self.assertEqual(response.data['description'], 'New version of the template')
self.assertTrue(
response.data['template'].endswith('ExampleTemplate_Updated.html')
)
# Delete the last report
response = self.delete(
reverse('api-report-template-detail', kwargs={'pk': reports[n - 1].pk}),
expected_code=204,
)
def test_metadata(self):
"""Unit tests for the metadata field."""
p = ReportTemplate.objects.first()
self.assertEqual(p.metadata, {})
self.assertIsNone(p.get_metadata('test'))
self.assertEqual(p.get_metadata('test', backup_value=123), 123)
# Test update via the set_metadata() method
p.set_metadata('test', 3)
self.assertEqual(p.get_metadata('test'), 3)
for k in ['apple', 'banana', 'carrot', 'carrot', 'banana']:
p.set_metadata(k, k)
self.assertEqual(len(p.metadata.keys()), 4)
def test_report_template_permissions(self):
"""Test that the user permissions are correctly applied.
- For all /api/report/ endpoints, any authenticated user should have full read access
- Write access is limited to staff users
- Non authenticated users should have no access at all
"""
# First test the "report list" endpoint
url = reverse('api-report-template-list')
template = ReportTemplate.objects.first()
detail_url = reverse('api-report-template-detail', kwargs={'pk': template.pk})
# Non-authenticated user should have no access
self.logout()
self.get(url, expected_code=401)
# Authenticated user should have read access
self.user.is_staff = False
self.user.save()
self.login()
# Check read access to template list URL
self.get(url, expected_code=200)
# Check read access to template detail URL
self.get(detail_url, expected_code=200)
# An update to the report template should fail
self.patch(
detail_url,
data={'description': 'Some new description here?'},
expected_code=403,
)
# Now, test with a staff user
self.logout()
self.user.is_staff = True
self.user.save()
self.login()
self.patch(
detail_url,
data={'description': 'An updated description'},
expected_code=200,
)
template.refresh_from_db()
self.assertEqual(template.description, 'An updated description')
class PrintTestMixins:
"""Mixin that enables e2e printing tests."""
plugin_ref = 'samplelabelprinter'
def do_activate_plugin(self):
"""Activate the 'samplelabel' plugin."""
plugin = registry.get_plugin(self.plugin_ref)
self.assertIsNotNone(plugin)
config = plugin.plugin_config()
self.assertIsNotNone(config)
config.active = True
config.save()
def run_print_test(self, qs, model_type, label: bool = True):
"""Run tests on single and multiple page printing.
Args:
qs: class of the base queryset
model_type: the model type of the queryset
label: whether the model is a label or report
"""
mdl = LabelTemplate if label else ReportTemplate
url = reverse('api-label-print' if label else 'api-report-print')
qs = qs.objects.all()
template = mdl.objects.filter(enabled=True, model_type=model_type).first()
plugin = registry.get_plugin(self.plugin_ref)
# Single page printing
self.post(
url,
{'template': template.pk, 'plugin': plugin.pk, 'items': [qs[0].pk]},
expected_code=201,
)
# Multi page printing
self.post(
url,
{
'template': template.pk,
'plugin': plugin.pk,
'items': [item.pk for item in qs],
},
expected_code=201,
max_query_time=15,
max_query_count=500 * len(qs),
)
# Test with wrong dimensions
if not hasattr(template, 'width'):
return
org_width = template.width
template.width = 0
template.save()
response = self.post(
url,
{'template': template.pk, 'plugin': plugin.pk, 'items': [qs[0].pk]},
expected_code=400,
)
self.assertEqual(str(response.data['template'][0]), 'Invalid label dimensions')
template.width = org_width
template.save()
class TestReportTest(PrintTestMixins, ReportTest):
"""Unit testing class for the stock item TestReport model."""
model = report_models.ReportTemplate
list_url = 'api-report-template-list'
detail_url = 'api-report-template-detail'
print_url = 'api-report-print'
def setUp(self):
"""Setup function for the stock item TestReport."""
apps.get_app_config('report').create_default_reports()
self.do_activate_plugin()
return super().setUp()
def test_print(self):
"""Printing tests for the TestReport."""
template = ReportTemplate.objects.filter(
enabled=True, model_type='stockitem'
).first()
self.assertIsNotNone(template)
# Ensure that the 'attach_to_model' attribute is initially False
template.attach_to_model = False
template.save()
template.refresh_from_db()
self.assertFalse(template.attach_to_model)
url = reverse(self.print_url)
# Try to print without providing a valid StockItem
self.post(url, {'template': template.pk}, expected_code=400)
# Try to print with an invalid StockItem
self.post(url, {'template': template.pk, 'items': [9999]}, expected_code=400)
# Now print with a valid StockItem
item = StockItem.objects.first()
n = item.attachments.count()
response = self.post(
url, {'template': template.pk, 'items': [item.pk]}, expected_code=201
)
# There should be a link to the generated PDF
self.assertTrue(response.data['output'].startswith('/media/report/'))
self.assertTrue(response.data['output'].endswith('.pdf'))
# By default, this should *not* have created an attachment against this stockitem
self.assertEqual(n, item.attachments.count())
self.assertFalse(
Attachment.objects.filter(model_id=item.pk, model_type='stockitem').exists()
)
# Now try again, but attach the generated PDF to the StockItem
template.attach_to_model = True
template.save()
response = self.post(
url, {'template': template.pk, 'items': [item.pk]}, expected_code=201
)
# A new attachment should have been created
self.assertEqual(n + 1, item.attachments.count())
attachment = item.attachments.order_by('-pk').first()
# The attachment should be a PDF
self.assertTrue(attachment.attachment.name.endswith('.pdf'))
def test_mdl_build(self):
"""Test the Build model."""
self.run_print_test(Build, 'build', label=False)
def test_mdl_returnorder(self):
"""Test the ReturnOrder model."""
self.run_print_test(ReturnOrder, 'returnorder', label=False)
def test_mdl_salesorder(self):
"""Test the SalesOrder model."""
self.run_print_test(SalesOrder, 'salesorder', label=False)
class AdminTest(AdminTestCase):
"""Tests for the admin interface integration."""
def test_admin(self):
"""Test the admin URL."""
self.helper(model=ReportTemplate)