Compare commits

...

1 Commits

Author SHA1 Message Date
Martin Hauser
209c60ea6e test(tables): Add reusable OrderableColumnsTestCase
Introduce `TableTestCases.OrderableColumnsTestCase`, a shared base class
that automatically discovers sortable columns from list-view querysets
and verifies each renders without exceptions in both ascending and
descending order.

Add per-table smoke tests across circuits, core, dcim, extras, ipam,
tenancy, users, virtualization, vpn, and wireless apps.

Fixes #21766
2026-04-03 15:01:57 +02:00
12 changed files with 689 additions and 103 deletions

View File

@@ -1,48 +1,46 @@
from django.test import RequestFactory, TestCase, tag
from circuits.models import CircuitGroupAssignment, CircuitTermination
from circuits.tables import CircuitGroupAssignmentTable, CircuitTerminationTable
from circuits.tables import *
from utilities.testing import TableTestCases
@tag('regression')
class CircuitTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = CircuitTermination.objects.all()
disallowed = {
'actions',
}
orderable_columns = [
column.name
for column in CircuitTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = CircuitTerminationTable(terminations)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class CircuitTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitTypeTable
@tag('regression')
class CircuitGroupAssignmentTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
assignment = CircuitGroupAssignment.objects.all()
disallowed = {
'actions',
}
class CircuitTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitTable
orderable_columns = [
column.name
for column in CircuitGroupAssignmentTable(assignment).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = CircuitGroupAssignmentTable(assignment)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class CircuitTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitTerminationTable
class CircuitGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitGroupTable
class CircuitGroupAssignmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = CircuitGroupAssignmentTable
class ProviderTableTest(TableTestCases.OrderableColumnsTestCase):
table = ProviderTable
class ProviderAccountTableTest(TableTestCases.OrderableColumnsTestCase):
table = ProviderAccountTable
class ProviderNetworkTableTest(TableTestCases.OrderableColumnsTestCase):
table = ProviderNetworkTable
class VirtualCircuitTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualCircuitTypeTable
class VirtualCircuitTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualCircuitTable
class VirtualCircuitTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualCircuitTerminationTable

View File

@@ -0,0 +1,26 @@
from core.models import ObjectChange
from core.tables import *
from utilities.testing import TableTestCases
class DataSourceTableTest(TableTestCases.OrderableColumnsTestCase):
table = DataSourceTable
class DataFileTableTest(TableTestCases.OrderableColumnsTestCase):
table = DataFileTable
class JobTableTest(TableTestCases.OrderableColumnsTestCase):
table = JobTable
class ObjectChangeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ObjectChangeTable
queryset_sources = [
('ObjectChangeListView', ObjectChange.objects.valid_models()),
]
class ConfigRevisionTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigRevisionTable

View File

@@ -0,0 +1,204 @@
from dcim.models import ConsolePort, Interface, PowerPort
from dcim.tables import *
from utilities.testing import TableTestCases
#
# Sites
#
class RegionTableTest(TableTestCases.OrderableColumnsTestCase):
table = RegionTable
class SiteGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = SiteGroupTable
class SiteTableTest(TableTestCases.OrderableColumnsTestCase):
table = SiteTable
class LocationTableTest(TableTestCases.OrderableColumnsTestCase):
table = LocationTable
#
# Racks
#
class RackRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackRoleTable
class RackTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackTypeTable
class RackTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackTable
class RackReservationTableTest(TableTestCases.OrderableColumnsTestCase):
table = RackReservationTable
#
# Device types
#
class ManufacturerTableTest(TableTestCases.OrderableColumnsTestCase):
table = ManufacturerTable
class DeviceTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceTypeTable
#
# Module types
#
class ModuleTypeProfileTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleTypeProfileTable
class ModuleTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleTypeTable
class ModuleTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleTable
#
# Devices
#
class DeviceRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceRoleTable
class PlatformTableTest(TableTestCases.OrderableColumnsTestCase):
table = PlatformTable
class DeviceTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceTable
#
# Device components
#
class ConsolePortTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConsolePortTable
class ConsoleServerPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConsoleServerPortTable
class PowerPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerPortTable
class PowerOutletTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerOutletTable
class InterfaceTableTest(TableTestCases.OrderableColumnsTestCase):
table = InterfaceTable
class FrontPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = FrontPortTable
class RearPortTableTest(TableTestCases.OrderableColumnsTestCase):
table = RearPortTable
class ModuleBayTableTest(TableTestCases.OrderableColumnsTestCase):
table = ModuleBayTable
class DeviceBayTableTest(TableTestCases.OrderableColumnsTestCase):
table = DeviceBayTable
class InventoryItemTableTest(TableTestCases.OrderableColumnsTestCase):
table = InventoryItemTable
class InventoryItemRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = InventoryItemRoleTable
#
# Connections
#
class ConsoleConnectionTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConsoleConnectionTable
queryset_sources = [
('ConsoleConnectionsListView', ConsolePort.objects.filter(_path__is_complete=True)),
]
class PowerConnectionTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerConnectionTable
queryset_sources = [
('PowerConnectionsListView', PowerPort.objects.filter(_path__is_complete=True)),
]
class InterfaceConnectionTableTest(TableTestCases.OrderableColumnsTestCase):
table = InterfaceConnectionTable
queryset_sources = [
('InterfaceConnectionsListView', Interface.objects.filter(_path__is_complete=True)),
]
#
# Cables
#
class CableTableTest(TableTestCases.OrderableColumnsTestCase):
table = CableTable
#
# Power
#
class PowerPanelTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerPanelTable
class PowerFeedTableTest(TableTestCases.OrderableColumnsTestCase):
table = PowerFeedTable
#
# Virtual chassis
#
class VirtualChassisTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualChassisTable
#
# Virtual device contexts
#
class VirtualDeviceContextTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualDeviceContextTable
#
# MAC addresses
#
class MACAddressTableTest(TableTestCases.OrderableColumnsTestCase):
table = MACAddressTable

View File

@@ -1,24 +1,84 @@
from django.test import RequestFactory, TestCase, tag
from extras.models import EventRule
from extras.tables import EventRuleTable
from extras.models import Bookmark, Notification, Subscription
from extras.tables import *
from utilities.testing import TableTestCases
@tag('regression')
class EventRuleTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
rule = EventRule.objects.all()
disallowed = {
'actions',
}
class CustomFieldTableTest(TableTestCases.OrderableColumnsTestCase):
table = CustomFieldTable
orderable_columns = [
column.name for column in EventRuleTable(rule).columns if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = EventRuleTable(rule)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class CustomFieldChoiceSetTableTest(TableTestCases.OrderableColumnsTestCase):
table = CustomFieldChoiceSetTable
class CustomLinkTableTest(TableTestCases.OrderableColumnsTestCase):
table = CustomLinkTable
class ExportTemplateTableTest(TableTestCases.OrderableColumnsTestCase):
table = ExportTemplateTable
class SavedFilterTableTest(TableTestCases.OrderableColumnsTestCase):
table = SavedFilterTable
class TableConfigTableTest(TableTestCases.OrderableColumnsTestCase):
table = TableConfigTable
class BookmarkTableTest(TableTestCases.OrderableColumnsTestCase):
table = BookmarkTable
queryset_sources = [
('BookmarkListView', Bookmark.objects.all()),
]
class NotificationGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = NotificationGroupTable
class NotificationTableTest(TableTestCases.OrderableColumnsTestCase):
table = NotificationTable
queryset_sources = [
('NotificationListView', Notification.objects.all()),
]
class SubscriptionTableTest(TableTestCases.OrderableColumnsTestCase):
table = SubscriptionTable
queryset_sources = [
('SubscriptionListView', Subscription.objects.all()),
]
class WebhookTableTest(TableTestCases.OrderableColumnsTestCase):
table = WebhookTable
class EventRuleTableTest(TableTestCases.OrderableColumnsTestCase):
table = EventRuleTable
class TagTableTest(TableTestCases.OrderableColumnsTestCase):
table = TagTable
class ConfigContextProfileTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigContextProfileTable
class ConfigContextTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigContextTable
class ConfigTemplateTableTest(TableTestCases.OrderableColumnsTestCase):
table = ConfigTemplateTable
class ImageAttachmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = ImageAttachmentTable
class JournalEntryTableTest(TableTestCases.OrderableColumnsTestCase):
table = JournalEntryTable

View File

@@ -1,9 +1,10 @@
from django.test import RequestFactory, TestCase
from netaddr import IPNetwork
from ipam.models import IPAddress, IPRange, Prefix
from ipam.tables import AnnotatedIPAddressTable
from ipam.models import FHRPGroupAssignment, IPAddress, IPRange, Prefix
from ipam.tables import *
from ipam.utils import annotate_ip_space
from utilities.testing import TableTestCases
class AnnotatedIPAddressTableTest(TestCase):
@@ -168,3 +169,82 @@ class AnnotatedIPAddressTableTest(TestCase):
# Pools are fully usable
self.assertEqual(available.first_ip, '2001:db8:1::/126')
self.assertEqual(available.size, 4)
#
# Table ordering tests
#
class VRFTableTest(TableTestCases.OrderableColumnsTestCase):
table = VRFTable
class RouteTargetTableTest(TableTestCases.OrderableColumnsTestCase):
table = RouteTargetTable
class RIRTableTest(TableTestCases.OrderableColumnsTestCase):
table = RIRTable
class AggregateTableTest(TableTestCases.OrderableColumnsTestCase):
table = AggregateTable
class RoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = RoleTable
class PrefixTableTest(TableTestCases.OrderableColumnsTestCase):
table = PrefixTable
class IPRangeTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPRangeTable
class IPAddressTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPAddressTable
class FHRPGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = FHRPGroupTable
class FHRPGroupAssignmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = FHRPGroupAssignmentTable
queryset_sources = [
('FHRPGroupAssignmentTable', FHRPGroupAssignment.objects.all()),
]
class VLANGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANGroupTable
class VLANTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANTable
class VLANTranslationPolicyTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANTranslationPolicyTable
class VLANTranslationRuleTableTest(TableTestCases.OrderableColumnsTestCase):
table = VLANTranslationRuleTable
class ASNRangeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ASNRangeTable
class ASNTableTest(TableTestCases.OrderableColumnsTestCase):
table = ASNTable
class ServiceTemplateTableTest(TableTestCases.OrderableColumnsTestCase):
table = ServiceTemplateTable
class ServiceTableTest(TableTestCases.OrderableColumnsTestCase):
table = ServiceTable

View File

@@ -0,0 +1,26 @@
from tenancy.tables import *
from utilities.testing import TableTestCases
class TenantGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = TenantGroupTable
class TenantTableTest(TableTestCases.OrderableColumnsTestCase):
table = TenantTable
class ContactGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactGroupTable
class ContactRoleTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactRoleTable
class ContactTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactTable
class ContactAssignmentTableTest(TableTestCases.OrderableColumnsTestCase):
table = ContactAssignmentTable

View File

@@ -1,24 +1,26 @@
from django.test import RequestFactory, TestCase, tag
from users.models import Token
from users.tables import TokenTable
from users.tables import *
from utilities.testing import TableTestCases
class TokenTableTest(TestCase):
@tag('regression')
def test_every_orderable_field_does_not_throw_exception(self):
tokens = Token.objects.all()
disallowed = {'actions'}
class TokenTableTest(TableTestCases.OrderableColumnsTestCase):
table = TokenTable
orderable_columns = [
column.name for column in TokenTable(tokens).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get("/")
for col in orderable_columns:
for direction in ('-', ''):
with self.subTest(col=col, direction=direction):
table = TokenTable(tokens)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
class UserTableTest(TableTestCases.OrderableColumnsTestCase):
table = UserTable
class GroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = GroupTable
class ObjectPermissionTableTest(TableTestCases.OrderableColumnsTestCase):
table = ObjectPermissionTable
class OwnerGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = OwnerGroupTable
class OwnerTableTest(TableTestCases.OrderableColumnsTestCase):
table = OwnerTable

View File

@@ -1,5 +1,6 @@
from .api import *
from .base import *
from .filtersets import *
from .tables import *
from .utils import *
from .views import *

View File

@@ -0,0 +1,130 @@
import inspect
from importlib import import_module
from django.test import RequestFactory
from netbox.views import generic
from .base import TestCase
__all__ = (
"ModelTableTestCase",
"TableTestCases",
)
class ModelTableTestCase(TestCase):
"""
Shared helpers for model-backed table tests.
Concrete subclasses should set `table` and may override `get_queryset()`
or `excluded_orderable_columns` as needed.
"""
table = None
excluded_orderable_columns = frozenset({"actions"})
# Optional explicit override for odd cases
queryset_sources = None
# Only these view types are considered sortable queryset sources by default
queryset_source_view_classes = (generic.ObjectListView,)
@classmethod
def validate_table_test_case(cls):
if cls.table is None:
raise AssertionError(f"{cls.__name__} must define `table`")
if getattr(cls.table._meta, "model", None) is None:
raise AssertionError(f"{cls.__name__}.table must be model-backed")
def get_request(self):
request = RequestFactory().get("/")
request.user = self.user
return request
def get_table(self, queryset):
return self.table(queryset)
@classmethod
def is_queryset_source_view(cls, view):
model = cls.table._meta.model
app_label = model._meta.app_label
return (
inspect.isclass(view)
and view.__module__.startswith(f"{app_label}.views")
and getattr(view, "table", None) is cls.table
and getattr(view, "queryset", None) is not None
and issubclass(view, cls.queryset_source_view_classes)
)
@classmethod
def get_queryset_sources(cls):
"""
Return iterable of (label, queryset) pairs to test.
By default, only discover list-style views that declare this table.
That keeps bulk edit/delete confirmation tables out of the ordering
smoke test.
"""
if cls.queryset_sources is not None:
return tuple(cls.queryset_sources)
model = cls.table._meta.model
app_label = model._meta.app_label
module = import_module(f"{app_label}.views")
sources = []
for _, view in inspect.getmembers(module, inspect.isclass):
if not cls.is_queryset_source_view(view):
continue
queryset = view.queryset
if hasattr(queryset, "all"):
queryset = queryset.all()
sources.append((view.__name__, queryset))
if not sources:
raise AssertionError(
f"{cls.__name__} could not find any list-style queryset source for "
f"{cls.table.__module__}.{cls.table.__name__}; "
"set `queryset_sources` explicitly if needed."
)
return tuple(sources)
def iter_orderable_columns(self, queryset):
for column in self.get_table(queryset).columns:
if not column.orderable:
continue
if column.name in self.excluded_orderable_columns:
continue
yield column.name
class TableTestCases:
"""
Keep test_* methods nested to avoid unittest auto-discovering the reusable
base classes directly.
"""
class OrderableColumnsTestCase(ModelTableTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.validate_table_test_case()
def test_every_orderable_column_renders(self):
request = self.get_request()
for source_name, queryset in self.get_queryset_sources():
for column_name in self.iter_orderable_columns(queryset):
for direction, prefix in (("asc", ""), ("desc", "-")):
with self.cleanupSubTest(
source=source_name,
column=column_name,
direction=direction,
):
table = self.get_table(queryset)
table.order_by = f"{prefix}{column_name}"
table.as_html(request)

View File

@@ -0,0 +1,26 @@
from utilities.testing import TableTestCases
from virtualization.tables import *
class ClusterTypeTableTest(TableTestCases.OrderableColumnsTestCase):
table = ClusterTypeTable
class ClusterGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = ClusterGroupTable
class ClusterTableTest(TableTestCases.OrderableColumnsTestCase):
table = ClusterTable
class VirtualMachineTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualMachineTable
class VMInterfaceTableTest(TableTestCases.OrderableColumnsTestCase):
table = VMInterfaceTable
class VirtualDiskTableTest(TableTestCases.OrderableColumnsTestCase):
table = VirtualDiskTable

View File

@@ -1,23 +1,42 @@
from django.test import RequestFactory, TestCase, tag
from vpn.models import TunnelTermination
from vpn.tables import TunnelTerminationTable
from utilities.testing import TableTestCases
from vpn.tables import *
@tag('regression')
class TunnelTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = TunnelTermination.objects.all()
fake_request = RequestFactory().get("/")
disallowed = {'actions'}
class TunnelGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = TunnelGroupTable
orderable_columns = [
column.name for column in TunnelTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
for col in orderable_columns:
for dir in ('-', ''):
table = TunnelTerminationTable(terminations)
table.order_by = f'{dir}{col}'
table.as_html(fake_request)
class TunnelTableTest(TableTestCases.OrderableColumnsTestCase):
table = TunnelTable
class TunnelTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = TunnelTerminationTable
class IKEProposalTableTest(TableTestCases.OrderableColumnsTestCase):
table = IKEProposalTable
class IKEPolicyTableTest(TableTestCases.OrderableColumnsTestCase):
table = IKEPolicyTable
class IPSecProposalTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPSecProposalTable
class IPSecPolicyTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPSecPolicyTable
class IPSecProfileTableTest(TableTestCases.OrderableColumnsTestCase):
table = IPSecProfileTable
class L2VPNTableTest(TableTestCases.OrderableColumnsTestCase):
table = L2VPNTable
class L2VPNTerminationTableTest(TableTestCases.OrderableColumnsTestCase):
table = L2VPNTerminationTable

View File

@@ -0,0 +1,14 @@
from utilities.testing import TableTestCases
from wireless.tables import *
class WirelessLANGroupTableTest(TableTestCases.OrderableColumnsTestCase):
table = WirelessLANGroupTable
class WirelessLANTableTest(TableTestCases.OrderableColumnsTestCase):
table = WirelessLANTable
class WirelessLinkTableTest(TableTestCases.OrderableColumnsTestCase):
table = WirelessLinkTable