jeremystretch 4 лет назад
Родитель
Сommit
6a4becfb46

+ 8 - 1
netbox/dcim/filtersets.py

@@ -14,6 +14,7 @@ from utilities.filters import (
     TreeNodeMultipleChoiceFilter,
     TreeNodeMultipleChoiceFilter,
 )
 )
 from virtualization.models import Cluster
 from virtualization.models import Cluster
+from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
 from .choices import *
 from .choices import *
 from .constants import *
 from .constants import *
 from .models import *
 from .models import *
@@ -987,12 +988,18 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
         choices=InterfaceTypeChoices,
         choices=InterfaceTypeChoices,
         null_value=None
         null_value=None
     )
     )
+    rf_role = django_filters.MultipleChoiceFilter(
+        choices=WirelessRoleChoices
+    )
+    rf_channel = django_filters.MultipleChoiceFilter(
+        choices=WirelessChannelChoices
+    )
 
 
     class Meta:
     class Meta:
         model = Interface
         model = Interface
         fields = [
         fields = [
             'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel',
             'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel',
-            'rf_channel_width', 'description',
+            'rf_channel_frequency', 'rf_channel_width', 'description',
         ]
         ]
 
 
     def filter_device(self, queryset, name, value):
     def filter_device(self, queryset, name, value):

+ 23 - 4
netbox/dcim/tests/test_filtersets.py

@@ -9,6 +9,7 @@ from tenancy.models import Tenant, TenantGroup
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.testing import ChangeLoggedFilterSetTests
 from utilities.testing import ChangeLoggedFilterSetTests
 from virtualization.models import Cluster, ClusterType
 from virtualization.models import Cluster, ClusterType
+from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
 
 
 
 
 class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
 class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -2063,6 +2064,8 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
             Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
             Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
             Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
             Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
             Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
             Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
+            Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_AP, rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel_frequency=2412, rf_channel_width=22),
+            Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_STATION, rf_channel=WirelessChannelChoices.CHANNEL_5G_32, rf_channel_frequency=5160, rf_channel_width=20),
         )
         )
         Interface.objects.bulk_create(interfaces)
         Interface.objects.bulk_create(interfaces)
 
 
@@ -2083,11 +2086,11 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'connected': True}
         params = {'connected': True}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'connected': False}
         params = {'connected': False}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
     def test_enabled(self):
     def test_enabled(self):
         params = {'enabled': 'true'}
         params = {'enabled': 'true'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
         params = {'enabled': 'false'}
         params = {'enabled': 'false'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
@@ -2099,7 +2102,7 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'mgmt_only': 'true'}
         params = {'mgmt_only': 'true'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'mgmt_only': 'false'}
         params = {'mgmt_only': 'false'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
     def test_mode(self):
     def test_mode(self):
         params = {'mode': InterfaceModeChoices.MODE_ACCESS}
         params = {'mode': InterfaceModeChoices.MODE_ACCESS}
@@ -2176,7 +2179,7 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'cabled': 'true'}
         params = {'cabled': 'true'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'cabled': 'false'}
         params = {'cabled': 'false'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
     def test_kind(self):
     def test_kind(self):
         params = {'kind': 'physical'}
         params = {'kind': 'physical'}
@@ -2192,6 +2195,22 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]}
         params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
