From a4a0b146c2646b67adb65a10c3d1b05c6b70a9da Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Apr 2020 13:23:29 -0400 Subject: [PATCH 1/7] Establish a separate configuration file for testing --- netbox/netbox/configuration.testing.py | 36 ++++++++++++++++++++++++++ scripts/cibuild.sh | 6 ++--- 2 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 netbox/netbox/configuration.testing.py diff --git a/netbox/netbox/configuration.testing.py b/netbox/netbox/configuration.testing.py new file mode 100644 index 000000000..8d66515bb --- /dev/null +++ b/netbox/netbox/configuration.testing.py @@ -0,0 +1,36 @@ +################################################################### +# This file serves as a base configuration for testing purposes # +# only. It is not intended for production use. # +################################################################### + +ALLOWED_HOSTS = ['*'] + +DATABASE = { + 'NAME': 'netbox', + 'USER': '', + 'PASSWORD': '', + 'HOST': 'localhost', + 'PORT': '', + 'CONN_MAX_AGE': 300, +} + +REDIS = { + 'tasks': { + 'HOST': 'localhost', + 'PORT': 6379, + 'PASSWORD': '', + 'DATABASE': 0, + 'DEFAULT_TIMEOUT': 300, + 'SSL': False, + }, + 'caching': { + 'HOST': 'localhost', + 'PORT': 6379, + 'PASSWORD': '', + 'DATABASE': 1, + 'DEFAULT_TIMEOUT': 300, + 'SSL': False, + } +} + +SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' diff --git a/scripts/cibuild.sh b/scripts/cibuild.sh index 282000b0a..21ccfef67 100755 --- a/scripts/cibuild.sh +++ b/scripts/cibuild.sh @@ -34,11 +34,9 @@ if [[ $RC != 0 ]]; then EXIT=$RC fi -# Prepare configuration file for use in CI +# Point to the testing configuration file for use in CI CONFIG="netbox/netbox/configuration.py" -cp netbox/netbox/configuration.example.py $CONFIG -sed -i -e "s/ALLOWED_HOSTS = \[\]/ALLOWED_HOSTS = \['*'\]/g" $CONFIG -sed -i -e "s/SECRET_KEY = ''/SECRET_KEY = 'netboxci'/g" $CONFIG +ln -s netbox/netbox/configuration.testing.py $CONFIG # Run NetBox tests coverage run --source="netbox/" netbox/manage.py test netbox/ From 7a16d49b3eb1c827a46fd3f1f82e8caff5b43345 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Apr 2020 13:29:54 -0400 Subject: [PATCH 2/7] Correct path to test configuration file --- scripts/cibuild.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/cibuild.sh b/scripts/cibuild.sh index 21ccfef67..6a0422308 100755 --- a/scripts/cibuild.sh +++ b/scripts/cibuild.sh @@ -35,8 +35,7 @@ if [[ $RC != 0 ]]; then fi # Point to the testing configuration file for use in CI -CONFIG="netbox/netbox/configuration.py" -ln -s netbox/netbox/configuration.testing.py $CONFIG +ln -s configuration.testing.py netbox/netbox/configuration.py # Run NetBox tests coverage run --source="netbox/" netbox/manage.py test netbox/ From cb38efbc1fe9e89963eb6182942e1d4025595a86 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Apr 2020 17:08:47 -0400 Subject: [PATCH 3/7] Initial implementation of tests for plugins framework --- netbox/extras/plugins/urls.py | 3 +- netbox/extras/tests/dummy_plugin/__init__.py | 15 ++++ netbox/extras/tests/dummy_plugin/admin.py | 9 +++ .../tests/dummy_plugin/api/serializers.py | 9 +++ netbox/extras/tests/dummy_plugin/api/urls.py | 6 ++ netbox/extras/tests/dummy_plugin/api/views.py | 8 ++ .../extras/tests/dummy_plugin/middleware.py | 7 ++ .../dummy_plugin/migrations/0001_initial.py | 23 ++++++ .../tests/dummy_plugin/migrations/__init__.py | 0 netbox/extras/tests/dummy_plugin/models.py | 13 ++++ .../extras/tests/dummy_plugin/navigation.py | 28 +++++++ .../tests/dummy_plugin/template_content.py | 20 +++++ netbox/extras/tests/dummy_plugin/urls.py | 8 ++ netbox/extras/tests/dummy_plugin/views.py | 11 +++ netbox/extras/tests/test_plugins.py | 74 +++++++++++++++++++ netbox/netbox/configuration.testing.py | 4 + 16 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 netbox/extras/tests/dummy_plugin/__init__.py create mode 100644 netbox/extras/tests/dummy_plugin/admin.py create mode 100644 netbox/extras/tests/dummy_plugin/api/serializers.py create mode 100644 netbox/extras/tests/dummy_plugin/api/urls.py create mode 100644 netbox/extras/tests/dummy_plugin/api/views.py create mode 100644 netbox/extras/tests/dummy_plugin/middleware.py create mode 100644 netbox/extras/tests/dummy_plugin/migrations/0001_initial.py create mode 100644 netbox/extras/tests/dummy_plugin/migrations/__init__.py create mode 100644 netbox/extras/tests/dummy_plugin/models.py create mode 100644 netbox/extras/tests/dummy_plugin/navigation.py create mode 100644 netbox/extras/tests/dummy_plugin/template_content.py create mode 100644 netbox/extras/tests/dummy_plugin/urls.py create mode 100644 netbox/extras/tests/dummy_plugin/views.py create mode 100644 netbox/extras/tests/test_plugins.py diff --git a/netbox/extras/plugins/urls.py b/netbox/extras/plugins/urls.py index f3f591cb0..1677692cf 100644 --- a/netbox/extras/plugins/urls.py +++ b/netbox/extras/plugins/urls.py @@ -19,7 +19,8 @@ plugin_admin_patterns = [ # Register base/API URL patterns for each plugin for plugin in settings.PLUGINS: - app = apps.get_app_config(plugin) + plugin_name = plugin.split('.')[-1] + app = apps.get_app_config(plugin_name) base_url = getattr(app, 'base_url') or app.label # Check if the plugin specifies any base URLs diff --git a/netbox/extras/tests/dummy_plugin/__init__.py b/netbox/extras/tests/dummy_plugin/__init__.py new file mode 100644 index 000000000..eaaff4f5e --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/__init__.py @@ -0,0 +1,15 @@ +from extras.plugins import PluginConfig + + +class DummyPluginConfig(PluginConfig): + name = 'extras.tests.dummy_plugin' + verbose_name = 'Dummy plugin' + version = '0.0' + description = 'For testing purposes only' + base_url = 'dummy-plugin' + middleware = [ + 'extras.tests.dummy_plugin.middleware.DummyMiddleware' + ] + + +config = DummyPluginConfig diff --git a/netbox/extras/tests/dummy_plugin/admin.py b/netbox/extras/tests/dummy_plugin/admin.py new file mode 100644 index 000000000..d6d2233e5 --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from netbox.admin import admin_site +from .models import DummyModel + + +@admin.register(DummyModel, site=admin_site) +class AnimalAdmin(admin.ModelAdmin): + list_display = ('name', 'number') diff --git a/netbox/extras/tests/dummy_plugin/api/serializers.py b/netbox/extras/tests/dummy_plugin/api/serializers.py new file mode 100644 index 000000000..c0f89fd0f --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/api/serializers.py @@ -0,0 +1,9 @@ +from rest_framework.serializers import ModelSerializer +from extras.tests.dummy_plugin.models import DummyModel + + +class DummySerializer(ModelSerializer): + + class Meta: + model = DummyModel + fields = ('id', 'name', 'sound') diff --git a/netbox/extras/tests/dummy_plugin/api/urls.py b/netbox/extras/tests/dummy_plugin/api/urls.py new file mode 100644 index 000000000..d6c27809b --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/api/urls.py @@ -0,0 +1,6 @@ +from rest_framework import routers +from .views import DummyViewSet + +router = routers.DefaultRouter() +router.register('dummy-models', DummyViewSet) +urlpatterns = router.urls diff --git a/netbox/extras/tests/dummy_plugin/api/views.py b/netbox/extras/tests/dummy_plugin/api/views.py new file mode 100644 index 000000000..1977ec2af --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/api/views.py @@ -0,0 +1,8 @@ +from rest_framework.viewsets import ModelViewSet +from extras.tests.dummy_plugin.models import DummyModel +from .serializers import DummySerializer + + +class DummyViewSet(ModelViewSet): + queryset = DummyModel.objects.all() + serializer_class = DummySerializer diff --git a/netbox/extras/tests/dummy_plugin/middleware.py b/netbox/extras/tests/dummy_plugin/middleware.py new file mode 100644 index 000000000..97592c3b2 --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/middleware.py @@ -0,0 +1,7 @@ +class DummyMiddleware: + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + return self.get_response(request) diff --git a/netbox/extras/tests/dummy_plugin/migrations/0001_initial.py b/netbox/extras/tests/dummy_plugin/migrations/0001_initial.py new file mode 100644 index 000000000..4342d9576 --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/migrations/0001_initial.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='DummyModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=20)), + ('number', models.IntegerField(default=100)), + ], + options={ + 'ordering': ['name'], + }, + ), + ] diff --git a/netbox/extras/tests/dummy_plugin/migrations/__init__.py b/netbox/extras/tests/dummy_plugin/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/extras/tests/dummy_plugin/models.py b/netbox/extras/tests/dummy_plugin/models.py new file mode 100644 index 000000000..9bd39a46b --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/models.py @@ -0,0 +1,13 @@ +from django.db import models + + +class DummyModel(models.Model): + name = models.CharField( + max_length=20 + ) + number = models.IntegerField( + default=100 + ) + + class Meta: + ordering = ['name'] diff --git a/netbox/extras/tests/dummy_plugin/navigation.py b/netbox/extras/tests/dummy_plugin/navigation.py new file mode 100644 index 000000000..028f0884f --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/navigation.py @@ -0,0 +1,28 @@ +from extras.plugins import PluginMenuButton, PluginMenuItem +from utilities.choices import ButtonColorChoices + + +menu_items = ( + PluginMenuItem( + link='plugins:dummy_plugin:dummy_models', + link_text='Item 1', + buttons=( + PluginMenuButton( + link='plugins:netbox_animal_sounds:random_animal', + title='Random animal', + icon_class='fa-question' + ), + PluginMenuButton( + link='admin:netbox_animal_sounds_animal_add', + title='Add a new animal', + icon_class='fa-plus', + color=ButtonColorChoices.GREEN, + permissions=['netbox_animal_sounds.add_animal'] + ), + ) + ), + PluginMenuItem( + link='plugins:dummy_plugin:dummy_models', + link_text='Item 2', + ), +) diff --git a/netbox/extras/tests/dummy_plugin/template_content.py b/netbox/extras/tests/dummy_plugin/template_content.py new file mode 100644 index 000000000..fed17ca0b --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/template_content.py @@ -0,0 +1,20 @@ +from extras.plugins import PluginTemplateExtension + + +class SiteContent(PluginTemplateExtension): + model = 'dcim.site' + + def left_page(self): + return "SITE CONTENT - LEFT PAGE" + + def right_page(self): + return "SITE CONTENT - RIGHT PAGE" + + def full_width_page(self): + return "SITE CONTENT - FULL WIDTH PAGE" + + def full_buttons(self): + return "SITE CONTENT - BUTTONS" + + +template_extensions = [SiteContent] diff --git a/netbox/extras/tests/dummy_plugin/urls.py b/netbox/extras/tests/dummy_plugin/urls.py new file mode 100644 index 000000000..053a7443e --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + + +urlpatterns = ( + path('models/', views.DummyModelsView.as_view(), name='dummy_models'), +) diff --git a/netbox/extras/tests/dummy_plugin/views.py b/netbox/extras/tests/dummy_plugin/views.py new file mode 100644 index 000000000..4512758df --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/views.py @@ -0,0 +1,11 @@ +from django.http import HttpResponse +from django.views.generic import View + +from .models import DummyModel + + +class DummyModelsView(View): + + def get(self, request): + instance_count = DummyModel.objects.count() + return HttpResponse(f"Instances: {instance_count}") diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py new file mode 100644 index 000000000..6134fbd82 --- /dev/null +++ b/netbox/extras/tests/test_plugins.py @@ -0,0 +1,74 @@ +from django.conf import settings +from django.test import Client, TestCase +from django.urls import reverse + +from extras.registry import registry +from extras.tests.dummy_plugin.models import DummyModel +from extras.tests.dummy_plugin.template_content import SiteContent + + +class PluginTest(TestCase): + + def test_config(self): + + self.assertIn('extras.tests.dummy_plugin.DummyPluginConfig', settings.INSTALLED_APPS) + + def test_models(self): + + # Test saving an instance + instance = DummyModel(name='Instance 1', number=100) + instance.save() + self.assertIsNotNone(instance.pk) + + # Test deleting an instance + instance.delete() + self.assertIsNone(instance.pk) + + def test_admin(self): + + # Test admin view URL resolution + url = reverse('admin:dummy_plugin_dummymodel_add') + self.assertEqual(url, '/admin/dummy_plugin/dummymodel/add/') + + def test_views(self): + + # Test URL resolution + url = reverse('plugins:dummy_plugin:dummy_models') + self.assertEqual(url, '/plugins/dummy-plugin/models/') + + # Test GET request + client = Client() + response = client.get(url) + self.assertEqual(response.status_code, 200) + + def test_api_views(self): + + # Test URL resolution + url = reverse('plugins-api:dummy_plugin-api:dummymodel-list') + self.assertEqual(url, '/api/plugins/dummy-plugin/dummy-models/') + + # Test GET request + client = Client() + response = client.get(url) + self.assertEqual(response.status_code, 200) + + def test_menu_items(self): + """ + Check that plugin MenuItems and MenuButtons are registered. + """ + self.assertIn('Dummy plugin', registry['plugin_menu_items']) + menu_items = registry['plugin_menu_items']['Dummy plugin'] + self.assertEqual(len(menu_items), 2) + self.assertEqual(len(menu_items[0].buttons), 2) + + def test_template_extensions(self): + """ + Check that plugin TemplateExtensions are registered. + """ + self.assertIn(SiteContent, registry['plugin_template_extensions']['dcim.site']) + + def test_middleware(self): + """ + Check that plugin middleware is registered. + """ + self.assertIn('extras.tests.dummy_plugin.middleware.DummyMiddleware', settings.MIDDLEWARE) diff --git a/netbox/netbox/configuration.testing.py b/netbox/netbox/configuration.testing.py index 8d66515bb..3cade747f 100644 --- a/netbox/netbox/configuration.testing.py +++ b/netbox/netbox/configuration.testing.py @@ -14,6 +14,10 @@ DATABASE = { 'CONN_MAX_AGE': 300, } +PLUGINS = [ + 'extras.tests.dummy_plugin' +] + REDIS = { 'tasks': { 'HOST': 'localhost', From e9d07551729dd0af22324d77d666a4076af60b70 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Apr 2020 17:18:15 -0400 Subject: [PATCH 4/7] Remove errant references to external plugin --- netbox/extras/tests/dummy_plugin/admin.py | 2 +- netbox/extras/tests/dummy_plugin/navigation.py | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/netbox/extras/tests/dummy_plugin/admin.py b/netbox/extras/tests/dummy_plugin/admin.py index d6d2233e5..83bc22ad8 100644 --- a/netbox/extras/tests/dummy_plugin/admin.py +++ b/netbox/extras/tests/dummy_plugin/admin.py @@ -5,5 +5,5 @@ from .models import DummyModel @admin.register(DummyModel, site=admin_site) -class AnimalAdmin(admin.ModelAdmin): +class DummyModelAdmin(admin.ModelAdmin): list_display = ('name', 'number') diff --git a/netbox/extras/tests/dummy_plugin/navigation.py b/netbox/extras/tests/dummy_plugin/navigation.py index 028f0884f..75b500916 100644 --- a/netbox/extras/tests/dummy_plugin/navigation.py +++ b/netbox/extras/tests/dummy_plugin/navigation.py @@ -1,5 +1,4 @@ from extras.plugins import PluginMenuButton, PluginMenuItem -from utilities.choices import ButtonColorChoices menu_items = ( @@ -8,16 +7,14 @@ menu_items = ( link_text='Item 1', buttons=( PluginMenuButton( - link='plugins:netbox_animal_sounds:random_animal', - title='Random animal', - icon_class='fa-question' + link='admin:dummy_plugin_dummymodel_add', + title='Add a new dummy model', + icon_class='fa-plus', ), PluginMenuButton( - link='admin:netbox_animal_sounds_animal_add', - title='Add a new animal', + link='admin:dummy_plugin_dummymodel_add', + title='Add a new dummy model', icon_class='fa-plus', - color=ButtonColorChoices.GREEN, - permissions=['netbox_animal_sounds.add_animal'] ), ) ), From 65c4fc09b01de4b44802f8681e901f2783589032 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Apr 2020 15:11:19 -0400 Subject: [PATCH 5/7] Fix CI tests --- netbox/extras/tests/dummy_plugin/api/serializers.py | 2 +- netbox/extras/tests/test_plugins.py | 3 ++- netbox/netbox/configuration.testing.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/netbox/extras/tests/dummy_plugin/api/serializers.py b/netbox/extras/tests/dummy_plugin/api/serializers.py index c0f89fd0f..101786168 100644 --- a/netbox/extras/tests/dummy_plugin/api/serializers.py +++ b/netbox/extras/tests/dummy_plugin/api/serializers.py @@ -6,4 +6,4 @@ class DummySerializer(ModelSerializer): class Meta: model = DummyModel - fields = ('id', 'name', 'sound') + fields = ('id', 'name', 'number') diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index 6134fbd82..83a7e33ed 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.test import Client, TestCase +from django.test import Client, TestCase, override_settings from django.urls import reverse from extras.registry import registry @@ -41,6 +41,7 @@ class PluginTest(TestCase): response = client.get(url) self.assertEqual(response.status_code, 200) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_api_views(self): # Test URL resolution diff --git a/netbox/netbox/configuration.testing.py b/netbox/netbox/configuration.testing.py index 3cade747f..09d5362ab 100644 --- a/netbox/netbox/configuration.testing.py +++ b/netbox/netbox/configuration.testing.py @@ -15,7 +15,7 @@ DATABASE = { } PLUGINS = [ - 'extras.tests.dummy_plugin' + 'extras.tests.dummy_plugin', ] REDIS = { From 3ada8feeb534d0d34c98767af307976525f52079 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Apr 2020 15:43:23 -0400 Subject: [PATCH 6/7] Naming tweaks --- netbox/extras/plugins/urls.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/extras/plugins/urls.py b/netbox/extras/plugins/urls.py index 1677692cf..b4360dc9e 100644 --- a/netbox/extras/plugins/urls.py +++ b/netbox/extras/plugins/urls.py @@ -18,14 +18,14 @@ plugin_admin_patterns = [ ] # Register base/API URL patterns for each plugin -for plugin in settings.PLUGINS: - plugin_name = plugin.split('.')[-1] +for plugin_path in settings.PLUGINS: + plugin_name = plugin_path.split('.')[-1] app = apps.get_app_config(plugin_name) base_url = getattr(app, 'base_url') or app.label # Check if the plugin specifies any base URLs try: - urlpatterns = import_string(f"{plugin}.urls.urlpatterns") + urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns") plugin_patterns.append( path(f"{base_url}/", include((urlpatterns, app.label))) ) @@ -34,7 +34,7 @@ for plugin in settings.PLUGINS: # Check if the plugin specifies any API URLs try: - urlpatterns = import_string(f"{plugin}.api.urls.urlpatterns") + urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns") plugin_api_patterns.append( path(f"{base_url}/", include((urlpatterns, f"{app.label}-api"))) ) From 446188b6f967808bd58ef19ac621878e591d7545 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Apr 2020 16:13:15 -0400 Subject: [PATCH 7/7] Skip PluginTest if dummy_plugin not in PLUGINS list --- netbox/extras/tests/test_plugins.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index 83a7e33ed..dba6308b9 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -1,12 +1,13 @@ +from unittest import skipIf + from django.conf import settings from django.test import Client, TestCase, override_settings from django.urls import reverse from extras.registry import registry -from extras.tests.dummy_plugin.models import DummyModel -from extras.tests.dummy_plugin.template_content import SiteContent +@skipIf('extras.tests.dummy_plugin.DummyPluginConfig' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS") class PluginTest(TestCase): def test_config(self): @@ -14,6 +15,7 @@ class PluginTest(TestCase): self.assertIn('extras.tests.dummy_plugin.DummyPluginConfig', settings.INSTALLED_APPS) def test_models(self): + from extras.tests.dummy_plugin.models import DummyModel # Test saving an instance instance = DummyModel(name='Instance 1', number=100) @@ -66,6 +68,8 @@ class PluginTest(TestCase): """ Check that plugin TemplateExtensions are registered. """ + from extras.tests.dummy_plugin.template_content import SiteContent + self.assertIn(SiteContent, registry['plugin_template_extensions']['dcim.site']) def test_middleware(self):