mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 03:26:45 +00:00
* Use registry.get_plugin() - Instead of registry.plugins.get() - get_plugin checks registry hash - performs registry reload if necessary * Add PluginValidationMixin class - Allows the entire model to be validated via plugins - Called on model.full_clean() - Called on model.save() * Update Validation sample plugin * Fix for InvenTreeTree models * Refactor build.models - Expose models to plugin validation * Update stock.models * Update more models - common.models - company.models * Update more models - label.models - order.models - part.models * More model updates * Update docs * Fix for potential plugin edge case - plugin slug is globally unique - do not use get_or_create with two lookup fields - will throw an IntegrityError if you change the name of a plugin * Inherit DiffMixin into PluginValidationMixin - Allows us to pass model diffs through to validation - Plugins can validate based on what has *changed* * Update documentation * Add get_plugin_config helper function * Bug fix * Bug fix * Update plugin hash when calling set_plugin_state * Working on unit testing * More unit testing * Move get_plugin_config into registry.py * Move extract_int into InvenTree.helpers * Fix log formatting * Update model definitions - Ensure there are no changes to the migrations * Comment out format line * Fix access to get_plugin_config * Fix tests for SimpleActionPlugin * More unit test fixes
473 lines
15 KiB
Python
473 lines
15 KiB
Python
"""Unit tests for base mixins for plugins."""
|
|
|
|
import os
|
|
|
|
from django.conf import settings
|
|
from django.test import TestCase
|
|
from django.urls import include, path, re_path, reverse
|
|
|
|
from error_report.models import Error
|
|
|
|
from InvenTree.unit_test 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,
|
|
)
|
|
from plugin.registry import registry
|
|
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.values()},
|
|
)
|
|
# human name
|
|
self.assertIn(
|
|
self.MIXIN_HUMAN_NAME,
|
|
{item['human_name'] for item in self.mixin.registered_mixins.values()},
|
|
)
|
|
|
|
|
|
class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
|
|
"""Tests for SettingsMixin."""
|
|
|
|
MIXIN_HUMAN_NAME = 'Settings'
|
|
MIXIN_NAME = 'settings'
|
|
MIXIN_ENABLE_CHECK = 'has_settings'
|
|
|
|
TEST_SETTINGS = {'SETTING1': {'default': '123'}}
|
|
|
|
def setUp(self):
|
|
"""Setup for all tests."""
|
|
|
|
class SettingsCls(SettingsMixin, InvenTreePlugin):
|
|
SETTINGS = self.TEST_SETTINGS
|
|
|
|
self.mixin = SettingsCls()
|
|
|
|
class NoSettingsCls(SettingsMixin, InvenTreePlugin):
|
|
pass
|
|
|
|
self.mixin_nothing = NoSettingsCls()
|
|
|
|
super().setUp()
|
|
|
|
def test_function(self):
|
|
"""Test that the mixin functions."""
|
|
# settings variable
|
|
self.assertEqual(self.mixin.settings, self.TEST_SETTINGS)
|
|
|
|
# calling settings
|
|
# not existing
|
|
self.assertEqual(self.mixin.get_setting('ABCD'), '')
|
|
self.assertEqual(self.mixin_nothing.get_setting('ABCD'), '')
|
|
|
|
# right setting
|
|
self.mixin.set_setting('SETTING1', '12345', self.user)
|
|
self.assertEqual(self.mixin.get_setting('SETTING1'), '12345')
|
|
|
|
# no setting
|
|
self.assertEqual(self.mixin_nothing.get_setting(''), '')
|
|
|
|
|
|
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'
|
|
|
|
URLS = [path('testpath', test, name='test')]
|
|
|
|
self.mixin = UrlsCls()
|
|
|
|
class NoUrlsCls(UrlsMixin, InvenTreePlugin):
|
|
pass
|
|
|
|
self.mixin_nothing = NoUrlsCls()
|
|
|
|
def test_function(self):
|
|
"""Test that the mixin functions."""
|
|
plg_name = self.mixin.plugin_name()
|
|
|
|
# base_url
|
|
target_url = f'{PLUGIN_BASE}/{plg_name}/'
|
|
self.assertEqual(self.mixin.base_url, target_url)
|
|
|
|
# urlpattern
|
|
target_pattern = re_path(
|
|
f'^{plg_name}/', include((self.mixin.urls, plg_name)), name=plg_name
|
|
)
|
|
self.assertEqual(
|
|
self.mixin.urlpatterns.reverse_dict, target_pattern.reverse_dict
|
|
)
|
|
|
|
# resolve the view
|
|
self.assertEqual(self.mixin.urlpatterns.resolve('/testpath').func(), 'ccc')
|
|
self.assertEqual(self.mixin.urlpatterns.reverse('test'), 'testpath')
|
|
|
|
# no url
|
|
self.assertIsNone(self.mixin_nothing.urls)
|
|
self.assertIsNone(self.mixin_nothing.urlpatterns)
|
|
|
|
# internal name
|
|
self.assertEqual(self.mixin.internal_name, f'plugin:{self.mixin.slug}:')
|
|
|
|
|
|
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 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'}]
|
|
NAVIGATION_TAB_NAME = 'abcd1'
|
|
|
|
self.mixin = NavigationCls()
|
|
|
|
class NothingNavigationCls(NavigationMixin, InvenTreePlugin):
|
|
pass
|
|
|
|
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'}]
|
|
)
|
|
|
|
# navigation name
|
|
self.assertEqual(self.mixin.navigation_name, 'abcd1')
|
|
self.assertEqual(self.nothing_mixin.navigation_name, '')
|
|
|
|
def test_fail(self):
|
|
"""Test that wrong links fail."""
|
|
with self.assertRaises(NotImplementedError):
|
|
|
|
class NavigationCls(NavigationMixin, InvenTreePlugin):
|
|
NAVIGATION = ['aa', 'aa']
|
|
|
|
NavigationCls()
|
|
|
|
|
|
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'
|
|
|
|
SETTINGS = {
|
|
'API_TOKEN': {'name': 'API Token', 'protected': True},
|
|
'API_URL': {
|
|
'name': 'External URL',
|
|
'description': 'Where is your API located?',
|
|
'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('orgs/inventree', simple_response=simple)
|
|
|
|
self.mixin = MixinCls()
|
|
|
|
# If running in github workflow, make use of GITHUB_TOKEN
|
|
if settings.TESTING:
|
|
token = os.getenv('GITHUB_TOKEN', None)
|
|
|
|
if token:
|
|
self.mixin.set_setting('API_TOKEN', token)
|
|
|
|
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."""
|
|
# check init
|
|
self.assertTrue(self.mixin.has_api_call)
|
|
# api_url
|
|
self.assertEqual('https://api.github.com', self.mixin.api_url)
|
|
|
|
# api_headers
|
|
headers = self.mixin.api_headers
|
|
self.assertEqual(headers['Content-Type'], 'application/json')
|
|
|
|
def test_args(self):
|
|
"""Test that building up args work."""
|
|
# api_build_url_args
|
|
# 1 arg
|
|
result = self.mixin.api_build_url_args({'a': 'b'})
|
|
self.assertEqual(result, '?a=b')
|
|
# more args
|
|
result = self.mixin.api_build_url_args({'a': 'b', 'c': 'd'})
|
|
self.assertEqual(result, '?a=b&c=d')
|
|
# list args
|
|
result = self.mixin.api_build_url_args({'a': 'b', 'c': ['d', 'e', 'f']})
|
|
self.assertEqual(result, '?a=b&c=d,e,f')
|
|
|
|
def test_api_call(self):
|
|
"""Test that api calls work."""
|
|
# api_call
|
|
result = self.mixin.get_external_url()
|
|
self.assertTrue(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)
|
|
self.assertTrue(result)
|
|
self.assertEqual(result.reason, 'OK')
|
|
|
|
# api_call with post and data
|
|
result = self.mixin.api_call(
|
|
'https://reqres.in/api/users/',
|
|
json={'name': 'morpheus', 'job': 'leader'},
|
|
method='POST',
|
|
endpoint_is_url=True,
|
|
)
|
|
|
|
self.assertTrue(result)
|
|
self.assertEqual(result['name'], 'morpheus')
|
|
|
|
# api_call with endpoint with leading slash
|
|
result = self.mixin.api_call('/orgs/inventree', simple_response=False)
|
|
self.assertTrue(result)
|
|
self.assertEqual(result.reason, 'OK')
|
|
|
|
# api_call with filter
|
|
result = self.mixin.api_call(
|
|
'repos/inventree/InvenTree/stargazers', url_args={'page': '2'}
|
|
)
|
|
self.assertTrue(result)
|
|
|
|
def test_function_errors(self):
|
|
"""Test function errors."""
|
|
# wrongly defined plugins should not load
|
|
with self.assertRaises(MixinNotImplementedError):
|
|
self.mixin_wrong.has_api_call()
|
|
|
|
# cover wrong token setting
|
|
with self.assertRaises(MixinNotImplementedError):
|
|
self.mixin_wrong2.has_api_call()
|
|
|
|
# Too many data arguments
|
|
with self.assertRaises(ValueError):
|
|
self.mixin.api_call(
|
|
'https://reqres.in/api/users/', json={'a': 1}, data={'a': 1}
|
|
)
|
|
|
|
# Sending a request with a wrong data format should result in 40
|
|
result = self.mixin.api_call(
|
|
'https://reqres.in/api/users/',
|
|
data={'name': 'morpheus', 'job': 'leader'},
|
|
method='POST',
|
|
endpoint_is_url=True,
|
|
simple_response=False,
|
|
)
|
|
|
|
self.assertEqual(result.status_code, 400)
|
|
self.assertIn('Bad Request', str(result.content))
|
|
|
|
|
|
class PanelMixinTests(InvenTreeTestCase):
|
|
"""Test that the PanelMixin plugin operates correctly."""
|
|
|
|
fixtures = ['category', 'part', 'location', 'stock']
|
|
|
|
roles = 'all'
|
|
|
|
def test_installed(self):
|
|
"""Test that the sample panel plugin is installed."""
|
|
plugins = registry.with_mixin('panel')
|
|
|
|
self.assertTrue(len(plugins) == 0)
|
|
|
|
# Now enable the plugin
|
|
registry.set_plugin_state('samplepanel', True)
|
|
plugins = registry.with_mixin('panel')
|
|
|
|
self.assertIn('samplepanel', [p.slug for p in plugins])
|
|
|
|
# Find 'inactive' plugins (should be None)
|
|
plugins = registry.with_mixin('panel', active=False)
|
|
self.assertEqual(len(plugins), 0)
|
|
|
|
def test_disabled(self):
|
|
"""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)
|
|
plugin.set_setting('ENABLE_BROKEN_PANEL', True)
|
|
|
|
# Ensure that the plugin is *not* enabled
|
|
config = plugin.plugin_config()
|
|
|
|
self.assertFalse(config.active)
|
|
|
|
# Load some pages, ensure that the panel content is *not* loaded
|
|
for url in [
|
|
reverse('part-detail', kwargs={'pk': 1}),
|
|
reverse('stock-item-detail', kwargs={'pk': 2}),
|
|
reverse('stock-location-detail', kwargs={'pk': 1}),
|
|
]:
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Test that these panels have *not* been loaded
|
|
self.assertNotIn('No Content', str(response.content))
|
|
self.assertNotIn('Hello world', str(response.content))
|
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
|
|
|
def test_enabled(self):
|
|
"""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)
|
|
|
|
# Ensure that the plugin is enabled
|
|
config = plugin.plugin_config()
|
|
config.active = True
|
|
config.save()
|
|
|
|
self.assertTrue(config.active)
|
|
self.assertEqual(len(registry.with_mixin('panel', active=True)), 1)
|
|
|
|
# Load some pages, ensure that the panel content is *not* loaded
|
|
urls = [
|
|
reverse('part-detail', kwargs={'pk': 1}),
|
|
reverse('stock-item-detail', kwargs={'pk': 2}),
|
|
reverse('stock-location-detail', kwargs={'pk': 2}),
|
|
]
|
|
|
|
plugin.set_setting('ENABLE_HELLO_WORLD', False)
|
|
plugin.set_setting('ENABLE_BROKEN_PANEL', False)
|
|
|
|
for url in urls:
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.assertIn('No Content', str(response.content))
|
|
|
|
# This panel is disabled by plugin setting
|
|
self.assertNotIn('Hello world!', str(response.content))
|
|
|
|
# This panel is only active for the "Part" view
|
|
if url == urls[0]:
|
|
self.assertIn('Custom Part Panel', str(response.content))
|
|
else:
|
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
|
|
|
# Enable the 'Hello World' panel
|
|
plugin.set_setting('ENABLE_HELLO_WORLD', True)
|
|
|
|
for url in urls:
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.assertIn('Hello world!', str(response.content))
|
|
|
|
# The 'Custom Part' panel should still be there, too
|
|
if url == urls[0]:
|
|
self.assertIn('Custom Part Panel', str(response.content))
|
|
else:
|
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
|
|
|
# Enable the 'broken panel' setting - this will cause all panels to not render
|
|
plugin.set_setting('ENABLE_BROKEN_PANEL', True)
|
|
|
|
n_errors = Error.objects.count()
|
|
|
|
for url in urls:
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# No custom panels should have been loaded
|
|
self.assertNotIn('No Content', str(response.content))
|
|
self.assertNotIn('Hello world!', str(response.content))
|
|
self.assertNotIn('Broken Panel', str(response.content))
|
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
|
|
|
# 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')
|