diff --git a/src/backend/InvenTree/InvenTree/unit_test.py b/src/backend/InvenTree/InvenTree/unit_test.py index b993bd70a9..f7832c782b 100644 --- a/src/backend/InvenTree/InvenTree/unit_test.py +++ b/src/backend/InvenTree/InvenTree/unit_test.py @@ -10,10 +10,11 @@ from pathlib import Path from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission -from django.db import connections +from django.db import connections, models from django.http.response import StreamingHttpResponse from django.test import TestCase from django.test.utils import CaptureQueriesContext +from django.urls import reverse from djmoney.contrib.exchange.models import ExchangeBackend, Rate from rest_framework.test import APITestCase @@ -486,3 +487,30 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase): def assertDictContainsSubset(self, a, b): """Assert that dictionary 'a' is a subset of dictionary 'b'.""" self.assertEqual(b, b | a) + + +class AdminTestCase(InvenTreeAPITestCase): + """Tests for the admin interface integration.""" + + superuser = True + + def helper(self, model: type[models.Model], model_kwargs=None): + """Test the admin URL.""" + if model_kwargs is None: + model_kwargs = {} + + # Add object + obj = model.objects.create(**model_kwargs) + app_app, app_mdl = model._meta.app_label, model._meta.model_name + + # 'Test listing + response = self.get(reverse(f'admin:{app_app}_{app_mdl}_changelist')) + self.assertEqual(response.status_code, 200) + + # Test change view + response = self.get( + reverse(f'admin:{app_app}_{app_mdl}_change', kwargs={'object_id': obj.pk}) + ) + self.assertEqual(response.status_code, 200) + + return obj diff --git a/src/backend/InvenTree/importer/tests.py b/src/backend/InvenTree/importer/tests.py index e7514610cc..45ea5052c7 100644 --- a/src/backend/InvenTree/importer/tests.py +++ b/src/backend/InvenTree/importer/tests.py @@ -4,11 +4,27 @@ import os from django.core.files.base import ContentFile -from importer.models import DataImportSession -from InvenTree.unit_test import InvenTreeTestCase +from importer.models import DataImportRow, DataImportSession +from InvenTree.unit_test import AdminTestCase, InvenTreeTestCase -class ImporterTest(InvenTreeTestCase): +class ImporterMixin: + """Helpers for import tests.""" + + def helper_file(self): + """Return test data.""" + fn = os.path.join(os.path.dirname(__file__), 'test_data', 'companies.csv') + + with open(fn, encoding='utf-8') as input_file: + data = input_file.read() + return data + + def helper_content(self): + """Return content file.""" + return ContentFile(self.helper_file(), 'companies.csv') + + +class ImporterTest(ImporterMixin, InvenTreeTestCase): """Basic tests for file imports.""" def test_import_session(self): @@ -17,13 +33,8 @@ class ImporterTest(InvenTreeTestCase): n = Company.objects.count() - fn = os.path.join(os.path.dirname(__file__), 'test_data', 'companies.csv') - - with open(fn, encoding='utf-8') as input_file: - data = input_file.read() - session = DataImportSession.objects.create( - data_file=ContentFile(data, 'companies.csv'), model_type='company' + data_file=self.helper_content(), model_type='company' ) session.extract_columns() @@ -61,3 +72,15 @@ class ImporterTest(InvenTreeTestCase): def test_field_defaults(self): """Test default field values.""" + + +class AdminTest(ImporterMixin, AdminTestCase): + """Tests for the admin interface integration.""" + + def test_admin(self): + """Test the admin URL.""" + session = self.helper( + model=DataImportSession, + model_kwargs={'data_file': self.helper_content(), 'model_type': 'company'}, + ) + self.helper(model=DataImportRow, model_kwargs={'session_id': session.id}) diff --git a/src/backend/InvenTree/machine/tests.py b/src/backend/InvenTree/machine/tests.py index 6075ee7118..16e6bee638 100755 --- a/src/backend/InvenTree/machine/tests.py +++ b/src/backend/InvenTree/machine/tests.py @@ -9,7 +9,7 @@ from django.urls import reverse from rest_framework import serializers -from InvenTree.unit_test import InvenTreeAPITestCase +from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus from machine.machine_types.label_printer import LabelPrinterBaseDriver from machine.models import MachineConfig @@ -309,3 +309,11 @@ class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase }, expected_code=400, ) + + +class AdminTest(AdminTestCase): + """Tests for the admin interface integration.""" + + def test_admin(self): + """Test the admin URL.""" + self.helper(model=MachineConfig) diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index e534a3ab39..429f7a3166 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -16,7 +16,7 @@ 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 InvenTreeAPITestCase +from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase from order.models import ReturnOrder, SalesOrder from plugin.registry import registry from report.models import LabelTemplate, ReportTemplate @@ -580,24 +580,6 @@ class TestReportTest(PrintTestMixins, ReportTest): Attachment.objects.filter(model_id=item.pk, model_type='stockitem').exists() ) - return - # TODO @matmair - Re-add this test after https://github.com/inventree/InvenTree/pull/7074/files#r1600694356 is resolved - # Change the setting, now the test report should be attached automatically - InvenTreeSetting.set_setting('REPORT_ATTACH_TEST_REPORT', True, None) - - response = self.post( - url, {'template': template.pk, 'items': [item.pk]}, expected_code=201 - ) - - # There should be a link to the generated PDF - self.assertEqual(response.data['output'].startswith('/media/report/'), True) - - # Check that a report has been uploaded - attachment = Attachment.objects.filter( - model_id=item.pk, model_type='stockitem' - ).first() - self.assertIsNotNone(attachment) - def test_mdl_build(self): """Test the Build model.""" self.run_print_test(Build, 'build', label=False) @@ -609,3 +591,11 @@ class TestReportTest(PrintTestMixins, ReportTest): 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) diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 4e06d0ac42..c49c4b3319 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1527,6 +1527,11 @@ class StocktakeTest(StockAPITestCase): def test_action(self): """Test each stocktake action endpoint, for validation.""" + target = { + 'api-stock-count': '10.00000', + 'api-stock-add': '10.00000', + 'api-stock-remove': '10.00000', + } for endpoint in ['api-stock-count', 'api-stock-add', 'api-stock-remove']: url = reverse(endpoint) @@ -1585,6 +1590,12 @@ class StocktakeTest(StockAPITestCase): status_code=status.HTTP_400_BAD_REQUEST, ) + # Valid POST + data = {'items': [{'pk': 1234, 'quantity': 10}]} + response = self.post(url, data, expected_code=201) + self.assertEqual(response.data['items'][0]['pk'], 1234) + self.assertEqual(response.data['items'][0]['quantity'], target[endpoint]) + def test_transfer(self): """Test stock transfers.""" stock_item = StockItem.objects.get(pk=1234) diff --git a/src/backend/InvenTree/stock/test_views.py b/src/backend/InvenTree/stock/test_views.py index 0552a5af7a..9f7d4e84ba 100644 --- a/src/backend/InvenTree/stock/test_views.py +++ b/src/backend/InvenTree/stock/test_views.py @@ -79,6 +79,11 @@ class StockDetailTest(StockViewTestCase): for act in actions: self.assertIn(act, html) + # Check with a wrong pk + response = self.client.get(reverse('stock-item-detail', kwargs={'pk': 99})) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse('stock-index')) + class StockOwnershipTest(StockViewTestCase): """Tests for stock ownership views.""" diff --git a/src/backend/InvenTree/stock/tests.py b/src/backend/InvenTree/stock/tests.py index dcbf2cf3d3..ba8ca82fee 100644 --- a/src/backend/InvenTree/stock/tests.py +++ b/src/backend/InvenTree/stock/tests.py @@ -10,7 +10,7 @@ from django.test import override_settings from build.models import Build from common.models import InvenTreeSetting from company.models import Company -from InvenTree.unit_test import InvenTreeTestCase +from InvenTree.unit_test import AdminTestCase, InvenTreeTestCase from order.models import SalesOrder from part.models import Part, PartTestTemplate from stock.status_codes import StockHistoryCode @@ -1347,3 +1347,11 @@ class StockLocationTest(InvenTreeTestCase): loc.location_type = None loc.save() self.assertEqual(loc.icon, '') + + +class AdminTest(AdminTestCase): + """Tests for the admin interface integration.""" + + def test_admin(self): + """Test the admin URL.""" + self.helper(model=StockLocationType) diff --git a/src/backend/InvenTree/users/api.py b/src/backend/InvenTree/users/api.py index c8cfb12807..5961425367 100644 --- a/src/backend/InvenTree/users/api.py +++ b/src/backend/InvenTree/users/api.py @@ -175,13 +175,10 @@ class GroupMixin: def get_serializer(self, *args, **kwargs): """Return serializer instance for this endpoint.""" # Do we wish to include extra detail? - try: - params = self.request.query_params - kwargs['permission_detail'] = InvenTree.helpers.str2bool( - params.get('permission_detail', None) - ) - except AttributeError: - pass + params = self.request.query_params + kwargs['permission_detail'] = InvenTree.helpers.str2bool( + params.get('permission_detail', None) + ) kwargs['context'] = self.get_serializer_context() return self.serializer_class(*args, **kwargs) @@ -346,7 +343,7 @@ class GetAuthToken(APIView): return Response(data) else: - raise exceptions.NotAuthenticated() + raise exceptions.NotAuthenticated() # pragma: no cover class TokenListView(DestroyAPIView, ListAPI): diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py index 6ae90644d7..9f34b06319 100644 --- a/src/backend/InvenTree/users/models.py +++ b/src/backend/InvenTree/users/models.py @@ -164,7 +164,7 @@ class ApiToken(AuthToken, InvenTree.models.MetadataMixin): """ # If the token has not yet been saved, return the raw key if self.pk is None: - return self.key + return self.key # pragma: no cover M = len(self.key) - 20 diff --git a/src/backend/InvenTree/users/test_api.py b/src/backend/InvenTree/users/test_api.py index ea271f7e19..f04f6314e0 100644 --- a/src/backend/InvenTree/users/test_api.py +++ b/src/backend/InvenTree/users/test_api.py @@ -64,12 +64,21 @@ class UserAPITests(InvenTreeAPITestCase): self.assertEqual(len(response.data), Group.objects.count()) # Check detail URL + pk = response.data[0]['pk'] response = self.get( - reverse('api-group-detail', kwargs={'pk': response.data[0]['pk']}), + reverse('api-group-detail', kwargs={'pk': pk}), expected_code=200 + ) + self.assertIn('name', response.data) + self.assertNotIn('permissions', response.data) + + # Check more detailed URL + response = self.get( + reverse('api-group-detail', kwargs={'pk': pk}), + data={'permission_detail': True}, expected_code=200, ) - self.assertIn('name', response.data) + self.assertIn('permissions', response.data) def test_logout(self): """Test api logout endpoint.""" @@ -208,3 +217,34 @@ class UserTokenTests(InvenTreeAPITestCase): ) self.assertIn('key', response.data) self.assertTrue(response.data['key'].startswith('inv-')) + + def test_token_api(self): + """Test the token API.""" + url = reverse('api-token-list') + response = self.get(url, expected_code=200) + self.assertEqual(response.data, []) + + # Get token + response = self.get(reverse('api-token'), expected_code=200) + self.assertIn('token', response.data) + + # Now there should be one token + response = self.get(url, expected_code=200) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['active'], True) + self.assertEqual(response.data[0]['revoked'], False) + self.assertEqual(response.data[0]['in_use'], False) + expected_day = str( + datetime.datetime.now().date() + datetime.timedelta(days=365) + ) + self.assertEqual(response.data[0]['expiry'], expected_day) + + # Destroy token + self.delete( + reverse('api-token-detail', kwargs={'pk': response.data[0]['id']}), + expected_code=204, + ) + + # Get token without auth (should fail) + self.client.logout() + self.get(reverse('api-token'), expected_code=401) diff --git a/src/backend/InvenTree/users/tests.py b/src/backend/InvenTree/users/tests.py index 669c550aca..9e123bb181 100644 --- a/src/backend/InvenTree/users/tests.py +++ b/src/backend/InvenTree/users/tests.py @@ -5,7 +5,8 @@ from django.contrib.auth.models import Group from django.test import TestCase, tag from django.urls import reverse -from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase +from common.settings import set_global_setting +from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase, InvenTreeTestCase from users.models import ApiToken, Owner, RuleSet @@ -270,6 +271,29 @@ class OwnerModelTest(InvenTreeTestCase): ) self.assertEqual(response['username'], self.username) + def test_display_name(self): + """Test the display name for the owner.""" + owner = Owner.get_owner(self.user) + self.assertEqual(owner.name(), 'testuser') + self.assertEqual(str(owner), 'testuser (user)') + + # Change setting + set_global_setting('DISPLAY_FULL_NAMES', True) + self.user.first_name = 'first' + self.user.last_name = 'last' + self.user.save() + owner = Owner.get_owner(self.user) + + # Now first / last should be used + self.assertEqual(owner.name(), 'first last') + self.assertEqual(str(owner), 'first last (user)') + + # Reset + set_global_setting('DISPLAY_FULL_NAMES', False) + self.user.first_name = '' + self.user.last_name = '' + self.user.save() + class MFALoginTest(InvenTreeAPITestCase): """Some simplistic tests to ensure that MFA is working.""" @@ -313,3 +337,15 @@ class MFALoginTest(InvenTreeAPITestCase): # Wrong login should not work auth_data['password'] = 'wrong' self.post(login_url, auth_data, expected_code=401) + + +class AdminTest(AdminTestCase): + """Tests for the admin interface integration.""" + + def test_admin(self): + """Test the admin URL.""" + my_token = self.helper( + model=ApiToken, model_kwargs={'user': self.user, 'name': 'test-token'} + ) + # Additionally test str fnc + self.assertEqual(str(my_token), my_token.token) diff --git a/src/backend/InvenTree/web/tests.py b/src/backend/InvenTree/web/tests.py index 68741e1f85..4603f25ded 100644 --- a/src/backend/InvenTree/web/tests.py +++ b/src/backend/InvenTree/web/tests.py @@ -37,12 +37,22 @@ class TemplateTagTest(InvenTreeTestCase): manifest_file = Path(__file__).parent.joinpath('static/web/.vite/manifest.json') # Try with removed manifest file - manifest_file.rename(manifest_file.with_suffix('.json.bak')) # Rename + new_name = manifest_file.rename( + manifest_file.with_suffix('.json.bak') + ) # Rename resp = spa_helper.spa_bundle() self.assertIsNone(resp) - manifest_file.with_suffix('.json.bak').rename( - manifest_file.with_suffix('.json') - ) # Name back + + # Try with differing name + resp = spa_helper.spa_bundle(new_name) + self.assertIsNotNone(resp) + + # Broken manifest file + manifest_file.write_text('broken') + resp = spa_helper.spa_bundle(manifest_file) + self.assertIsNone(resp) + + new_name.rename(manifest_file.with_suffix('.json')) # Name back def test_spa_settings(self): """Test the 'spa_settings' template tag.""" @@ -78,6 +88,11 @@ class TemplateTagTest(InvenTreeTestCase): self.assertNotIn('show_server_selector', rsp) self.assertEqual(rsp['server_list'], ['aa', 'bb']) + def test_redirects(self): + """Test the redirect helper.""" + response = self.client.get('/assets/testpath') + self.assertEqual(response.url, '/static/web/assets/testpath') + class TestWebHelpers(InvenTreeAPITestCase): """Tests for the web helpers."""