diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 6c801a95ca..40bb0db81d 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -522,7 +522,7 @@ if "mysql" in db_engine: # pragma: no cover # https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level if "isolation_level" not in db_options: serializable = _is_true( - os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true") + os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "false") ) db_options["isolation_level"] = ( "serializable" if serializable else "read committed" diff --git a/InvenTree/build/migrations/0032_auto_20211014_0632.py b/InvenTree/build/migrations/0032_auto_20211014_0632.py index 8842e25cc7..a9d5fd124c 100644 --- a/InvenTree/build/migrations/0032_auto_20211014_0632.py +++ b/InvenTree/build/migrations/0032_auto_20211014_0632.py @@ -24,6 +24,10 @@ def build_refs(apps, schema_editor): except: # pragma: no cover ref = 0 + # Clip integer value to ensure it does not overflow database field + if ref > 0x7fffffff: + ref = 0x7fffffff + build.reference_int = ref build.save() diff --git a/InvenTree/order/migrations/0052_auto_20211014_0631.py b/InvenTree/order/migrations/0052_auto_20211014_0631.py index fda4335e08..20024f5f9c 100644 --- a/InvenTree/order/migrations/0052_auto_20211014_0631.py +++ b/InvenTree/order/migrations/0052_auto_20211014_0631.py @@ -23,6 +23,10 @@ def build_refs(apps, schema_editor): except: # pragma: no cover ref = 0 + # Clip integer value to ensure it does not overflow database field + if ref > 0x7fffffff: + ref = 0x7fffffff + order.reference_int = ref order.save() @@ -40,6 +44,10 @@ def build_refs(apps, schema_editor): except: # pragma: no cover ref = 0 + # Clip integer value to ensure it does not overflow database field + if ref > 0x7fffffff: + ref = 0x7fffffff + order.reference_int = ref order.save() diff --git a/InvenTree/order/test_migrations.py b/InvenTree/order/test_migrations.py index 61299a8e2f..4e5aed80a3 100644 --- a/InvenTree/order/test_migrations.py +++ b/InvenTree/order/test_migrations.py @@ -56,6 +56,19 @@ class TestRefIntMigrations(MigratorTestCase): with self.assertRaises(AttributeError): print(sales_order.reference_int) + # Create orders with very large reference values + self.po_pk = PurchaseOrder.objects.create( + supplier=supplier, + reference='999999999999999999999999999999999', + description='Big reference field', + ).pk + + self.so_pk = SalesOrder.objects.create( + customer=supplier, + reference='999999999999999999999999999999999', + description='Big reference field', + ).pk + def test_ref_field(self): """ Test that the 'reference_int' field has been created and is filled out correctly @@ -73,6 +86,15 @@ class TestRefIntMigrations(MigratorTestCase): self.assertEqual(po.reference_int, ii) self.assertEqual(so.reference_int, ii) + # Tests for orders with overly large reference values + po = PurchaseOrder.objects.get(pk=self.po_pk) + self.assertEqual(po.reference, '999999999999999999999999999999999') + self.assertEqual(po.reference_int, 0x7fffffff) + + so = SalesOrder.objects.get(pk=self.so_pk) + self.assertEqual(so.reference, '999999999999999999999999999999999') + self.assertEqual(so.reference_int, 0x7fffffff) + class TestShipmentMigration(MigratorTestCase): """ diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 4cd2123ef3..aa196a9962 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -467,6 +467,10 @@ class APICallMixin: if endpoint_is_url: url = endpoint else: + + if endpoint.startswith('/'): + endpoint = endpoint[1:] + url = f'{self.api_url}/{endpoint}' # build kwargs for call @@ -474,6 +478,7 @@ class APICallMixin: 'url': url, 'headers': headers, } + if data: kwargs['data'] = json.dumps(data) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index e91f4da365..13cfb47d79 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -1,4 +1,4 @@ -""" Unit tests for base mixins for plugins """ +"""Unit tests for base mixins for plugins.""" from django.conf import settings from django.test import TestCase @@ -17,7 +17,10 @@ from plugin.urls import PLUGIN_BASE class BaseMixinDefinition: + """Mixin to test the meta functions of all mixins.""" + def test_mixin_name(self): + """Test that the mixin registers itseld correctly.""" # mixin name self.assertIn(self.MIXIN_NAME, [item['key'] for item in self.mixin.registered_mixins]) # human name @@ -25,6 +28,8 @@ class BaseMixinDefinition: class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): + """Tests for SettingsMixin.""" + MIXIN_HUMAN_NAME = 'Settings' MIXIN_NAME = 'settings' MIXIN_ENABLE_CHECK = 'has_settings' @@ -32,6 +37,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): TEST_SETTINGS = {'SETTING1': {'default': '123', }} def setUp(self): + """Setup for all tests.""" class SettingsCls(SettingsMixin, InvenTreePlugin): SETTINGS = self.TEST_SETTINGS self.mixin = SettingsCls() @@ -43,6 +49,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): super().setUp() def test_function(self): + """Test that the mixin functions.""" # settings variable self.assertEqual(self.mixin.settings, self.TEST_SETTINGS) @@ -60,11 +67,14 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): class UrlsMixinTest(BaseMixinDefinition, TestCase): + """Tests for UrlsMixin.""" + MIXIN_HUMAN_NAME = 'URLs' MIXIN_NAME = 'urls' MIXIN_ENABLE_CHECK = 'has_urls' def setUp(self): + """Setup for all tests.""" class UrlsCls(UrlsMixin, InvenTreePlugin): def test(): return 'ccc' @@ -76,6 +86,7 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase): self.mixin_nothing = NoUrlsCls() def test_function(self): + """Test that the mixin functions.""" plg_name = self.mixin.plugin_name() # base_url @@ -99,26 +110,32 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase): class AppMixinTest(BaseMixinDefinition, TestCase): + """Tests for AppMixin.""" + MIXIN_HUMAN_NAME = 'App registration' MIXIN_NAME = 'app' MIXIN_ENABLE_CHECK = 'has_app' def setUp(self): + """Setup for all tests.""" class TestCls(AppMixin, InvenTreePlugin): pass self.mixin = TestCls() def test_function(self): - # test that this plugin is in settings + """Test that the sample plugin registers in settings.""" self.assertIn('plugin.samples.integration', settings.INSTALLED_APPS) class NavigationMixinTest(BaseMixinDefinition, TestCase): + """Tests for NavigationMixin.""" + MIXIN_HUMAN_NAME = 'Navigation Links' MIXIN_NAME = 'navigation' MIXIN_ENABLE_CHECK = 'has_naviation' def setUp(self): + """Setup for all tests.""" class NavigationCls(NavigationMixin, InvenTreePlugin): NAVIGATION = [ {'name': 'aa', 'link': 'plugin:test:test_view'}, @@ -131,6 +148,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): self.nothing_mixin = NothingNavigationCls() def test_function(self): + """Test that a correct configuration functions.""" # check right configuration self.assertEqual(self.mixin.navigation, [{'name': 'aa', 'link': 'plugin:test:test_view'}, ]) @@ -139,7 +157,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(self.nothing_mixin.navigation_name, '') def test_fail(self): - # check wrong links fails + """Test that wrong links fail.""" with self.assertRaises(NotImplementedError): class NavigationCls(NavigationMixin, InvenTreePlugin): NAVIGATION = ['aa', 'aa'] @@ -147,11 +165,15 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): class APICallMixinTest(BaseMixinDefinition, TestCase): + """Tests for APICallMixin.""" + MIXIN_HUMAN_NAME = 'API calls' MIXIN_NAME = 'api_call' MIXIN_ENABLE_CHECK = 'has_api_call' def setUp(self): + """Setup for all tests.""" + class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin): NAME = "Sample API Caller" @@ -163,40 +185,47 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): 'API_URL': { 'name': 'External URL', 'description': 'Where is your API located?', - 'default': 'reqres.in', + 'default': 'https://api.github.com', }, } + API_URL_SETTING = 'API_URL' API_TOKEN_SETTING = 'API_TOKEN' + @property + def api_url(self): + """Override API URL for this test""" + return "https://api.github.com" + def get_external_url(self, simple: bool = True): - ''' - returns data from the sample endpoint - ''' - return self.api_call('api/users/2', simple_response=simple) + """Returns data from the sample endpoint.""" + return self.api_call('orgs/inventree', simple_response=simple) + self.mixin = MixinCls() class WrongCLS(APICallMixin, InvenTreePlugin): pass + self.mixin_wrong = WrongCLS() class WrongCLS2(APICallMixin, InvenTreePlugin): API_URL_SETTING = 'test' + self.mixin_wrong2 = WrongCLS2() def test_base_setup(self): - """Test that the base settings work""" + """Test that the base settings work.""" # check init self.assertTrue(self.mixin.has_api_call) # api_url - self.assertEqual('https://reqres.in', self.mixin.api_url) + self.assertEqual('https://api.github.com', self.mixin.api_url) # api_headers headers = self.mixin.api_headers self.assertEqual(headers, {'Bearer': '', 'Content-Type': 'application/json'}) def test_args(self): - """Test that building up args work""" + """Test that building up args work.""" # api_build_url_args # 1 arg result = self.mixin.api_build_url_args({'a': 'b'}) @@ -209,11 +238,13 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(result, '?a=b&c=d,e,f') def test_api_call(self): - """Test that api calls work""" + """Test that api calls work.""" # api_call result = self.mixin.get_external_url() self.assertTrue(result) - self.assertIn('data', result,) + + for key in ['login', 'email', 'name', 'twitter_username']: + self.assertIn(key, result) # api_call without json conversion result = self.mixin.get_external_url(False) @@ -221,25 +252,25 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(result.reason, 'OK') # api_call with full url - result = self.mixin.api_call('https://reqres.in/api/users/2', endpoint_is_url=True) + result = self.mixin.api_call('https://api.github.com/orgs/inventree', endpoint_is_url=True) self.assertTrue(result) # api_call with post and data result = self.mixin.api_call( - 'api/users/', - data={"name": "morpheus", "job": "leader"}, - method='POST' + 'repos/inventree/InvenTree', + method='GET' ) + self.assertTrue(result) - self.assertEqual(result['name'], 'morpheus') + self.assertEqual(result['name'], 'InvenTree') + self.assertEqual(result['html_url'], 'https://github.com/inventree/InvenTree') # api_call with filter - result = self.mixin.api_call('api/users', url_args={'page': '2'}) + result = self.mixin.api_call('repos/inventree/InvenTree/stargazers', url_args={'page': '2'}) self.assertTrue(result) - self.assertEqual(result['page'], 2) def test_function_errors(self): - """Test function errors""" + """Test function errors.""" # wrongly defined plugins should not load with self.assertRaises(MixinNotImplementedError): self.mixin_wrong.has_api_call() @@ -250,7 +281,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): class PanelMixinTests(InvenTreeTestCase): - """Test that the PanelMixin plugin operates correctly""" + """Test that the PanelMixin plugin operates correctly.""" fixtures = [ 'category', @@ -262,8 +293,7 @@ class PanelMixinTests(InvenTreeTestCase): roles = 'all' def test_installed(self): - """Test that the sample panel plugin is installed""" - + """Test that the sample panel plugin is installed.""" plugins = registry.with_mixin('panel') self.assertTrue(len(plugins) > 0) @@ -275,8 +305,7 @@ class PanelMixinTests(InvenTreeTestCase): self.assertEqual(len(plugins), 0) def test_disabled(self): - """Test that the panels *do not load* if the plugin is not enabled""" - + """Test that the panels *do not load* if the plugin is not enabled.""" plugin = registry.get_plugin('samplepanel') plugin.set_setting('ENABLE_HELLO_WORLD', True) @@ -305,10 +334,7 @@ class PanelMixinTests(InvenTreeTestCase): self.assertNotIn('Custom Part Panel', str(response.content)) def test_enabled(self): - """ - Test that the panels *do* load if the plugin is enabled - """ - + """Test that the panels *do* load if the plugin is enabled.""" plugin = registry.get_plugin('samplepanel') self.assertEqual(len(registry.with_mixin('panel', active=True)), 0) @@ -382,8 +408,7 @@ class PanelMixinTests(InvenTreeTestCase): self.assertEqual(Error.objects.count(), n_errors + len(urls)) def test_mixin(self): - """Test that ImplementationError is raised""" - + """Test that ImplementationError is raised.""" with self.assertRaises(MixinNotImplementedError): class Wrong(PanelMixin, InvenTreePlugin): pass diff --git a/InvenTree/stock/migrations/0069_auto_20211109_2347.py b/InvenTree/stock/migrations/0069_auto_20211109_2347.py index 748ac8d4cd..a874ba7b89 100644 --- a/InvenTree/stock/migrations/0069_auto_20211109_2347.py +++ b/InvenTree/stock/migrations/0069_auto_20211109_2347.py @@ -28,6 +28,9 @@ def update_serials(apps, schema_editor): except: serial = 0 + # Ensure the integer value is not too large for the database field + if serial > 0x7fffffff: + serial = 0x7fffffff item.serial_int = serial item.save() diff --git a/InvenTree/stock/test_migrations.py b/InvenTree/stock/test_migrations.py new file mode 100644 index 0000000000..07fa9f2fc7 --- /dev/null +++ b/InvenTree/stock/test_migrations.py @@ -0,0 +1,69 @@ +"""Unit tests for data migrations in the 'stock' app""" + +from django_test_migrations.contrib.unittest_case import MigratorTestCase + +from InvenTree import helpers + + +class TestSerialNumberMigration(MigratorTestCase): + """Test data migration which updates serial numbers""" + + migrate_from = ('stock', '0067_alter_stockitem_part') + migrate_to = ('stock', helpers.getNewestMigrationFile('stock')) + + def prepare(self): + """Create initial data for this migration""" + + Part = self.old_state.apps.get_model('part', 'part') + StockItem = self.old_state.apps.get_model('stock', 'stockitem') + + # Create a base part + my_part = Part.objects.create( + name='PART-123', + description='Some part', + active=True, + trackable=True, + level=0, + tree_id=0, + lft=0, rght=0 + ) + + # Create some serialized stock items + for sn in range(10, 20): + StockItem.objects.create( + part=my_part, + quantity=1, + serial=sn, + level=0, + tree_id=0, + lft=0, rght=0 + ) + + # Create a stock item with a very large serial number + item = StockItem.objects.create( + part=my_part, + quantity=1, + serial='9999999999999999999999999999999999999999999999999999999999999', + level=0, + tree_id=0, + lft=0, rght=0 + ) + + self.big_ref_pk = item.pk + + def test_migrations(self): + """Test that the migrations have been applied correctly""" + + StockItem = self.new_state.apps.get_model('stock', 'stockitem') + + # Check that the serial number integer conversion has been applied correctly + for sn in range(10, 20): + item = StockItem.objects.get(serial_int=sn) + + self.assertEqual(item.serial, str(sn)) + + big_ref_item = StockItem.objects.get(pk=self.big_ref_pk) + + # Check that the StockItem maximum serial number + self.assertEqual(big_ref_item.serial, '9999999999999999999999999999999999999999999999999999999999999') + self.assertEqual(big_ref_item.serial_int, 0x7fffffff)