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',