+    def test_rf_role(self):
+        params = {'rf_role': [WirelessRoleChoices.ROLE_AP, WirelessRoleChoices.ROLE_STATION]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_rf_channel(self):
+        params = {'rf_channel': [WirelessChannelChoices.CHANNEL_24G_1, WirelessChannelChoices.CHANNEL_5G_32]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_rf_channel_frequency(self):
+        params = {'rf_channel_frequency': [2412, 5160]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_rf_channel_width(self):
+        params = {'rf_channel_width': [22, 20]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
 
 
 class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
 class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = FrontPort.objects.all()
     queryset = FrontPort.objects.all()

+ 4 - 2
netbox/wireless/api/serializers.py

@@ -10,6 +10,7 @@ from wireless.models import *
 from .nested_serializers import *
 from .nested_serializers import *
 
 
 __all__ = (
 __all__ = (
+    'WirelessLANGroupSerializer',
     'WirelessLANSerializer',
     'WirelessLANSerializer',
     'WirelessLinkSerializer',
     'WirelessLinkSerializer',
 )
 )
@@ -17,7 +18,7 @@ __all__ = (
 
 
 class WirelessLANGroupSerializer(NestedGroupModelSerializer):
 class WirelessLANGroupSerializer(NestedGroupModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
-    parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
+    parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None)
     wirelesslan_count = serializers.IntegerField(read_only=True)
     wirelesslan_count = serializers.IntegerField(read_only=True)
 
 
     class Meta:
     class Meta:
@@ -30,6 +31,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer):
 
 
 class WirelessLANSerializer(PrimaryModelSerializer):
 class WirelessLANSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
+    group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
     vlan = NestedVLANSerializer(required=False, allow_null=True)
     vlan = NestedVLANSerializer(required=False, allow_null=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
@@ -37,7 +39,7 @@ class WirelessLANSerializer(PrimaryModelSerializer):
     class Meta:
     class Meta:
         model = WirelessLAN
         model = WirelessLAN
         fields = [
         fields = [
-            'id', 'url', 'display', 'ssid', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk',
+            'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk',
         ]
         ]
 
 
 
 

+ 15 - 2
netbox/wireless/filtersets.py

@@ -3,7 +3,9 @@ from django.db.models import Q
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
 from extras.filters import TagFilter
 from extras.filters import TagFilter
+from ipam.models import VLAN
 from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
+from utilities.filters import TreeNodeMultipleChoiceFilter
 from .choices import *
 from .choices import *
 from .models import *
 from .models import *
 
 
@@ -34,8 +36,19 @@ class WirelessLANFilterSet(PrimaryModelFilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
-    group_id = django_filters.ModelMultipleChoiceFilter(
-        queryset=WirelessLANGroup.objects.all()
+    group_id = TreeNodeMultipleChoiceFilter(
+        queryset=WirelessLANGroup.objects.all(),
+        field_name='group',
+        lookup_expr='in'
+    )
+    group = TreeNodeMultipleChoiceFilter(
+        queryset=WirelessLANGroup.objects.all(),
+        field_name='group',
+        lookup_expr='in',
+        to_field_name='slug'
+    )
+    vlan_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=VLAN.objects.all()
     )
     )
     auth_type = django_filters.MultipleChoiceFilter(
     auth_type = django_filters.MultipleChoiceFilter(
         choices=WirelessAuthTypeChoices
         choices=WirelessAuthTypeChoices

+ 1 - 0
netbox/wireless/forms/bulk_import.py

@@ -36,6 +36,7 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm):
     )
     )
     vlan = CSVModelChoiceField(
     vlan = CSVModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
+        required=False,
         to_field_name='name',
         to_field_name='name',
         help_text='Bridged VLAN'
         help_text='Bridged VLAN'
     )
     )

+ 2 - 0
netbox/wireless/forms/models.py

@@ -62,6 +62,7 @@ class WirelessLANForm(BootstrapMixin, CustomFieldModelForm):
 class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm):
 class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm):
     device_a = DynamicModelChoiceField(
     device_a = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
+        required=False,
         label='Device A',
         label='Device A',
         initial_params={
         initial_params={
             'interfaces': '$interface_a'
             'interfaces': '$interface_a'
@@ -78,6 +79,7 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm):
     )
     )
     device_b = DynamicModelChoiceField(
     device_b = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
+        required=False,
         label='Device B',
         label='Device B',
         initial_params={
         initial_params={
             'interfaces': '$interface_b'
             'interfaces': '$interface_b'

+ 6 - 6
netbox/wireless/graphql/schema.py

@@ -5,11 +5,11 @@ from .types import *
 
 
 
 
 class WirelessQuery(graphene.ObjectType):
 class WirelessQuery(graphene.ObjectType):
-    wirelesslan = ObjectField(WirelessLANType)
-    wirelesslan_list = ObjectListField(WirelessLANType)
+    wireless_lan = ObjectField(WirelessLANType)
+    wireless_lan_list = ObjectListField(WirelessLANType)
 
 
-    wirelesslangroup = ObjectField(WirelessLANGroupType)
-    wirelesslangroup_list = ObjectListField(WirelessLANGroupType)
+    wireless_lan_group = ObjectField(WirelessLANGroupType)
+    wireless_lan_group_list = ObjectListField(WirelessLANGroupType)
 
 
-    wirelesslink = ObjectField(WirelessLinkType)
-    wirelesslink_list = ObjectListField(WirelessLinkType)
+    wireless_link = ObjectField(WirelessLinkType)
+    wireless_link_list = ObjectListField(WirelessLinkType)

+ 15 - 3
netbox/wireless/graphql/types.py

@@ -1,5 +1,5 @@
 from wireless import filtersets, models
 from wireless import filtersets, models
-from netbox.graphql.types import ObjectType
+from netbox.graphql.types import ObjectType, PrimaryObjectType
 
 
 __all__ = (
 __all__ = (
     'WirelessLANType',
     'WirelessLANType',
@@ -16,17 +16,29 @@ class WirelessLANGroupType(ObjectType):
         filterset_class = filtersets.WirelessLANGroupFilterSet
         filterset_class = filtersets.WirelessLANGroupFilterSet
 
 
 
 
-class WirelessLANType(ObjectType):
+class WirelessLANType(PrimaryObjectType):
 
 
     class Meta:
     class Meta:
         model = models.WirelessLAN
         model = models.WirelessLAN
         fields = '__all__'
         fields = '__all__'
         filterset_class = filtersets.WirelessLANFilterSet
         filterset_class = filtersets.WirelessLANFilterSet
 
 
+    def resolve_auth_type(self, info):
+        return self.auth_type or None
 
 
-class WirelessLinkType(ObjectType):
+    def resolve_auth_cipher(self, info):
+        return self.auth_cipher or None
+
+
+class WirelessLinkType(PrimaryObjectType):
 
 
     class Meta:
     class Meta:
         model = models.WirelessLink
         model = models.WirelessLink
         fields = '__all__'
         fields = '__all__'
         filterset_class = filtersets.WirelessLinkFilterSet
         filterset_class = filtersets.WirelessLinkFilterSet
+
+    def resolve_auth_type(self, info):
+        return self.auth_type or None
+
+    def resolve_auth_cipher(self, info):
+        return self.auth_cipher or None

+ 0 - 1
netbox/wireless/signals.py

@@ -63,5 +63,4 @@ def nullify_connected_interfaces(instance, **kwargs):
 
 
     # Delete and retrace any dependent cable paths
     # Delete and retrace any dependent cable paths
     for cablepath in CablePath.objects.filter(path__contains=instance):
     for cablepath in CablePath.objects.filter(path__contains=instance):
-        print(f'Deleting cable path {cablepath.pk}')
         cablepath.delete()
         cablepath.delete()

+ 0 - 0
netbox/wireless/tests/__init__.py


+ 141 - 0
netbox/wireless/tests/test_api.py

@@ -0,0 +1,141 @@
+from django.urls import reverse
+
+from wireless.choices import *
+from wireless.models import *
+from dcim.choices import InterfaceTypeChoices
+from dcim.models import Interface
+from utilities.testing import APITestCase, APIViewTestCases, create_test_device
+
+
+class AppTest(APITestCase):
+
+    def test_root(self):
+        url = reverse('wireless-api:api-root')
+        response = self.client.get('{}?format=api'.format(url), **self.header)
+
+        self.assertEqual(response.status_code, 200)
+
+
+class WirelessLANGroupTest(APIViewTestCases.APIViewTestCase):
+    model = WirelessLANGroup
+    brief_fields = ['_depth', 'display', 'id', 'name', 'slug', 'url', 'wirelesslan_count']
+    create_data = [
+        {
+            'name': 'Wireless LAN Group 4',
+            'slug': 'wireless-lan-group-4',
+        },
+        {
+            'name': 'Wireless LAN Group 5',
+            'slug': 'wireless-lan-group-5',
+        },
+        {
+            'name': 'Wireless LAN Group 6',
+            'slug': 'wireless-lan-group-6',
+        },
+    ]
+    bulk_update_data = {
+        'description': 'New description',
+    }
+
+    @classmethod
+    def setUpTestData(cls):
+
+        WirelessLANGroup.objects.create(name='Wireless LAN Group 1', slug='wireless-lan-group-1')
+        WirelessLANGroup.objects.create(name='Wireless LAN Group 2', slug='wireless-lan-group-2')
+        WirelessLANGroup.objects.create(name='Wireless LAN Group 3', slug='wireless-lan-group-3')
+
+
+class WirelessLANTest(APIViewTestCases.APIViewTestCase):
+    model = WirelessLAN
+    brief_fields = ['display', 'id', 'ssid', 'url']
+
+    @classmethod
+    def setUpTestData(cls):
+
+        groups = (
+            WirelessLANGroup(name='Group 1', slug='group-1'),
+            WirelessLANGroup(name='Group 2', slug='group-2'),
+            WirelessLANGroup(name='Group 3', slug='group-3'),
+        )
+        for group in groups:
+            group.save()
+
+        wireless_lans = (
+            WirelessLAN(ssid='WLAN1'),
+            WirelessLAN(ssid='WLAN2'),
+            WirelessLAN(ssid='WLAN3'),
+        )
+        WirelessLAN.objects.bulk_create(wireless_lans)
+
+        cls.create_data = [
+            {
+                'ssid': 'WLAN4',
+                'group': groups[0].pk,
+                'auth_type': WirelessAuthTypeChoices.TYPE_OPEN,
+            },
+            {
+                'ssid': 'WLAN5',
+                'group': groups[1].pk,
+                'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
+            },
+            {
+                'ssid': 'WLAN6',
+                'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE,
+            },
+        ]
+
+        cls.bulk_update_data = {
+            'group': groups[2].pk,
+            'description': 'New description',
+            'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
+            'auth_cipher': WirelessAuthCipherChoices.CIPHER_AES,
+            'auth_psk': 'abc123def456',
+        }
+
+
+class WirelessLinkTest(APIViewTestCases.APIViewTestCase):
+    model = WirelessLink
+    brief_fields = ['display', 'id', 'ssid', 'url']
+    bulk_update_data = {
+        'status': 'planned',
+    }
+
+    @classmethod
+    def setUpTestData(cls):
+        device = create_test_device('test-device')
+        interfaces = [
+            Interface(
+                device=device,
+                name=f'radio{i}',
+                type=InterfaceTypeChoices.TYPE_80211AC,
+                rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
+                rf_channel_frequency=5160,
+                rf_channel_width=20
+            ) for i in range(12)
+        ]
+        Interface.objects.bulk_create(interfaces)
+
+        wireless_links = (
+            WirelessLink(ssid='LINK1', interface_a=interfaces[0], interface_b=interfaces[1]),
+            WirelessLink(ssid='LINK2', interface_a=interfaces[2], interface_b=interfaces[3]),
+            WirelessLink(ssid='LINK3', interface_a=interfaces[4], interface_b=interfaces[5]),
+        )
+        WirelessLink.objects.bulk_create(wireless_links)
+
+        cls.create_data = [
+            {
+                'interface_a': interfaces[6].pk,
+                'interface_b': interfaces[7].pk,
+                'ssid': 'LINK4',
+            },
+            {
+                'interface_a': interfaces[8].pk,
+                'interface_b': interfaces[9].pk,
+                'ssid': 'LINK5',
+            },
+            {
+                'interface_a': interfaces[10].pk,
+                'interface_b': interfaces[11].pk,
+                'ssid': 'LINK6',
+            },
+        ]

+ 194 - 0
netbox/wireless/tests/test_filtersets.py

@@ -0,0 +1,194 @@
+from django.test import TestCase
+
+from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
+from dcim.models import Interface
+from ipam.models import VLAN
+from wireless.choices import *
+from wireless.filtersets import *
+from wireless.models import *
+from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
+
+
+class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
+    queryset = WirelessLANGroup.objects.all()
+    filterset = WirelessLANGroupFilterSet
+
+    @classmethod
+    def setUpTestData(cls):
+
+        groups = (
+            WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1', description='A'),
+            WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2', description='B'),
+            WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3', description='C'),
+        )
+        for group in groups:
+            group.save()
+
+        child_groups = (
+            WirelessLANGroup(name='Wireless LAN Group 1A', slug='wireless-lan-group-1a', parent=groups[0]),
+            WirelessLANGroup(name='Wireless LAN Group 1B', slug='wireless-lan-group-1b', parent=groups[0]),
+            WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=groups[1]),
+            WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=groups[1]),
+            WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=groups[2]),
+            WirelessLANGroup(name='Wireless LAN Group 3B', slug='wireless-lan-group-3b', parent=groups[2]),
+        )
+        for group in child_groups:
+            group.save()
+
+    def test_name(self):
+        params = {'name': ['Wireless LAN Group 1', 'Wireless LAN Group 2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_slug(self):
+        params = {'slug': ['wireless-lan-group-1', 'wireless-lan-group-2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_description(self):
+        params = {'description': ['A', 'B']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_parent(self):
+        parent_groups = WirelessLANGroup.objects.filter(parent__isnull=True)[:2]
+        params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
+
+class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
+    queryset = WirelessLAN.objects.all()
+    filterset = WirelessLANFilterSet
+
+    @classmethod
+    def setUpTestData(cls):
+
+        groups = (
+            WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
+            WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'),
+            WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3'),
+        )
+        for group in groups:
+            group.save()
+
+        vlans = (
+            VLAN(name='VLAN1', vid=1),
+            VLAN(name='VLAN2', vid=2),
+            VLAN(name='VLAN3', vid=3),
+        )
+        VLAN.objects.bulk_create(vlans)
+
+        wireless_lans = (
+            WirelessLAN(ssid='WLAN1', group=groups[0], vlan=vlans[0], auth_type=WirelessAuthTypeChoices.TYPE_OPEN, auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, auth_psk='PSK1'),
+            WirelessLAN(ssid='WLAN2', group=groups[1], vlan=vlans[1], auth_type=WirelessAuthTypeChoices.TYPE_WEP, auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, auth_psk='PSK2'),
+            WirelessLAN(ssid='WLAN3', group=groups[2], vlan=vlans[2], auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, auth_psk='PSK3'),
+        )
+        WirelessLAN.objects.bulk_create(wireless_lans)
+
+    def test_ssid(self):
+        params = {'ssid': ['WLAN1', 'WLAN2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_group(self):
+        groups = WirelessLANGroup.objects.all()[:2]
+        params = {'group_id': [groups[0].pk, groups[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'group': [groups[0].slug, groups[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_vlan(self):
+        vlans = VLAN.objects.all()[:2]
+        params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_auth_type(self):
+        params = {'auth_type': [WirelessAuthTypeChoices.TYPE_OPEN, WirelessAuthTypeChoices.TYPE_WEP]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_auth_cipher(self):
+        params = {'auth_cipher': [WirelessAuthCipherChoices.CIPHER_AUTO, WirelessAuthCipherChoices.CIPHER_TKIP]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_auth_psk(self):
+        params = {'auth_psk': ['PSK1', 'PSK2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
+    queryset = WirelessLink.objects.all()
+    filterset = WirelessLinkFilterSet
+
+    @classmethod
+    def setUpTestData(cls):
+
+        devices = (
+            create_test_device('device1'),
+            create_test_device('device2'),
+            create_test_device('device3'),
+            create_test_device('device4'),
+        )
+
+        interfaces = (
+            Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_80211AC),
+            Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_80211AC),
+            Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_80211AC),
+            Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_80211AC),
+            Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_80211AC),
+            Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_80211AC),
+            Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC),
+            Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC),
+        )
+        Interface.objects.bulk_create(interfaces)
+
+        # Wireless links
+        WirelessLink(
+            interface_a=interfaces[0],
+            interface_b=interfaces[2],
+            ssid='LINK1',
+            status=LinkStatusChoices.STATUS_CONNECTED,
+            auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
+            auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
+            auth_psk='PSK1'
+        ).save()
+        WirelessLink(
+            interface_a=interfaces[1],
+            interface_b=interfaces[3],
+            ssid='LINK2',
+            status=LinkStatusChoices.STATUS_PLANNED,
+            auth_type=WirelessAuthTypeChoices.TYPE_WEP,
+            auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
+            auth_psk='PSK2'
+        ).save()
+        WirelessLink(
+            interface_a=interfaces[4],
+            interface_b=interfaces[6],
+            ssid='LINK3',
+            status=LinkStatusChoices.STATUS_DECOMMISSIONING,
+            auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
+            auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
+            auth_psk='PSK3'
+        ).save()
+        WirelessLink(
+            interface_a=interfaces[5],
+            interface_b=interfaces[7],
+            ssid='LINK4'
+        ).save()
+
+    def test_ssid(self):
+        params = {'ssid': ['LINK1', 'LINK2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_status(self):
+        params = {'status': [LinkStatusChoices.STATUS_PLANNED, LinkStatusChoices.STATUS_DECOMMISSIONING]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_auth_type(self):
+        params = {'auth_type': [WirelessAuthTypeChoices.TYPE_OPEN, WirelessAuthTypeChoices.TYPE_WEP]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_auth_cipher(self):
+        params = {'auth_cipher': [WirelessAuthCipherChoices.CIPHER_AUTO, WirelessAuthCipherChoices.CIPHER_TKIP]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_auth_psk(self):
+        params = {'auth_psk': ['PSK1', 'PSK2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 120 - 0
netbox/wireless/tests/test_views.py

@@ -0,0 +1,120 @@
+from wireless.choices import *
+from wireless.models import *
+from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
+from dcim.models import Interface
+from utilities.testing import ViewTestCases, create_tags, create_test_device
+
+
+class WirelessLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
+    model = WirelessLANGroup
+
+    @classmethod
+    def setUpTestData(cls):
+
+        groups = (
+            WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
+            WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'),
+            WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3'),
+        )
+        for group in groups:
+            group.save()
+
+        cls.form_data = {
+            'name': 'Wireless LAN Group X',
+            'slug': 'wireless-lan-group-x',
+            'parent': groups[2].pk,
+            'description': 'A new wireless LAN group',
+        }
+
+        cls.csv_data = (
+            "name,slug,description",
+            "Wireles sLAN Group 4,wireless-lan-group-4,Fourth wireless LAN group",
+            "Wireless LAN Group 5,wireless-lan-group-5,Fifth wireless LAN group",
+            "Wireless LAN Group 6,wireless-lan-group-6,Sixth wireless LAN group",
+        )
+
+        cls.bulk_edit_data = {
+            'description': 'New description',
+        }
+
+
+class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+    model = WirelessLAN
+
+    @classmethod
+    def setUpTestData(cls):
+
+        groups = (
+            WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
+            WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'),
+        )
+        for group in groups:
+            group.save()
+
+        WirelessLAN.objects.bulk_create([
+            WirelessLAN(group=groups[0], ssid='WLAN1'),
+            WirelessLAN(group=groups[0], ssid='WLAN2'),
+            WirelessLAN(group=groups[0], ssid='WLAN3'),
+        ])
+
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+        cls.form_data = {
+            'ssid': 'WLAN2',
+            'group': groups[1].pk,
+            'tags': [t.pk for t in tags],
+        }
+
+        cls.csv_data = (
+            "group,ssid",
+            "Wireless LAN Group 2,WLAN4",
+            "Wireless LAN Group 2,WLAN5",
+            "Wireless LAN Group 2,WLAN6",
+        )
+
+        cls.bulk_edit_data = {
+            'description': 'New description',
+        }
+
+
+class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+    model = WirelessLink
+
+    @classmethod
+    def setUpTestData(cls):
+        device = create_test_device('test-device')
+        interfaces = [
+            Interface(
+                device=device,
+                name=f'radio{i}',
+                type=InterfaceTypeChoices.TYPE_80211AC,
+                rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
+                rf_channel_frequency=5160,
+                rf_channel_width=20
+            ) for i in range(12)
+        ]
+        Interface.objects.bulk_create(interfaces)
+
+        WirelessLink(interface_a=interfaces[0], interface_b=interfaces[1], ssid='LINK1').save()
+        WirelessLink(interface_a=interfaces[2], interface_b=interfaces[3], ssid='LINK2').save()
+        WirelessLink(interface_a=interfaces[4], interface_b=interfaces[5], ssid='LINK3').save()
+
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+        cls.form_data = {
+            'interface_a': interfaces[6].pk,
+            'interface_b': interfaces[7].pk,
+            'status': LinkStatusChoices.STATUS_PLANNED,
+            'tags': [t.pk for t in tags],
+        }
+
+        cls.csv_data = (
+            "interface_a,interface_b,status",
+            f"{interfaces[6].pk},{interfaces[7].pk},connected",
+            f"{interfaces[8].pk},{interfaces[9].pk},connected",
+            f"{interfaces[10].pk},{interfaces[11].pk},connected",
+        )
+
+        cls.bulk_edit_data = {
+            'status': LinkStatusChoices.STATUS_PLANNED,
+        }