mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-07 18:03:47 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bbecef77d | ||
|
|
1ebeb71ad8 | ||
|
|
d6a1cc5558 | ||
|
|
09f7df0726 | ||
|
|
f242f17ce5 | ||
|
|
7d71503ea2 | ||
|
|
e07a5966ae |
@@ -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.StandardTableTestCase):
|
||||
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.StandardTableTestCase):
|
||||
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.StandardTableTestCase):
|
||||
table = CircuitTerminationTable
|
||||
|
||||
|
||||
class CircuitGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = CircuitGroupTable
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = CircuitGroupAssignmentTable
|
||||
|
||||
|
||||
class ProviderTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ProviderTable
|
||||
|
||||
|
||||
class ProviderAccountTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ProviderAccountTable
|
||||
|
||||
|
||||
class ProviderNetworkTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ProviderNetworkTable
|
||||
|
||||
|
||||
class VirtualCircuitTypeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VirtualCircuitTypeTable
|
||||
|
||||
|
||||
class VirtualCircuitTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VirtualCircuitTable
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VirtualCircuitTerminationTable
|
||||
|
||||
26
netbox/core/tests/test_tables.py
Normal file
26
netbox/core/tests/test_tables.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from core.models import ObjectChange
|
||||
from core.tables import *
|
||||
from utilities.testing import TableTestCases
|
||||
|
||||
|
||||
class DataSourceTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = DataSourceTable
|
||||
|
||||
|
||||
class DataFileTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = DataFileTable
|
||||
|
||||
|
||||
class JobTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = JobTable
|
||||
|
||||
|
||||
class ObjectChangeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ObjectChangeTable
|
||||
queryset_sources = [
|
||||
('ObjectChangeListView', ObjectChange.objects.valid_models()),
|
||||
]
|
||||
|
||||
|
||||
class ConfigRevisionTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ConfigRevisionTable
|
||||
@@ -26,6 +26,7 @@ from tenancy.models import *
|
||||
from users.filterset_mixins import OwnerFilterMixin
|
||||
from users.models import User
|
||||
from utilities.filters import (
|
||||
MultiValueBigNumberFilter,
|
||||
MultiValueCharFilter,
|
||||
MultiValueContentTypeFilter,
|
||||
MultiValueMACAddressFilter,
|
||||
@@ -2175,7 +2176,7 @@ class InterfaceFilterSet(
|
||||
distinct=False,
|
||||
label=_('LAG interface (ID)'),
|
||||
)
|
||||
speed = MultiValueNumberFilter()
|
||||
speed = MultiValueBigNumberFilter(min_value=0)
|
||||
duplex = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfaceDuplexChoices,
|
||||
distinct=False,
|
||||
|
||||
@@ -20,7 +20,13 @@ from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
|
||||
from tenancy.models import Tenant
|
||||
from users.models import User
|
||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||
from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField
|
||||
from utilities.forms.fields import (
|
||||
ColorField,
|
||||
DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
JSONField,
|
||||
PositiveBigIntegerField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
||||
from virtualization.models import Cluster
|
||||
@@ -1420,7 +1426,7 @@ class InterfaceBulkEditForm(
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
speed = PositiveBigIntegerField(
|
||||
label=_('Speed'),
|
||||
required=False,
|
||||
widget=NumberWithOptions(
|
||||
|
||||
@@ -19,7 +19,7 @@ from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from users.models import User
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, PositiveBigIntegerField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import NumberWithOptions
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||
@@ -1603,7 +1603,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
choices=InterfaceTypeChoices,
|
||||
required=False
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
speed = PositiveBigIntegerField(
|
||||
label=_('Speed'),
|
||||
required=False,
|
||||
widget=NumberWithOptions(
|
||||
|
||||
@@ -47,7 +47,13 @@ if TYPE_CHECKING:
|
||||
VRFFilter,
|
||||
)
|
||||
from netbox.graphql.enums import ColorEnum
|
||||
from netbox.graphql.filter_lookups import FloatLookup, IntegerArrayLookup, IntegerLookup, TreeNodeFilter
|
||||
from netbox.graphql.filter_lookups import (
|
||||
BigIntegerLookup,
|
||||
FloatLookup,
|
||||
IntegerArrayLookup,
|
||||
IntegerLookup,
|
||||
TreeNodeFilter,
|
||||
)
|
||||
from users.graphql.filters import UserFilter
|
||||
from virtualization.graphql.filters import ClusterFilter
|
||||
from vpn.graphql.filters import L2VPNFilter, TunnelTerminationFilter
|
||||
@@ -519,7 +525,7 @@ class InterfaceFilter(
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
mgmt_only: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
speed: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
duplex: BaseFilterLookup[Annotated['InterfaceDuplexEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
|
||||
@@ -433,6 +433,7 @@ class MACAddressType(PrimaryObjectType):
|
||||
)
|
||||
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
_name: str
|
||||
speed: BigInt | None
|
||||
wwn: str | None
|
||||
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
bridge: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
15
netbox/dcim/migrations/0227_alter_interface_speed_bigint.py
Normal file
15
netbox/dcim/migrations/0227_alter_interface_speed_bigint.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0226_modulebay_rebuild_tree'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='speed',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -806,7 +806,7 @@ class Interface(
|
||||
verbose_name=_('management only'),
|
||||
help_text=_('This interface is used only for out-of-band management')
|
||||
)
|
||||
speed = models.PositiveIntegerField(
|
||||
speed = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('speed (Kbps)')
|
||||
|
||||
@@ -382,6 +382,17 @@ class PathEndpointTable(CableTerminationTable):
|
||||
orderable=False
|
||||
)
|
||||
|
||||
def value_connection(self, value):
|
||||
if value:
|
||||
connections = []
|
||||
for termination in value:
|
||||
if hasattr(termination, 'parent_object'):
|
||||
connections.append(f'{termination.parent_object} > {termination}')
|
||||
else:
|
||||
connections.append(str(termination))
|
||||
return ', '.join(connections)
|
||||
return None
|
||||
|
||||
|
||||
class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
@@ -683,6 +694,15 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
||||
orderable=False
|
||||
)
|
||||
|
||||
def value_connection(self, record, value):
|
||||
if record.is_virtual and hasattr(record, 'virtual_circuit_termination') and record.virtual_circuit_termination:
|
||||
connections = [
|
||||
f"{t.interface.parent_object} > {t.interface} via {t.parent_object}"
|
||||
for t in record.connected_endpoints
|
||||
]
|
||||
return ', '.join(connections)
|
||||
return super().value_connection(value)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.Interface
|
||||
fields = (
|
||||
|
||||
@@ -1930,9 +1930,9 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Interface 4',
|
||||
'type': '1000base-t',
|
||||
'type': 'other',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'speed': 1000000,
|
||||
'speed': 16_000_000_000,
|
||||
'duplex': 'full',
|
||||
'vrf': vrfs[0].pk,
|
||||
'poe_mode': InterfacePoEModeChoices.MODE_PD,
|
||||
|
||||
@@ -4655,7 +4655,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
enabled=True,
|
||||
mgmt_only=True,
|
||||
tx_power=40,
|
||||
speed=100000,
|
||||
speed=16_000_000_000,
|
||||
duplex='full',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
|
||||
@@ -4757,7 +4757,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_speed(self):
|
||||
params = {'speed': [1000000, 100000]}
|
||||
params = {'speed': [16_000_000_000, 1_000_000, 100_000]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_duplex(self):
|
||||
|
||||
204
netbox/dcim/tests/test_tables.py
Normal file
204
netbox/dcim/tests/test_tables.py
Normal 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.StandardTableTestCase):
|
||||
table = RegionTable
|
||||
|
||||
|
||||
class SiteGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = SiteGroupTable
|
||||
|
||||
|
||||
class SiteTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = SiteTable
|
||||
|
||||
|
||||
class LocationTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = LocationTable
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackRoleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RackRoleTable
|
||||
|
||||
|
||||
class RackTypeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RackTypeTable
|
||||
|
||||
|
||||
class RackTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RackTable
|
||||
|
||||
|
||||
class RackReservationTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RackReservationTable
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
class ManufacturerTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ManufacturerTable
|
||||
|
||||
|
||||
class DeviceTypeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = DeviceTypeTable
|
||||
|
||||
|
||||
#
|
||||
# Module types
|
||||
#
|
||||
|
||||
class ModuleTypeProfileTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ModuleTypeProfileTable
|
||||
|
||||
|
||||
class ModuleTypeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ModuleTypeTable
|
||||
|
||||
|
||||
class ModuleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ModuleTable
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceRoleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = DeviceRoleTable
|
||||
|
||||
|
||||
class PlatformTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = PlatformTable
|
||||
|
||||
|
||||
class DeviceTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = DeviceTable
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ConsolePortTable
|
||||
|
||||
|
||||
class ConsoleServerPortTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ConsoleServerPortTable
|
||||
|
||||
|
||||
class PowerPortTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = PowerPortTable
|
||||
|
||||
|
||||
class PowerOutletTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = PowerOutletTable
|
||||
|
||||
|
||||
class InterfaceTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = InterfaceTable
|
||||
|
||||
|
||||
class FrontPortTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = FrontPortTable
|
||||
|
||||
|
||||
class RearPortTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RearPortTable
|
||||
|
||||
|
||||
class ModuleBayTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ModuleBayTable
|
||||
|
||||
|
||||
class DeviceBayTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = DeviceBayTable
|
||||
|
||||
|
||||
class InventoryItemTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = InventoryItemTable
|
||||
|
||||
|
||||
class InventoryItemRoleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = InventoryItemRoleTable
|
||||
|
||||
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ConsoleConnectionTable
|
||||
queryset_sources = [
|
||||
('ConsoleConnectionsListView', ConsolePort.objects.filter(_path__is_complete=True)),
|
||||
]
|
||||
|
||||
|
||||
class PowerConnectionTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = PowerConnectionTable
|
||||
queryset_sources = [
|
||||
('PowerConnectionsListView', PowerPort.objects.filter(_path__is_complete=True)),
|
||||
]
|
||||
|
||||
|
||||
class InterfaceConnectionTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = InterfaceConnectionTable
|
||||
queryset_sources = [
|
||||
('InterfaceConnectionsListView', Interface.objects.filter(_path__is_complete=True)),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
class CableTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = CableTable
|
||||
|
||||
|
||||
#
|
||||
# Power
|
||||
#
|
||||
|
||||
class PowerPanelTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = PowerPanelTable
|
||||
|
||||
|
||||
class PowerFeedTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = PowerFeedTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VirtualChassisTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual device contexts
|
||||
#
|
||||
|
||||
class VirtualDeviceContextTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VirtualDeviceContextTable
|
||||
|
||||
|
||||
#
|
||||
# MAC addresses
|
||||
#
|
||||
|
||||
class MACAddressTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = MACAddressTable
|
||||
@@ -2961,13 +2961,13 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Interface X',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'type': InterfaceTypeChoices.TYPE_OTHER,
|
||||
'enabled': False,
|
||||
'bridge': interfaces[4].pk,
|
||||
'lag': interfaces[3].pk,
|
||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||
'mtu': 65000,
|
||||
'speed': 1000000,
|
||||
'speed': 16_000_000_000,
|
||||
'duplex': 'full',
|
||||
'mgmt_only': True,
|
||||
'description': 'A front port',
|
||||
@@ -2985,13 +2985,13 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Interface [4-6]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'type': InterfaceTypeChoices.TYPE_OTHER,
|
||||
'enabled': False,
|
||||
'bridge': interfaces[4].pk,
|
||||
'lag': interfaces[3].pk,
|
||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||
'mtu': 2000,
|
||||
'speed': 100000,
|
||||
'speed': 16_000_000_000,
|
||||
'duplex': 'half',
|
||||
'mgmt_only': True,
|
||||
'description': 'A front port',
|
||||
|
||||
@@ -1,24 +1,93 @@
|
||||
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.StandardTableTestCase):
|
||||
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.StandardTableTestCase):
|
||||
table = CustomFieldChoiceSetTable
|
||||
|
||||
|
||||
class CustomLinkTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = CustomLinkTable
|
||||
|
||||
|
||||
class ExportTemplateTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ExportTemplateTable
|
||||
|
||||
|
||||
class SavedFilterTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = SavedFilterTable
|
||||
|
||||
|
||||
class TableConfigTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = TableConfigTable
|
||||
|
||||
|
||||
class BookmarkTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = BookmarkTable
|
||||
|
||||
# The list view for this table lives in account.views (not extras.views),
|
||||
# so auto-discovery cannot find it. Provide an explicit queryset source.
|
||||
queryset_sources = [
|
||||
('Bookmark.objects.all()', Bookmark.objects.all()),
|
||||
]
|
||||
|
||||
|
||||
class NotificationGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = NotificationGroupTable
|
||||
|
||||
|
||||
class NotificationTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = NotificationTable
|
||||
|
||||
# The list view for this table lives in account.views (not extras.views),
|
||||
# so auto-discovery cannot find it. Provide an explicit queryset source.
|
||||
queryset_sources = [
|
||||
('Notification.objects.all()', Notification.objects.all()),
|
||||
]
|
||||
|
||||
|
||||
class SubscriptionTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = SubscriptionTable
|
||||
|
||||
# The list view for this table lives in account.views (not extras.views),
|
||||
# so auto-discovery cannot find it. Provide an explicit queryset source.
|
||||
queryset_sources = [
|
||||
('Subscription.objects.all()', Subscription.objects.all()),
|
||||
]
|
||||
|
||||
|
||||
class WebhookTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = WebhookTable
|
||||
|
||||
|
||||
class EventRuleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = EventRuleTable
|
||||
|
||||
|
||||
class TagTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = TagTable
|
||||
|
||||
|
||||
class ConfigContextProfileTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ConfigContextProfileTable
|
||||
|
||||
|
||||
class ConfigContextTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ConfigContextTable
|
||||
|
||||
|
||||
class ConfigTemplateTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ConfigTemplateTable
|
||||
|
||||
|
||||
class ImageAttachmentTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ImageAttachmentTable
|
||||
|
||||
|
||||
class JournalEntryTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = JournalEntryTable
|
||||
|
||||
@@ -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,85 @@ 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.StandardTableTestCase):
|
||||
table = VRFTable
|
||||
|
||||
|
||||
class RouteTargetTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RouteTargetTable
|
||||
|
||||
|
||||
class RIRTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RIRTable
|
||||
|
||||
|
||||
class AggregateTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = AggregateTable
|
||||
|
||||
|
||||
class RoleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = RoleTable
|
||||
|
||||
|
||||
class PrefixTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = PrefixTable
|
||||
|
||||
|
||||
class IPRangeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = IPRangeTable
|
||||
|
||||
|
||||
class IPAddressTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = IPAddressTable
|
||||
|
||||
|
||||
class FHRPGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = FHRPGroupTable
|
||||
|
||||
|
||||
class FHRPGroupAssignmentTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = FHRPGroupAssignmentTable
|
||||
|
||||
# No ObjectListView exists for this table; it is only rendered inline on
|
||||
# the FHRPGroup detail view. Provide an explicit queryset source.
|
||||
queryset_sources = [
|
||||
('FHRPGroupAssignment.objects.all()', FHRPGroupAssignment.objects.all()),
|
||||
]
|
||||
|
||||
|
||||
class VLANGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VLANGroupTable
|
||||
|
||||
|
||||
class VLANTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VLANTable
|
||||
|
||||
|
||||
class VLANTranslationPolicyTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VLANTranslationPolicyTable
|
||||
|
||||
|
||||
class VLANTranslationRuleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VLANTranslationRuleTable
|
||||
|
||||
|
||||
class ASNRangeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ASNRangeTable
|
||||
|
||||
|
||||
class ASNTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ASNTable
|
||||
|
||||
|
||||
class ServiceTemplateTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ServiceTemplateTable
|
||||
|
||||
|
||||
class ServiceTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ServiceTable
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<h2 class="card-header" id="module{{ module.pk }}">
|
||||
<i class="mdi mdi-file-document-outline"></i> {{ module }}
|
||||
<div class="card-actions">
|
||||
{% if perms.extras.edit_scriptmodule %}
|
||||
{% if perms.extras.change_scriptmodule %}
|
||||
<a href="{% url 'extras:scriptmodule_edit' pk=module.pk %}" class="btn btn-ghost-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
|
||||
26
netbox/tenancy/tests/test_tables.py
Normal file
26
netbox/tenancy/tests/test_tables.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from tenancy.tables import *
|
||||
from utilities.testing import TableTestCases
|
||||
|
||||
|
||||
class TenantGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = TenantGroupTable
|
||||
|
||||
|
||||
class TenantTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = TenantTable
|
||||
|
||||
|
||||
class ContactGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ContactGroupTable
|
||||
|
||||
|
||||
class ContactRoleTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ContactRoleTable
|
||||
|
||||
|
||||
class ContactTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ContactTable
|
||||
|
||||
|
||||
class ContactAssignmentTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ContactAssignmentTable
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.StandardTableTestCase):
|
||||
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.StandardTableTestCase):
|
||||
table = UserTable
|
||||
|
||||
|
||||
class GroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = GroupTable
|
||||
|
||||
|
||||
class ObjectPermissionTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ObjectPermissionTable
|
||||
|
||||
|
||||
class OwnerGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = OwnerGroupTable
|
||||
|
||||
|
||||
class OwnerTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = OwnerTable
|
||||
|
||||
@@ -7,9 +7,12 @@ from django_filters.constants import EMPTY_VALUES
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
|
||||
from .forms.fields import BigIntegerField
|
||||
|
||||
__all__ = (
|
||||
'ContentTypeFilter',
|
||||
'MultiValueArrayFilter',
|
||||
'MultiValueBigNumberFilter',
|
||||
'MultiValueCharFilter',
|
||||
'MultiValueContentTypeFilter',
|
||||
'MultiValueDateFilter',
|
||||
@@ -77,6 +80,11 @@ class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.IntegerField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT64)
|
||||
class MultiValueBigNumberFilter(MultiValueNumberFilter):
|
||||
field_class = multivalue_field_factory(BigIntegerField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DECIMAL)
|
||||
class MultiValueDecimalFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.DecimalField)
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import BigIntegerField as BigIntegerModelField
|
||||
from django.db.models import Count
|
||||
from django.forms.fields import InvalidJSONInput
|
||||
from django.forms.fields import JSONField as _JSONField
|
||||
@@ -13,17 +14,39 @@ from utilities.forms import widgets
|
||||
from utilities.validators import EnhancedURLValidator
|
||||
|
||||
__all__ = (
|
||||
'BigIntegerField',
|
||||
'ColorField',
|
||||
'CommentField',
|
||||
'JSONField',
|
||||
'LaxURLField',
|
||||
'MACAddressField',
|
||||
'PositiveBigIntegerField',
|
||||
'QueryField',
|
||||
'SlugField',
|
||||
'TagFilterField',
|
||||
)
|
||||
|
||||
|
||||
class BigIntegerField(forms.IntegerField):
|
||||
"""
|
||||
An IntegerField constrained to the range of a signed 64-bit integer.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('min_value', -BigIntegerModelField.MAX_BIGINT - 1)
|
||||
kwargs.setdefault('max_value', BigIntegerModelField.MAX_BIGINT)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class PositiveBigIntegerField(BigIntegerField):
|
||||
"""
|
||||
An IntegerField constrained to the range supported by Django's
|
||||
PositiveBigIntegerField model field.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('min_value', 0)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class QueryField(forms.CharField):
|
||||
"""
|
||||
A CharField subclass used for global search/query fields in filter forms.
|
||||
|
||||
@@ -186,26 +186,52 @@ def action_url(parser, token):
|
||||
return ActionURLNode(model, action, kwargs, asvar)
|
||||
|
||||
|
||||
def _format_speed(speed, divisor, unit):
|
||||
"""
|
||||
Format a speed value with a given divisor and unit.
|
||||
|
||||
Handles decimal values and strips trailing zeros for clean output.
|
||||
"""
|
||||
whole, remainder = divmod(speed, divisor)
|
||||
if remainder == 0:
|
||||
return f'{whole} {unit}'
|
||||
|
||||
# Divisors are powers of 10, so len(str(divisor)) - 1 matches the decimal precision.
|
||||
precision = len(str(divisor)) - 1
|
||||
fraction = f'{remainder:0{precision}d}'.rstrip('0')
|
||||
return f'{whole}.{fraction} {unit}'
|
||||
|
||||
|
||||
@register.filter()
|
||||
def humanize_speed(speed):
|
||||
"""
|
||||
Humanize speeds given in Kbps. Examples:
|
||||
Humanize speeds given in Kbps, always using the largest appropriate unit.
|
||||
|
||||
1544 => "1.544 Mbps"
|
||||
100000 => "100 Mbps"
|
||||
10000000 => "10 Gbps"
|
||||
Decimal values are displayed when the result is not a whole number;
|
||||
trailing zeros after the decimal point are stripped for clean output.
|
||||
|
||||
Examples:
|
||||
|
||||
1_544 => "1.544 Mbps"
|
||||
100_000 => "100 Mbps"
|
||||
1_000_000 => "1 Gbps"
|
||||
2_500_000 => "2.5 Gbps"
|
||||
10_000_000 => "10 Gbps"
|
||||
800_000_000 => "800 Gbps"
|
||||
1_600_000_000 => "1.6 Tbps"
|
||||
"""
|
||||
if not speed:
|
||||
return ''
|
||||
if speed >= 1000000000 and speed % 1000000000 == 0:
|
||||
return '{} Tbps'.format(int(speed / 1000000000))
|
||||
if speed >= 1000000 and speed % 1000000 == 0:
|
||||
return '{} Gbps'.format(int(speed / 1000000))
|
||||
if speed >= 1000 and speed % 1000 == 0:
|
||||
return '{} Mbps'.format(int(speed / 1000))
|
||||
if speed >= 1000:
|
||||
return '{} Mbps'.format(float(speed) / 1000)
|
||||
return '{} Kbps'.format(speed)
|
||||
|
||||
speed = int(speed)
|
||||
|
||||
if speed >= 1_000_000_000:
|
||||
return _format_speed(speed, 1_000_000_000, 'Tbps')
|
||||
if speed >= 1_000_000:
|
||||
return _format_speed(speed, 1_000_000, 'Gbps')
|
||||
if speed >= 1_000:
|
||||
return _format_speed(speed, 1_000, 'Mbps')
|
||||
return f'{speed} Kbps'
|
||||
|
||||
|
||||
def _humanize_capacity(value, divisor=1000):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .api import *
|
||||
from .base import *
|
||||
from .filtersets import *
|
||||
from .tables import *
|
||||
from .utils import *
|
||||
from .views import *
|
||||
|
||||
157
netbox/utilities/testing/tables.py
Normal file
157
netbox/utilities/testing/tables.py
Normal file
@@ -0,0 +1,157 @@
|
||||
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 ordering smoke tests.
|
||||
|
||||
Concrete subclasses should set `table` and may override `queryset_sources`,
|
||||
`queryset_source_view_classes`, 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):
|
||||
"""
|
||||
Assert that the test case is correctly configured with a model-backed table.
|
||||
|
||||
Raises:
|
||||
AssertionError: If ``table`` is not set or is not backed by a Django model.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Build a minimal ``GET`` request authenticated as the test user.
|
||||
"""
|
||||
request = RequestFactory().get("/")
|
||||
request.user = self.user
|
||||
return request
|
||||
|
||||
def get_table(self, queryset):
|
||||
"""
|
||||
Instantiate the table class under test with the given *queryset*.
|
||||
"""
|
||||
return self.table(queryset)
|
||||
|
||||
@classmethod
|
||||
def is_queryset_source_view(cls, view):
|
||||
"""
|
||||
Return ``True`` if *view* is a list-style view class that declares
|
||||
this test case's table and exposes a usable queryset.
|
||||
"""
|
||||
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.
|
||||
|
||||
The label is included in the subtest failure output.
|
||||
|
||||
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):
|
||||
"""
|
||||
Yield the names of all orderable columns for *queryset*, excluding
|
||||
any listed in ``excluded_orderable_columns``.
|
||||
"""
|
||||
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 StandardTableTestCase(ModelTableTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.validate_table_test_case()
|
||||
|
||||
def test_every_orderable_column_renders(self):
|
||||
"""
|
||||
Verify that each declared ordering can be applied without error.
|
||||
|
||||
This is intentionally a smoke test. It validates ordering against
|
||||
the configured queryset sources but does not create model
|
||||
instances by default, so it complements rather than replaces
|
||||
data-backed rendering tests for tables whose behavior depends on
|
||||
populated querysets.
|
||||
"""
|
||||
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.subTest(source=source_name, column=column_name, direction=direction):
|
||||
table = self.get_table(queryset)
|
||||
table.order_by = f"{prefix}{column_name}"
|
||||
table.as_html(request)
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import patch
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from utilities.templatetags.builtins.tags import static_with_params
|
||||
from utilities.templatetags.helpers import _humanize_capacity
|
||||
from utilities.templatetags.helpers import _humanize_capacity, humanize_speed
|
||||
|
||||
|
||||
class StaticWithParamsTest(TestCase):
|
||||
@@ -90,3 +90,87 @@ class HumanizeCapacityTest(TestCase):
|
||||
|
||||
def test_default_divisor_is_1000(self):
|
||||
self.assertEqual(_humanize_capacity(2000), '2.00 GB')
|
||||
|
||||
|
||||
class HumanizeSpeedTest(TestCase):
|
||||
"""
|
||||
Test the humanize_speed filter for correct unit selection and decimal formatting.
|
||||
"""
|
||||
|
||||
# Falsy / empty inputs
|
||||
|
||||
def test_none(self):
|
||||
self.assertEqual(humanize_speed(None), '')
|
||||
|
||||
def test_zero(self):
|
||||
self.assertEqual(humanize_speed(0), '')
|
||||
|
||||
def test_empty_string(self):
|
||||
self.assertEqual(humanize_speed(''), '')
|
||||
|
||||
# Kbps (below 1000)
|
||||
|
||||
def test_kbps(self):
|
||||
self.assertEqual(humanize_speed(100), '100 Kbps')
|
||||
|
||||
def test_kbps_low(self):
|
||||
self.assertEqual(humanize_speed(1), '1 Kbps')
|
||||
|
||||
# Mbps (1,000 – 999,999)
|
||||
|
||||
def test_mbps_whole(self):
|
||||
self.assertEqual(humanize_speed(100_000), '100 Mbps')
|
||||
|
||||
def test_mbps_decimal(self):
|
||||
self.assertEqual(humanize_speed(1_544), '1.544 Mbps')
|
||||
|
||||
def test_mbps_10(self):
|
||||
self.assertEqual(humanize_speed(10_000), '10 Mbps')
|
||||
|
||||
# Gbps (1,000,000 – 999,999,999)
|
||||
|
||||
def test_gbps_whole(self):
|
||||
self.assertEqual(humanize_speed(1_000_000), '1 Gbps')
|
||||
|
||||
def test_gbps_decimal(self):
|
||||
self.assertEqual(humanize_speed(2_500_000), '2.5 Gbps')
|
||||
|
||||
def test_gbps_10(self):
|
||||
self.assertEqual(humanize_speed(10_000_000), '10 Gbps')
|
||||
|
||||
def test_gbps_25(self):
|
||||
self.assertEqual(humanize_speed(25_000_000), '25 Gbps')
|
||||
|
||||
def test_gbps_40(self):
|
||||
self.assertEqual(humanize_speed(40_000_000), '40 Gbps')
|
||||
|
||||
def test_gbps_100(self):
|
||||
self.assertEqual(humanize_speed(100_000_000), '100 Gbps')
|
||||
|
||||
def test_gbps_400(self):
|
||||
self.assertEqual(humanize_speed(400_000_000), '400 Gbps')
|
||||
|
||||
def test_gbps_800(self):
|
||||
self.assertEqual(humanize_speed(800_000_000), '800 Gbps')
|
||||
|
||||
# Tbps (1,000,000,000+)
|
||||
|
||||
def test_tbps_whole(self):
|
||||
self.assertEqual(humanize_speed(1_000_000_000), '1 Tbps')
|
||||
|
||||
def test_tbps_decimal(self):
|
||||
self.assertEqual(humanize_speed(1_600_000_000), '1.6 Tbps')
|
||||
|
||||
# Edge cases
|
||||
|
||||
def test_string_input(self):
|
||||
"""Ensure string values are cast to int correctly."""
|
||||
self.assertEqual(humanize_speed('2500000'), '2.5 Gbps')
|
||||
|
||||
def test_non_round_remainder_preserved(self):
|
||||
"""Ensure fractional parts with interior zeros are preserved."""
|
||||
self.assertEqual(humanize_speed(1_001_000), '1.001 Gbps')
|
||||
|
||||
def test_trailing_zeros_stripped(self):
|
||||
"""Ensure trailing fractional zeros are stripped (5.500 → 5.5)."""
|
||||
self.assertEqual(humanize_speed(5_500_000), '5.5 Gbps')
|
||||
|
||||
26
netbox/virtualization/tests/test_tables.py
Normal file
26
netbox/virtualization/tests/test_tables.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from utilities.testing import TableTestCases
|
||||
from virtualization.tables import *
|
||||
|
||||
|
||||
class ClusterTypeTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ClusterTypeTable
|
||||
|
||||
|
||||
class ClusterGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ClusterGroupTable
|
||||
|
||||
|
||||
class ClusterTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = ClusterTable
|
||||
|
||||
|
||||
class VirtualMachineTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VirtualMachineTable
|
||||
|
||||
|
||||
class VMInterfaceTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VMInterfaceTable
|
||||
|
||||
|
||||
class VirtualDiskTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = VirtualDiskTable
|
||||
@@ -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.StandardTableTestCase):
|
||||
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.StandardTableTestCase):
|
||||
table = TunnelTable
|
||||
|
||||
|
||||
class TunnelTerminationTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = TunnelTerminationTable
|
||||
|
||||
|
||||
class IKEProposalTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = IKEProposalTable
|
||||
|
||||
|
||||
class IKEPolicyTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = IKEPolicyTable
|
||||
|
||||
|
||||
class IPSecProposalTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = IPSecProposalTable
|
||||
|
||||
|
||||
class IPSecPolicyTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = IPSecPolicyTable
|
||||
|
||||
|
||||
class IPSecProfileTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = IPSecProfileTable
|
||||
|
||||
|
||||
class L2VPNTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = L2VPNTable
|
||||
|
||||
|
||||
class L2VPNTerminationTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = L2VPNTerminationTable
|
||||
|
||||
14
netbox/wireless/tests/test_tables.py
Normal file
14
netbox/wireless/tests/test_tables.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from utilities.testing import TableTestCases
|
||||
from wireless.tables import *
|
||||
|
||||
|
||||
class WirelessLANGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = WirelessLANGroupTable
|
||||
|
||||
|
||||
class WirelessLANTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = WirelessLANTable
|
||||
|
||||
|
||||
class WirelessLinkTableTest(TableTestCases.StandardTableTestCase):
|
||||
table = WirelessLinkTable
|
||||
Reference in New Issue
Block a user