diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index a1259c7a5d..2f99274bd4 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -910,6 +910,7 @@ if DEBUG or TESTING: # Plugin test settings PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested? PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing? +PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now PLUGIN_RETRY = get_setting('PLUGIN_RETRY', 5) # how often should plugin loading be tried? PLUGIN_FILE_CHECKED = False # Was the plugin file checked? diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index e2acfb5c36..41e6303a81 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -76,7 +76,7 @@ class BulkNotificationMethodTests(BaseNotificationIntegrationTest): def test_BulkNotificationMethod(self): """ Ensure the implementation requirements are tested. - NotImplementedError needs to raise if the send_bulk() method is not set. + MixinNotImplementedError needs to raise if the send_bulk() method is not set. """ class WrongImplementation(BulkNotificationMethod): @@ -94,7 +94,7 @@ class SingleNotificationMethodTests(BaseNotificationIntegrationTest): def test_SingleNotificationMethod(self): """ Ensure the implementation requirements are tested. - NotImplementedError needs to raise if the send() method is not set. + MixinNotImplementedError needs to raise if the send() method is not set. """ class WrongImplementation(SingleNotificationMethod): diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index d2294b4416..d870269736 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -26,9 +26,10 @@ def trigger_event(event, *args, **kwargs): if not settings.PLUGINS_ENABLED: # Do nothing if plugins are not enabled - return + return # pragma: no cover - if not canAppAccessDatabase(): + # Make sure the database can be accessed and is not beeing tested rn + if not canAppAccessDatabase() and not settings.PLUGIN_TESTING_EVENTS: logger.debug(f"Ignoring triggered event '{event}' - database not ready") return @@ -91,7 +92,7 @@ def process_event(plugin_slug, event, *args, **kwargs): plugin = registry.plugins.get(plugin_slug, None) - if plugin is None: + if plugin is None: # pragma: no cover logger.error(f"Could not find matching plugin for '{plugin_slug}'") return @@ -106,7 +107,7 @@ def allow_table_event(table_name): if isImportingData(): # Prevent table events during the data import process - return False + return False # pragma: no cover table_name = table_name.lower().strip() diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index fef2a51681..4cd2123ef3 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -541,7 +541,7 @@ class PanelMixin: def get_custom_panels(self, view, request): """ This method *must* be implemented by the plugin class """ - raise NotImplementedError(f"{__class__} is missing the 'get_custom_panels' method") + raise MixinNotImplementedError(f"{__class__} is missing the 'get_custom_panels' method") def get_panel_context(self, view, request, context): """ @@ -559,7 +559,7 @@ class PanelMixin: try: context['object'] = view.get_object() - except AttributeError: + except AttributeError: # pragma: no cover pass return context diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 9759e0f00d..e91f4da365 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -8,6 +8,7 @@ from error_report.models import Error from InvenTree.helpers import InvenTreeTestCase from plugin import InvenTreePlugin +from plugin.base.integration.mixins import PanelMixin from plugin.helpers import MixinNotImplementedError from plugin.mixins import (APICallMixin, AppMixin, NavigationMixin, SettingsMixin, UrlsMixin) @@ -324,7 +325,7 @@ class PanelMixinTests(InvenTreeTestCase): urls = [ reverse('part-detail', kwargs={'pk': 1}), reverse('stock-item-detail', kwargs={'pk': 2}), - reverse('stock-location-detail', kwargs={'pk': 1}), + reverse('stock-location-detail', kwargs={'pk': 2}), ] plugin.set_setting('ENABLE_HELLO_WORLD', False) @@ -379,3 +380,13 @@ class PanelMixinTests(InvenTreeTestCase): # Assert that each request threw an error self.assertEqual(Error.objects.count(), n_errors + len(urls)) + + def test_mixin(self): + """Test that ImplementationError is raised""" + + with self.assertRaises(MixinNotImplementedError): + class Wrong(PanelMixin, InvenTreePlugin): + pass + + plugin = Wrong() + plugin.get_custom_panels('abc', 'abc') diff --git a/InvenTree/plugin/base/locate/mixins.py b/InvenTree/plugin/base/locate/mixins.py index 3f91b998c5..5d804b70f2 100644 --- a/InvenTree/plugin/base/locate/mixins.py +++ b/InvenTree/plugin/base/locate/mixins.py @@ -2,7 +2,7 @@ import logging -from plugin.helpers import MixinImplementationError +from plugin.helpers import MixinNotImplementedError logger = logging.getLogger('inventree') @@ -58,7 +58,7 @@ class LocateMixin: if item.in_stock and item.location is not None: self.locate_stock_location(item.location.pk) - except StockItem.DoesNotExist: + except StockItem.DoesNotExist: # pragma: no cover logger.warning("LocateMixin: StockItem pk={item_pk} not found") pass @@ -71,4 +71,4 @@ class LocateMixin: Note: The default implementation here does nothing! """ - raise MixinImplementationError + raise MixinNotImplementedError diff --git a/InvenTree/plugin/base/locate/test_locate.py b/InvenTree/plugin/base/locate/test_locate.py index 361b791c4b..96dfca7cb1 100644 --- a/InvenTree/plugin/base/locate/test_locate.py +++ b/InvenTree/plugin/base/locate/test_locate.py @@ -5,7 +5,8 @@ Unit tests for the 'locate' plugin mixin class from django.urls import reverse from InvenTree.api_tester import InvenTreeAPITestCase -from plugin.registry import registry +from plugin import InvenTreePlugin, MixinNotImplementedError, registry +from plugin.base.locate.mixins import LocateMixin from stock.models import StockItem, StockLocation @@ -145,3 +146,17 @@ class LocatePluginTests(InvenTreeAPITestCase): # Item metadata should have been altered! self.assertTrue(location.metadata['located']) + + def test_mixin_locate(self): + """Test the sample mixin redirection""" + class SamplePlugin(LocateMixin, InvenTreePlugin): + pass + + plugin = SamplePlugin() + + # Test that the request is patched through to location + with self.assertRaises(MixinNotImplementedError): + plugin.locate_stock_item(1) + + # Test that it runs through + plugin.locate_stock_item(999) diff --git a/InvenTree/plugin/samples/event/event_sample.py b/InvenTree/plugin/samples/event/event_sample.py index bea21c3ea0..acddbf95c6 100644 --- a/InvenTree/plugin/samples/event/event_sample.py +++ b/InvenTree/plugin/samples/event/event_sample.py @@ -2,6 +2,10 @@ Sample plugin which responds to events """ +import warnings + +from django.conf import settings + from plugin import InvenTreePlugin from plugin.mixins import EventMixin @@ -21,3 +25,7 @@ class EventPluginSample(EventMixin, InvenTreePlugin): print(f"Processing triggered event: '{event}'") print("args:", str(args)) print("kwargs:", str(kwargs)) + + # Issue warning that we can test for + if settings.PLUGIN_TESTING: + warnings.warn(f'Event `{event}` triggered') diff --git a/InvenTree/plugin/samples/event/test_event_sample.py b/InvenTree/plugin/samples/event/test_event_sample.py new file mode 100644 index 0000000000..284b306a92 --- /dev/null +++ b/InvenTree/plugin/samples/event/test_event_sample.py @@ -0,0 +1,40 @@ +"""Unit tests for event_sample sample plugins""" + +from django.conf import settings +from django.test import TestCase + +from plugin import InvenTreePlugin, registry +from plugin.base.event.events import trigger_event +from plugin.helpers import MixinNotImplementedError +from plugin.mixins import EventMixin + + +class EventPluginSampleTests(TestCase): + """Tests for EventPluginSample""" + + def test_run_event(self): + """Check if the event is issued""" + # Activate plugin + config = registry.get_plugin('sampleevent').plugin_config() + config.active = True + config.save() + + # Enable event testing + settings.PLUGIN_TESTING_EVENTS = True + # Check that an event is issued + with self.assertWarns(Warning) as cm: + trigger_event('test.event') + self.assertEqual(cm.warning.args[0], 'Event `test.event` triggered') + + # Disable again + settings.PLUGIN_TESTING_EVENTS = False + + def test_mixin(self): + """Test that MixinNotImplementedError is raised""" + + with self.assertRaises(MixinNotImplementedError): + class Wrong(EventMixin, InvenTreePlugin): + pass + + plugin = Wrong() + plugin.process_event('abc') diff --git a/InvenTree/plugin/samples/locate/locate_sample.py b/InvenTree/plugin/samples/locate/locate_sample.py index 99242ead35..d4ce411098 100644 --- a/InvenTree/plugin/samples/locate/locate_sample.py +++ b/InvenTree/plugin/samples/locate/locate_sample.py @@ -37,7 +37,7 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): # Tag metadata item.set_metadata('located', True) - except (ValueError, StockItem.DoesNotExist): + except (ValueError, StockItem.DoesNotExist): # pragma: no cover logger.error(f"StockItem ID {item_pk} does not exist!") def locate_stock_location(self, location_pk): @@ -53,5 +53,5 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): # Tag metadata location.set_metadata('located', True) - except (ValueError, StockLocation.DoesNotExist): + except (ValueError, StockLocation.DoesNotExist): # pragma: no cover logger.error(f"Location ID {location_pk} does not exist!") diff --git a/InvenTree/plugin/samples/locate/test_locate_sample.py b/InvenTree/plugin/samples/locate/test_locate_sample.py new file mode 100644 index 0000000000..fe7ba28cca --- /dev/null +++ b/InvenTree/plugin/samples/locate/test_locate_sample.py @@ -0,0 +1,60 @@ +"""Unit tests for locate_sample sample plugins""" + +from django.urls import reverse + +from InvenTree.api_tester import InvenTreeAPITestCase +from plugin import InvenTreePlugin, registry +from plugin.helpers import MixinNotImplementedError +from plugin.mixins import LocateMixin + + +class SampleLocatePlugintests(InvenTreeAPITestCase): + """Tests for SampleLocatePlugin""" + + fixtures = [ + 'location', + 'category', + 'part', + 'stock' + ] + + def test_run_locator(self): + """Check if the event is issued""" + # Activate plugin + config = registry.get_plugin('samplelocate').plugin_config() + config.active = True + config.save() + + # Test APIs + url = reverse('api-locate-plugin') + + # No plugin + self.post(url, {}, expected_code=400) + + # Wrong plugin + self.post(url, {'plugin': 'sampleevent'}, expected_code=400) + + # Right plugin - no search item + self.post(url, {'plugin': 'samplelocate'}, expected_code=400) + + # Right plugin - wrong reference + self.post(url, {'plugin': 'samplelocate', 'item': 999}, expected_code=404) + + # Right plugin - right reference + self.post(url, {'plugin': 'samplelocate', 'item': 1}, expected_code=200) + + # Right plugin - wrong reference + self.post(url, {'plugin': 'samplelocate', 'location': 999}, expected_code=404) + + # Right plugin - right reference + self.post(url, {'plugin': 'samplelocate', 'location': 1}, expected_code=200) + + def test_mixin(self): + """Test that MixinNotImplementedError is raised""" + + with self.assertRaises(MixinNotImplementedError): + class Wrong(LocateMixin, InvenTreePlugin): + pass + + plugin = Wrong() + plugin.locate_stock_location(1)