From 11d3a3c7f8f7086ced8a30130bfeb06cad1dd624 Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Fri, 28 Nov 2025 14:40:50 +0100 Subject: [PATCH 1/8] [change] Add controller config and connection apps --- .../tests/test_admin.py | 733 ++++++------ .../tests/test_models.py | 154 +-- .../tests/test_selenium.py | 374 +++--- .../tests/test_tasks.py | 56 +- tests/media/.gitignore | 3 + tests/media/floorplan.jpg | 0 tests/openwisp2/sample_config/__init__.py | 0 tests/openwisp2/sample_config/admin.py | 12 + tests/openwisp2/sample_config/api/views.py | 112 ++ tests/openwisp2/sample_config/apps.py | 9 + .../fixtures/test_templates.json | 50 + .../sample_config/migrations/0001_initial.py | 1048 +++++++++++++++++ .../0002_default_groups_permissions.py | 25 + .../0003_name_unique_per_organization.py | 25 + .../migrations/0004_devicegroup_templates.py | 31 + .../0005_add_organizationalloweddevice.py | 18 + ...ice__is_deactivated_alter_config_status.py | 41 + .../migrations/0007_alter_config_status.py | 39 + .../sample_config/migrations/__init__.py | 0 tests/openwisp2/sample_config/models.py | 113 ++ tests/openwisp2/sample_config/pytest.py | 10 + tests/openwisp2/sample_config/tests.py | 143 +++ tests/openwisp2/sample_config/views.py | 65 + tests/openwisp2/sample_connection/__init__.py | 0 tests/openwisp2/sample_connection/admin.py | 1 + .../sample_connection/api/__init__.py | 0 .../openwisp2/sample_connection/api/views.py | 50 + tests/openwisp2/sample_connection/apps.py | 9 + .../migrations/0001_initial.py | 292 +++++ .../0002_default_group_permissions.py | 19 + .../0003_name_unique_per_organization.py | 20 + .../sample_connection/migrations/__init__.py | 0 tests/openwisp2/sample_connection/models.py | 29 + tests/openwisp2/sample_connection/pytest.py | 10 + tests/openwisp2/sample_connection/tests.py | 71 ++ .../sample_firmware_upgrader/tests.py | 24 +- tests/openwisp2/settings.py | 30 +- tests/openwisp2/urls.py | 48 +- 38 files changed, 3024 insertions(+), 640 deletions(-) create mode 100644 tests/media/.gitignore create mode 100644 tests/media/floorplan.jpg create mode 100644 tests/openwisp2/sample_config/__init__.py create mode 100644 tests/openwisp2/sample_config/admin.py create mode 100644 tests/openwisp2/sample_config/api/views.py create mode 100644 tests/openwisp2/sample_config/apps.py create mode 100644 tests/openwisp2/sample_config/fixtures/test_templates.json create mode 100644 tests/openwisp2/sample_config/migrations/0001_initial.py create mode 100644 tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py create mode 100644 tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py create mode 100644 tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py create mode 100644 tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py create mode 100644 tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py create mode 100644 tests/openwisp2/sample_config/migrations/0007_alter_config_status.py create mode 100644 tests/openwisp2/sample_config/migrations/__init__.py create mode 100644 tests/openwisp2/sample_config/models.py create mode 100644 tests/openwisp2/sample_config/pytest.py create mode 100644 tests/openwisp2/sample_config/tests.py create mode 100644 tests/openwisp2/sample_config/views.py create mode 100644 tests/openwisp2/sample_connection/__init__.py create mode 100644 tests/openwisp2/sample_connection/admin.py create mode 100644 tests/openwisp2/sample_connection/api/__init__.py create mode 100644 tests/openwisp2/sample_connection/api/views.py create mode 100644 tests/openwisp2/sample_connection/apps.py create mode 100644 tests/openwisp2/sample_connection/migrations/0001_initial.py create mode 100644 tests/openwisp2/sample_connection/migrations/0002_default_group_permissions.py create mode 100644 tests/openwisp2/sample_connection/migrations/0003_name_unique_per_organization.py create mode 100644 tests/openwisp2/sample_connection/migrations/__init__.py create mode 100644 tests/openwisp2/sample_connection/models.py create mode 100644 tests/openwisp2/sample_connection/pytest.py create mode 100644 tests/openwisp2/sample_connection/tests.py diff --git a/openwisp_firmware_upgrader/tests/test_admin.py b/openwisp_firmware_upgrader/tests/test_admin.py index f4fd7ed6a..f9378f9f7 100644 --- a/openwisp_firmware_upgrader/tests/test_admin.py +++ b/openwisp_firmware_upgrader/tests/test_admin.py @@ -46,7 +46,9 @@ class MockRequest: class BaseTestAdmin(TestMultitenantAdminMixin, TestUpgraderMixin): - app_label = "firmware_upgrader" + app_label = 'firmware_upgrader' + config_app_label = 'config' + connection_app_label = 'connection' _device_params = TestConfigAdmin._device_params.copy() _device_params.update( { @@ -106,7 +108,9 @@ def setUp(self, *args, **kwargs): self.factory = RequestFactory() def make_device_admin_request(self, pk): - return self.factory.get(reverse("admin:config_device_change", args=[pk])) + return self.factory.get( + reverse(f'admin:{self.config_app_label}_device_change', args=[pk]) + ) @property def build_list_url(self): @@ -185,8 +189,10 @@ def test_view_device_administrator(self): device_fw = self._create_device_firmware() org = self._get_org() self._create_administrator(organizations=[org]) - self._login(username="administrator", password="tester") - url = reverse("admin:config_device_change", args=[device_fw.device_id]) + self._login(username='administrator', password='tester') + url = reverse( + f'admin:{self.config_app_label}_device_change', args=[device_fw.device_id] + ) r = self.client.get(url) self.assertContains(r, str(device_fw.image_id)) @@ -359,7 +365,7 @@ def test_save_device_with_deleted_devicefirmware(self): ) FirmwareImage.objects.all().delete() response = self.client.post( - reverse("admin:config_device_change", args=[device.id]), + reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]), data=device_params, follow=True, ) @@ -377,7 +383,7 @@ def test_device_firmware_upgrade_without_device_connection( device = device_fw.device device.deviceconnection_set.all().delete() response = self.client.get( - reverse("admin:config_device_change", args=[device.id]) + reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) ) self.assertNotIn( "'NoneType' object has no attribute 'update_strategy'", @@ -391,7 +397,7 @@ def test_deactivated_firmware_image_inline(self): device = self._create_config(organization=self._get_org()).device device.deactivate() response = self.client.get( - reverse("admin:config_device_change", args=[device.id]) + reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) ) # Check that it is not possible to add a DeviceFirmwareImage to a # deactivated device in the admin interface. @@ -402,7 +408,7 @@ def test_deactivated_firmware_image_inline(self): ) self._create_device_firmware(device=device) response = self.client.get( - reverse("admin:config_device_change", args=[device.id]) + reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) ) # Ensure that a deactivated device's existing DeviceFirmwareImage # is displayed as readonly in the admin interface. @@ -423,7 +429,7 @@ def test_device_upgrade_shared_firmware(self, *args): device = self._create_device_with_connection() device_conn = device.deviceconnection_set.first() device_params = self._get_device_params(device, device_conn, shared_image) - path = reverse("admin:config_device_change", args=[device.id]) + path = reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) with self.subTest("Test with administrator account"): self.client.force_login(administrator) @@ -510,394 +516,431 @@ def test_admin_multitenancy(self): ) -_mock_upgrade = "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade" -_mock_connect = "openwisp_controller.connection.models.DeviceConnection.connect" - - -@mock.patch(_mock_upgrade, return_value=True) -@mock.patch(_mock_connect, return_value=True) class TestAdminTransaction( BaseTestAdmin, AdminActionPermTestMixin, TransactionTestCase ): + _mock_upgrade = 'openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade' + _mock_connect = 'openwisp_controller.connection.models.DeviceConnection.connect' + + @mock.patch(_mock_upgrade, return_value=True) def test_upgrade_selected_action_perms(self, *args): - env = self._create_upgrade_env() - org = env["d1"].organization - self._create_firmwareless_device(organization=org) - user = self._create_user(is_staff=True) - self._create_org_user(user=user, organization=org, is_admin=True) - # The user is redirected to the BatchUpgradeOperation page after success operation. - # Thus, we need to add the permission to the user. - user.user_permissions.add( - Permission.objects.get( - codename=f"change_{BatchUpgradeOperation._meta.model_name}" + with mock.patch(self._mock_connect, return_value=True): + env = self._create_upgrade_env() + org = env["d1"].organization + self._create_firmwareless_device(organization=org) + user = self._create_user(is_staff=True) + self._create_org_user(user=user, organization=org, is_admin=True) + # The user is redirected to the BatchUpgradeOperation page after success operation. + # Thus, we need to add the permission to the user. + user.user_permissions.add( + Permission.objects.get( + codename=f"change_{BatchUpgradeOperation._meta.model_name}" + ) ) - ) - self._test_action_permission( - path=self.build_list_url, - action="upgrade_selected", - user=user, - obj=env["build1"], - message=( - "You can track the progress of this mass upgrade operation " - "in this page. Refresh the page from time to time to check " - "its progress." - ), - required_perms=["change"], - extra_payload={ - "upgrade_all": "upgrade_all", - "upgrade_options": '{"c": true}', - }, - ) - - def test_upgrade_related(self, *args): - self._login() - env = self._create_upgrade_env() - self._create_firmwareless_device(organization=env["d1"].organization) - # check state is good before proceeding - fw = DeviceFirmware.objects.filter( - image__build_id=env["build2"].pk - ).select_related("image") - self.assertEqual(Device.objects.count(), 3) - self.assertEqual(UpgradeOperation.objects.count(), 0) - self.assertEqual(fw.count(), 0) - - with self.subTest("Invalid upgrade_options"): - response = self.client.post( - self.build_list_url, - { - "action": "upgrade_selected", - "upgrade_related": "upgrade_related", - "upgrade_options": "invalid", - ACTION_CHECKBOX_NAME: (env["build2"].pk,), + self._test_action_permission( + path=self.build_list_url, + action="upgrade_selected", + user=user, + obj=env["build1"], + message=( + "You can track the progress of this mass upgrade operation " + "in this page. Refresh the page from time to time to check " + "its progress." + ), + required_perms=["change"], + extra_payload={ + "upgrade_all": "upgrade_all", + "upgrade_options": '{"c": true}', }, - follow=True, ) - id_attr = ( - ' id="id_upgrade_options_error"' if django.VERSION >= (5, 2) else "" + + @mock.patch(_mock_upgrade, return_value=True) + def test_upgrade_related(self, *args): + with mock.patch(self._mock_connect, return_value=True): + self._login() + env = self._create_upgrade_env() + self._create_firmwareless_device(organization=env["d1"].organization) + # check state is good before proceeding + fw = DeviceFirmware.objects.filter( + image__build_id=env["build2"].pk + ).select_related("image") + self.assertEqual(Device.objects.count(), 3) + self.assertEqual(UpgradeOperation.objects.count(), 0) + self.assertEqual(fw.count(), 0) + + with self.subTest("Invalid upgrade_options"): + response = self.client.post( + self.build_list_url, + { + "action": "upgrade_selected", + "upgrade_related": "upgrade_related", + "upgrade_options": "invalid", + ACTION_CHECKBOX_NAME: (env["build2"].pk,), + }, + follow=True, + ) + id_attr = ( + ' id="id_upgrade_options_error"' if django.VERSION >= (5, 2) else "" + ) + self.assertContains( + response, + f'', + ) + + with self.subTest("Test with valid upgrade_options"): + r = self.client.post( + self.build_list_url, + { + "action": "upgrade_selected", + "upgrade_related": "upgrade_related", + "upgrade_options": '{"c": true}', + ACTION_CHECKBOX_NAME: (env["build2"].pk,), + }, + follow=True, + ) + self.assertContains(r, '
  • ') + self.assertContains(r, "track the progress") + self.assertEqual( + UpgradeOperation.objects.filter( + upgrade_options={'c': True} + ).count(), + 2, + ) + self.assertEqual(fw.count(), 2) + + @mock.patch(_mock_upgrade, return_value=True) + def test_upgrade_all(self, *args): + with mock.patch(self._mock_connect, return_value=True): + self._login() + env = self._create_upgrade_env() + self._create_firmwareless_device(organization=env["d1"].organization) + # check state is good before proceeding + fw = DeviceFirmware.objects.filter( + image__build_id=env["build2"].pk + ).select_related("image") + self.assertEqual(Device.objects.count(), 3) + self.assertEqual(UpgradeOperation.objects.count(), 0) + self.assertEqual(fw.count(), 0) + + with self.subTest("Invalid upgrade_options"): + response = self.client.post( + self.build_list_url, + { + "action": "upgrade_selected", + "upgrade_all": "upgrade_all", + "upgrade_options": "invalid", + ACTION_CHECKBOX_NAME: (env["build2"].pk,), + }, + follow=True, + ) + self.assertEqual(response.status_code, 200) + id_attr = ( + ' id="id_upgrade_options_error"' if django.VERSION >= (5, 2) else "" + ) + self.assertContains( + response, + f'', + ) + + with self.subTest("Test with valid upgrade_options"): + response = self.client.post( + self.build_list_url, + { + "action": "upgrade_selected", + "upgrade_all": "upgrade_all", + "upgrade_options": '{"c": true}', + ACTION_CHECKBOX_NAME: (env["build2"].pk,), + }, + follow=True, + ) + self.assertContains(response, '
  • ') + self.assertContains(response, "track the progress") + self.assertEqual( + UpgradeOperation.objects.filter( + upgrade_options={'c': True} + ).count(), + 3, + ) + self.assertEqual(fw.count(), 3) + self.assertContains( + response, + ( + '
      ' + '
    • yes' + "Attempt to preserve all changed files in /etc/ (-c)
    • " + '
    • no' + "Attempt to preserve all changed files in /, except those from " + "packages but including changed confs. (-o)
    • " + '
    • no' + "Do not save configuration over reflash (-n)
    • " + '
    • no' + "Skip from backup files that are equal to those in /rom (-u)
    • " + '
    • no' + "Do not attempt to restore the partition table after flash. (-p)
    • " + '
    • no' + "Include in backup a list of current installed packages at " + "/etc/backup/installed_packages.txt (-k)
    • " + '
    • no' + "Flash image even if image checks fail, this is dangerous! (-F)
    " + ), + html=True, + ) + + @mock.patch(_mock_upgrade, return_value=True) + def test_mass_upgrade_shared_image(self, *args): + with mock.patch(self._mock_connect, return_value=True): + self._login() + shared_image = self._create_firmware_image(organization=None) + shared_build = shared_image.build + self._create_device_with_connection( + organization=self._create_org(name="org1"), + model=shared_image.boards[0], ) - self.assertContains( - response, - f'', + self._create_device_with_connection( + organization=self._create_org(name="org2"), + model=shared_image.boards[0], ) + fw = DeviceFirmware.objects.filter( + image__build_id=shared_build.pk + ).select_related("image") + self.assertEqual(Device.objects.count(), 2) + self.assertEqual(UpgradeOperation.objects.count(), 0) + self.assertEqual(fw.count(), 0) - with self.subTest("Test with valid upgrade_options"): - r = self.client.post( + response = self.client.post( self.build_list_url, { "action": "upgrade_selected", - "upgrade_related": "upgrade_related", + "upgrade_all": "upgrade_all", "upgrade_options": '{"c": true}', - ACTION_CHECKBOX_NAME: (env["build2"].pk,), + ACTION_CHECKBOX_NAME: (shared_build.pk,), }, follow=True, ) - self.assertContains(r, '
  • ') - self.assertContains(r, "track the progress") + self.assertContains(response, '
  • ') + self.assertContains(response, "track the progress") self.assertEqual( UpgradeOperation.objects.filter(upgrade_options={"c": True}).count(), 2 ) self.assertEqual(fw.count(), 2) - def test_upgrade_all(self, *args): - self._login() - env = self._create_upgrade_env() - self._create_firmwareless_device(organization=env["d1"].organization) - # check state is good before proceeding - fw = DeviceFirmware.objects.filter( - image__build_id=env["build2"].pk - ).select_related("image") - self.assertEqual(Device.objects.count(), 3) - self.assertEqual(UpgradeOperation.objects.count(), 0) - self.assertEqual(fw.count(), 0) - - with self.subTest("Invalid upgrade_options"): - response = self.client.post( - self.build_list_url, - { - "action": "upgrade_selected", - "upgrade_all": "upgrade_all", - "upgrade_options": "invalid", - ACTION_CHECKBOX_NAME: (env["build2"].pk,), - }, - follow=True, + @mock.patch(_mock_upgrade, return_value=True) + def test_massive_upgrade_operation_page(self, *args): + with mock.patch(self._mock_connect, return_value=True): + self.test_upgrade_all() + uo = UpgradeOperation.objects.first() + url = reverse( + f'admin:{self.app_label}_batchupgradeoperation_change', + args=[uo.batch.pk], ) - self.assertEqual(response.status_code, 200) - id_attr = ( - ' id="id_upgrade_options_error"' if django.VERSION >= (5, 2) else "" + response = self.client.get(url) + self.assertContains(response, "Success rate") + self.assertContains(response, "Failure rate") + self.assertContains(response, "Abortion rate") + + @mock.patch(_mock_upgrade, return_value=True) + def test_recent_upgrades(self, *args): + with mock.patch(self._mock_connect, return_value=True): + self._login() + env = self._create_upgrade_env() + url = reverse( + f'admin:{self.config_app_label}_device_change', args=[env['d2'].pk] ) - self.assertContains( - response, - f'', + r = self.client.get(url) + self.assertNotContains(r, "Recent Firmware Upgrades") + env["build2"].batch_upgrade(firmwareless=True) + r = self.client.get(url) + self.assertContains(r, "Recent Firmware Upgrades") + + @mock.patch(_mock_upgrade, return_value=True) + def test_upgrade_operation_inline(self, *args): + with mock.patch(self._mock_connect, return_value=True): + device_fw = self._create_device_firmware() + device_fw.save(upgrade=True) + device = device_fw.device + request = self.make_device_admin_request(device.pk) + request.user = User.objects.first() + deviceadmin = DeviceAdmin(model=Device, admin_site=admin.site) + self.assertNotIn( + DeviceUpgradeOperationInline, deviceadmin.get_inlines(request, obj=None) + ) + self.assertIn( + DeviceUpgradeOperationInline, + deviceadmin.get_inlines(request, obj=device), ) - with self.subTest("Test with valid upgrade_options"): + @mock.patch(_mock_upgrade, return_value=True) + def test_upgrade_operation_inline_queryset(self, *args): + with mock.patch(self._mock_connect, return_value=True): + device_fw = self._create_device_firmware() + device_fw.save(upgrade=True) + # expect only 1 + uo = device_fw.device.upgradeoperation_set.get() + device = device_fw.device + request = self.make_device_admin_request(device.pk) + request.user = User.objects.first() + inline = DeviceUpgradeOperationInline(Device, admin.site) + qs = inline.get_queryset(request) + self.assertEqual(qs.count(), 1) + self.assertIn(uo, qs) + uo.created = localtime() - timedelta(days=30) + uo.modified = uo.created + uo.save() + qs = inline.get_queryset(request) + self.assertEqual(qs.count(), 0) + + @mock.patch(_mock_upgrade, return_value=True) + def test_device_firmware_upgrade_options(self, *args): + with mock.patch(self._mock_connect, return_value=True): + self._login() + device_fw = self._create_device_firmware() + device = device_fw.device + device_conn = device.deviceconnection_set.first() + build = self._create_build(version="0.2") + image = self._create_firmware_image(build=build) + upgrade_options = { + "c": True, + "o": False, + "u": False, + "n": False, + "p": False, + "k": False, + "F": True, + } + device_params = self._get_device_params( + device, device_conn, image, device_fw, json.dumps(upgrade_options) + ) response = self.client.post( - self.build_list_url, - { - "action": "upgrade_selected", - "upgrade_all": "upgrade_all", - "upgrade_options": '{"c": true}', - ACTION_CHECKBOX_NAME: (env["build2"].pk,), - }, + reverse( + f'admin:{self.config_app_label}_device_change', args=[device.id] + ), + data=device_params, follow=True, ) - self.assertContains(response, '
  • ') - self.assertContains(response, "track the progress") - self.assertEqual( - UpgradeOperation.objects.filter(upgrade_options={"c": True}).count(), 3 - ) - self.assertEqual(fw.count(), 3) + self.assertEqual(response.status_code, 200) + self.assertEqual(device.upgradeoperation_set.count(), 1) + upgrade_operation = device.upgradeoperation_set.first() + self.assertEqual(upgrade_operation.upgrade_options, upgrade_options) self.assertContains( response, ( - '
      ' - '
    • yes' + '
      • ' + 'yes' "Attempt to preserve all changed files in /etc/ (-c)
      • " '
      • no' - "Attempt to preserve all changed files in /, except those from " - "packages but including changed confs. (-o)
      • " - '
      • no' - "Do not save configuration over reflash (-n)
      • " - '
      • no' - "Skip from backup files that are equal to those in /rom (-u)
      • " + "Attempt to preserve all changed files in /, except those from packages " + "but including changed confs. (-o)" + '
      • Do not save configuration over reflash (-n)
      • ' + '
      • noSkip from backup files ' + "that are equal to those in /rom (-u)
      • " '
      • no' "Do not attempt to restore the partition table after flash. (-p)
      • " '
      • no' "Include in backup a list of current installed packages at " "/etc/backup/installed_packages.txt (-k)
      • " - '
      • no' + '
      • yes' "Flash image even if image checks fail, this is dangerous! (-F)
      " ), html=True, ) - def test_mass_upgrade_shared_image(self, *args): - self._login() - shared_image = self._create_firmware_image(organization=None) - shared_build = shared_image.build - self._create_device_with_connection( - organization=self._create_org(name="org1"), - model=shared_image.boards[0], - ) - self._create_device_with_connection( - organization=self._create_org(name="org2"), - model=shared_image.boards[0], - ) - fw = DeviceFirmware.objects.filter( - image__build_id=shared_build.pk - ).select_related("image") - self.assertEqual(Device.objects.count(), 2) - self.assertEqual(UpgradeOperation.objects.count(), 0) - self.assertEqual(fw.count(), 0) - - response = self.client.post( - self.build_list_url, - { - "action": "upgrade_selected", - "upgrade_all": "upgrade_all", - "upgrade_options": '{"c": true}', - ACTION_CHECKBOX_NAME: (shared_build.pk,), - }, - follow=True, - ) - self.assertContains(response, '
    • ') - self.assertContains(response, "track the progress") - self.assertEqual( - UpgradeOperation.objects.filter(upgrade_options={"c": True}).count(), 2 - ) - self.assertEqual(fw.count(), 2) - - def test_massive_upgrade_operation_page(self, *args): - self.test_upgrade_all() - uo = UpgradeOperation.objects.first() - url = reverse( - f"admin:{self.app_label}_batchupgradeoperation_change", args=[uo.batch.pk] - ) - response = self.client.get(url) - self.assertContains(response, "Success rate") - self.assertContains(response, "Failure rate") - self.assertContains(response, "Abortion rate") - - def test_recent_upgrades(self, *args): - self._login() - env = self._create_upgrade_env() - url = reverse("admin:config_device_change", args=[env["d2"].pk]) - r = self.client.get(url) - self.assertNotContains(r, "Recent Firmware Upgrades") - env["build2"].batch_upgrade(firmwareless=True) - r = self.client.get(url) - self.assertContains(r, "Recent Firmware Upgrades") - - def test_upgrade_operation_inline(self, *args): - device_fw = self._create_device_firmware() - device_fw.save(upgrade=True) - device = device_fw.device - request = self.make_device_admin_request(device.pk) - request.user = User.objects.first() - deviceadmin = DeviceAdmin(model=Device, admin_site=admin.site) - self.assertNotIn( - DeviceUpgradeOperationInline, deviceadmin.get_inlines(request, obj=None) - ) - self.assertIn( - DeviceUpgradeOperationInline, deviceadmin.get_inlines(request, obj=device) - ) - - def test_upgrade_operation_inline_queryset(self, *args): - device_fw = self._create_device_firmware() - device_fw.save(upgrade=True) - # expect only 1 - uo = device_fw.device.upgradeoperation_set.get() - device = device_fw.device - request = self.make_device_admin_request(device.pk) - request.user = User.objects.first() - inline = DeviceUpgradeOperationInline(Device, admin.site) - qs = inline.get_queryset(request) - self.assertEqual(qs.count(), 1) - self.assertIn(uo, qs) - uo.created = localtime() - timedelta(days=30) - uo.modified = uo.created - uo.save() - qs = inline.get_queryset(request) - self.assertEqual(qs.count(), 0) - - def test_device_firmware_upgrade_options(self, *args): - self._login() - device_fw = self._create_device_firmware() - device = device_fw.device - device_conn = device.deviceconnection_set.first() - build = self._create_build(version="0.2") - image = self._create_firmware_image(build=build) - upgrade_options = { - "c": True, - "o": False, - "u": False, - "n": False, - "p": False, - "k": False, - "F": True, - } - device_params = self._get_device_params( - device, device_conn, image, device_fw, json.dumps(upgrade_options) - ) - response = self.client.post( - reverse("admin:config_device_change", args=[device.id]), - data=device_params, - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(device.upgradeoperation_set.count(), 1) - upgrade_operation = device.upgradeoperation_set.first() - self.assertEqual(upgrade_operation.upgrade_options, upgrade_options) - self.assertContains( - response, - ( - '
      • ' - 'yes' - "Attempt to preserve all changed files in /etc/ (-c)
      • " - '
      • no' - "Attempt to preserve all changed files in /, except those from packages " - "but including changed confs. (-o)
      • " - '
      • Do not save configuration over reflash (-n)
      • ' - '
      • noSkip from backup files ' - "that are equal to those in /rom (-u)
      • " - '
      • no' - "Do not attempt to restore the partition table after flash. (-p)
      • " - '
      • no' - "Include in backup a list of current installed packages at " - "/etc/backup/installed_packages.txt (-k)
      • " - '
      • yes' - "Flash image even if image checks fail, this is dangerous! (-F)
      " - ), - html=True, - ) - + @mock.patch(_mock_upgrade, return_value=True) @mock.patch.object(OpenWisp1, "SCHEMA", None) def test_using_upgrade_options_with_unsupported_upgrader(self, *args): - self._login() - device_fw = self._create_device_firmware() - device = device_fw.device - device.config.backend = "netjsonconfig.OpenWisp" - device.config.save() - device_conn = device.deviceconnection_set.first() - device_conn.update_strategy = conn_settings.DEFAULT_UPDATE_STRATEGIES[1][0] - device_conn.save() - build = self._create_build(version="0.2") - image = self._create_firmware_image(build=build) - upgrade_options = { - "c": True, - "o": False, - "u": False, - "n": False, - "p": False, - "k": False, - "F": True, - } - - device_params = self._get_device_params( - device, device_conn, image, device_fw, json.dumps(upgrade_options) - ) - device_params.update( - { - "model": device.model, - "devicefirmware-0-image": str(image.id), - "devicefirmware-0-id": str(device_fw.id), - "devicefirmware-0-upgrade_options": json.dumps(upgrade_options), - "organization": str(device.organization.id), - "config-0-id": str(device.config.pk), - "config-0-device": str(device.id), - "deviceconnection_set-0-credentials": str(device_conn.credentials_id), - "deviceconnection_set-0-id": str(device_conn.id), - "deviceconnection_set-0-update_strategy": ( - conn_settings.DEFAULT_UPDATE_STRATEGIES[1][0] - ), - "deviceconnection_set-0-enabled": True, - "devicefirmware-TOTAL_FORMS": 1, - "devicefirmware-INITIAL_FORMS": 1, - "upgradeoperation_set-TOTAL_FORMS": 0, - "upgradeoperation_set-INITIAL_FORMS": 0, - "upgradeoperation_set-MIN_NUM_FORMS": 0, - "upgradeoperation_set-MAX_NUM_FORMS": 0, - "_continue": True, + with mock.patch(self._mock_connect, return_value=True): + self._login() + device_fw = self._create_device_firmware() + device = device_fw.device + device.config.backend = "netjsonconfig.OpenWisp" + device.config.save() + device_conn = device.deviceconnection_set.first() + device_conn.update_strategy = conn_settings.DEFAULT_UPDATE_STRATEGIES[1][0] + device_conn.save() + build = self._create_build(version="0.2") + image = self._create_firmware_image(build=build) + upgrade_options = { + "c": True, + "o": False, + "u": False, + "n": False, + "p": False, + "k": False, + "F": True, } - ) - with self.subTest("Test DeviceFirmwareInline does not have schema defined"): - response = self.client.get( - reverse("admin:config_device_change", args=[device.id]) + device_params = self._get_device_params( + device, device_conn, image, device_fw, json.dumps(upgrade_options) ) - self.assertContains( - response, "" + device_params.update( + { + 'model': device.model, + 'devicefirmware-0-image': str(image.id), + 'devicefirmware-0-id': str(device_fw.id), + 'devicefirmware-0-upgrade_options': json.dumps(upgrade_options), + 'organization': str(device.organization.id), + 'config-0-id': str(device.config.pk), + 'config-0-device': str(device.id), + 'deviceconnection_set-0-credentials': str( + device_conn.credentials_id + ), + 'deviceconnection_set-0-id': str(device_conn.id), + 'deviceconnection_set-0-update_strategy': ( + conn_settings.DEFAULT_UPDATE_STRATEGIES[1][0] + ), + "deviceconnection_set-0-enabled": True, + "devicefirmware-TOTAL_FORMS": 1, + "devicefirmware-INITIAL_FORMS": 1, + "upgradeoperation_set-TOTAL_FORMS": 0, + "upgradeoperation_set-INITIAL_FORMS": 0, + "upgradeoperation_set-MIN_NUM_FORMS": 0, + "upgradeoperation_set-MAX_NUM_FORMS": 0, + "_continue": True, + } ) - with self.subTest("Test using upgrade options with unsupported upgrader"): - response = self.client.post( - reverse("admin:config_device_change", args=[device.id]), - data=device_params, - follow=True, - ) - self.assertContains( - response, - ( - '
      • Using upgrade ' - "options is not allowed with this upgrader.
      " - ), - ) + with self.subTest('Test DeviceFirmwareInline does not have schema defined'): + response = self.client.get( + reverse( + f'admin:{self.config_app_label}_device_change', args=[device.id] + ) + ) + self.assertContains( + response, '' + ) - with self.subTest("Test upgrading without upgrade options"): - del device_params["devicefirmware-0-upgrade_options"] - response = self.client.post( - reverse("admin:config_device_change", args=[device.id]), - data=device_params, - follow=True, - ) - self.assertContains( - response, - ( - '
      Upgrade options are ' - "not supported for this upgrader.
      " - ), - ) + with self.subTest('Test using upgrade options with unsupported upgrader'): + response = self.client.post( + reverse( + f'admin:{self.config_app_label}_device_change', args=[device.id] + ), + data=device_params, + follow=True, + ) + self.assertContains( + response, + ( + '
      • Using upgrade ' + 'options is not allowed with this upgrader.
      ' + ), + ) + + with self.subTest('Test upgrading without upgrade options'): + del device_params['devicefirmware-0-upgrade_options'] + response = self.client.post( + reverse( + f'admin:{self.config_app_label}_device_change', args=[device.id] + ), + data=device_params, + follow=True, + ) + self.assertContains( + response, + ( + '
      Upgrade options are ' + 'not supported for this upgrader.
      ' + ), + ) del TestConfigAdmin diff --git a/openwisp_firmware_upgrader/tests/test_models.py b/openwisp_firmware_upgrader/tests/test_models.py index 054ea2b5c..72bf8a3c2 100644 --- a/openwisp_firmware_upgrader/tests/test_models.py +++ b/openwisp_firmware_upgrader/tests/test_models.py @@ -489,69 +489,69 @@ class TestModelsTransaction(TestUpgraderMixin, TransactionTestCase): image_type = TestModels.image_type @mock.patch(_mock_updrade, return_value=True) - @mock.patch(_mock_connect, return_value=True) def test_dry_run(self, *args): - env = self._create_upgrade_env() - # check pending upgrades - result = BatchUpgradeOperation.dry_run(build=env["build1"]) - self.assertEqual( - list(result["device_firmwares"]), - list(DeviceFirmware.objects.all().order_by("-created")), - ) - self.assertEqual(list(result["devices"]), []) - # upgrade devices - env["build1"].batch_upgrade(firmwareless=True) - # check pending upgrades again - result = BatchUpgradeOperation.dry_run(build=env["build1"]) - self.assertEqual(list(result["device_firmwares"]), []) - self.assertEqual(list(result["devices"]), []) + with mock.patch(self._mock_connect, return_value=True): + env = self._create_upgrade_env() + # check pending upgrades + result = BatchUpgradeOperation.dry_run(build=env["build1"]) + self.assertEqual( + list(result["device_firmwares"]), + list(DeviceFirmware.objects.all().order_by("-created")), + ) + self.assertEqual(list(result["devices"]), []) + # upgrade devices + env["build1"].batch_upgrade(firmwareless=True) + # check pending upgrades again + result = BatchUpgradeOperation.dry_run(build=env["build1"]) + self.assertEqual(list(result["device_firmwares"]), []) + self.assertEqual(list(result["devices"]), []) @mock.patch(_mock_updrade, return_value=True) - @mock.patch(_mock_connect, return_value=True) def test_upgrade_related_devices(self, *args): - env = self._create_upgrade_env() - # check everything is as expected - self.assertEqual(UpgradeOperation.objects.count(), 0) - self.assertEqual(env["d1"].devicefirmware.image, env["image1a"]) - self.assertEqual(env["d2"].devicefirmware.image, env["image1b"]) - # upgrade all related - env["build2"].batch_upgrade(firmwareless=False) - # ensure image is changed - env["d1"].devicefirmware.refresh_from_db() - env["d2"].devicefirmware.refresh_from_db() - self.assertEqual(env["d1"].devicefirmware.image, env["image2a"]) - self.assertEqual(env["d2"].devicefirmware.image, env["image2b"]) - # ensure upgrade operation objects have been created - self.assertEqual(UpgradeOperation.objects.count(), 2) - batch_qs = BatchUpgradeOperation.objects.all() - self.assertEqual(batch_qs.count(), 1) - batch = batch_qs.first() - self.assertEqual(batch.upgradeoperation_set.count(), 2) - self.assertEqual(batch.build, env["build2"]) - self.assertEqual(batch.status, "success") + with mock.patch(self._mock_connect, return_value=True): + env = self._create_upgrade_env() + # check everything is as expected + self.assertEqual(UpgradeOperation.objects.count(), 0) + self.assertEqual(env["d1"].devicefirmware.image, env["image1a"]) + self.assertEqual(env["d2"].devicefirmware.image, env["image1b"]) + # upgrade all related + env["build2"].batch_upgrade(firmwareless=False) + # ensure image is changed + env["d1"].devicefirmware.refresh_from_db() + env["d2"].devicefirmware.refresh_from_db() + self.assertEqual(env["d1"].devicefirmware.image, env["image2a"]) + self.assertEqual(env["d2"].devicefirmware.image, env["image2b"]) + # ensure upgrade operation objects have been created + self.assertEqual(UpgradeOperation.objects.count(), 2) + batch_qs = BatchUpgradeOperation.objects.all() + self.assertEqual(batch_qs.count(), 1) + batch = batch_qs.first() + self.assertEqual(batch.upgradeoperation_set.count(), 2) + self.assertEqual(batch.build, env["build2"]) + self.assertEqual(batch.status, "success") @mock.patch(_mock_updrade, return_value=True) - @mock.patch(_mock_connect, return_value=True) def test_upgrade_firmwareless_devices(self, *args): - env = self._create_upgrade_env(device_firmware=False) - # check everything is as expected - self.assertEqual(UpgradeOperation.objects.count(), 0) - self.assertFalse(hasattr(env["d1"], "devicefirmware")) - self.assertFalse(hasattr(env["d2"], "devicefirmware")) - # upgrade all related - env["build2"].batch_upgrade(firmwareless=True) - env["d1"].refresh_from_db() - env["d2"].refresh_from_db() - self.assertEqual(env["d1"].devicefirmware.image, env["image2a"]) - self.assertEqual(env["d2"].devicefirmware.image, env["image2b"]) - # ensure upgrade operation objects have been created - self.assertEqual(UpgradeOperation.objects.count(), 2) - batch_qs = BatchUpgradeOperation.objects.all() - self.assertEqual(batch_qs.count(), 1) - batch = batch_qs.first() - self.assertEqual(batch.upgradeoperation_set.count(), 2) - self.assertEqual(batch.build, env["build2"]) - self.assertEqual(batch.status, "success") + with mock.patch(self._mock_connect, return_value=True): + env = self._create_upgrade_env(device_firmware=False) + # check everything is as expected + self.assertEqual(UpgradeOperation.objects.count(), 0) + self.assertFalse(hasattr(env["d1"], "devicefirmware")) + self.assertFalse(hasattr(env["d2"], "devicefirmware")) + # upgrade all related + env["build2"].batch_upgrade(firmwareless=True) + env["d1"].refresh_from_db() + env["d2"].refresh_from_db() + self.assertEqual(env["d1"].devicefirmware.image, env["image2a"]) + self.assertEqual(env["d2"].devicefirmware.image, env["image2b"]) + # ensure upgrade operation objects have been created + self.assertEqual(UpgradeOperation.objects.count(), 2) + batch_qs = BatchUpgradeOperation.objects.all() + self.assertEqual(batch_qs.count(), 1) + batch = batch_qs.first() + self.assertEqual(batch.upgradeoperation_set.count(), 2) + self.assertEqual(batch.build, env["build2"]) + self.assertEqual(batch.status, "success") @mock.patch.object(upgrade_firmware, "max_retries", 0) def test_batch_upgrade_failure(self): @@ -563,28 +563,28 @@ def test_batch_upgrade_failure(self): self.assertEqual(BatchUpgradeOperation.objects.count(), 1) @mock.patch(_mock_updrade, return_value=True) - @mock.patch(_mock_connect, return_value=True) def test_upgrade_related_devices_existing_fw(self, *args): - env = self._create_upgrade_env() - self.assertEqual(UpgradeOperation.objects.count(), 0) - self.assertEqual(env["d1"].devicefirmware.image, env["image1a"]) - self.assertEqual(env["d2"].devicefirmware.image, env["image1b"]) - env["d1"].devicefirmware.installed = False - env["d1"].devicefirmware.save(upgrade=False) - env["d2"].devicefirmware.installed = False - env["d2"].devicefirmware.save(upgrade=False) - env["build1"].batch_upgrade(firmwareless=False) - env["d1"].devicefirmware.refresh_from_db() - env["d2"].devicefirmware.refresh_from_db() - self.assertEqual(env["d1"].devicefirmware.image, env["image1a"]) - self.assertEqual(env["d2"].devicefirmware.image, env["image1b"]) - self.assertEqual(UpgradeOperation.objects.count(), 2) - batch_qs = BatchUpgradeOperation.objects.all() - self.assertEqual(batch_qs.count(), 1) - batch = batch_qs.first() - self.assertEqual(batch.upgradeoperation_set.count(), 2) - self.assertEqual(batch.build, env["build1"]) - self.assertEqual(batch.status, "success") + with mock.patch(self._mock_connect, return_value=True): + env = self._create_upgrade_env() + self.assertEqual(UpgradeOperation.objects.count(), 0) + self.assertEqual(env["d1"].devicefirmware.image, env["image1a"]) + self.assertEqual(env["d2"].devicefirmware.image, env["image1b"]) + env["d1"].devicefirmware.installed = False + env["d1"].devicefirmware.save(upgrade=False) + env["d2"].devicefirmware.installed = False + env["d2"].devicefirmware.save(upgrade=False) + env["build1"].batch_upgrade(firmwareless=False) + env["d1"].devicefirmware.refresh_from_db() + env["d2"].devicefirmware.refresh_from_db() + self.assertEqual(env["d1"].devicefirmware.image, env["image1a"]) + self.assertEqual(env["d2"].devicefirmware.image, env["image1b"]) + self.assertEqual(UpgradeOperation.objects.count(), 2) + batch_qs = BatchUpgradeOperation.objects.all() + self.assertEqual(batch_qs.count(), 1) + batch = batch_qs.first() + self.assertEqual(batch.upgradeoperation_set.count(), 2) + self.assertEqual(batch.build, env["build1"]) + self.assertEqual(batch.status, "success") def test_upgrade_retried(self): env = self._create_upgrade_env() diff --git a/openwisp_firmware_upgrader/tests/test_selenium.py b/openwisp_firmware_upgrader/tests/test_selenium.py index 1fadb6aa8..729f26612 100644 --- a/openwisp_firmware_upgrader/tests/test_selenium.py +++ b/openwisp_firmware_upgrader/tests/test_selenium.py @@ -27,10 +27,11 @@ @tag("selenium_tests") class TestDeviceAdmin(TestUpgraderMixin, SeleniumTestMixin, StaticLiveServerTestCase): - config_app_label = "config" - firmware_app_label = "firmware_upgrader" - os = "OpenWrt 19.07-SNAPSHOT r11061-6ffd4d8a4d" - image_type = REVERSE_FIRMWARE_IMAGE_MAP["YunCore XD3200"] + config_app_label = 'config' + firmware_app_label = 'firmware_upgrader' + os = 'OpenWrt 19.07-SNAPSHOT r11061-6ffd4d8a4d' + image_type = REVERSE_FIRMWARE_IMAGE_MAP['YunCore XD3200'] + _mock_connect = 'openwisp_controller.connection.models.DeviceConnection.connect' def _set_up_env(self): org = self._get_org() @@ -99,9 +100,10 @@ def test_restoring_deleted_device(self): By.XPATH, '//*[@id="device_form"]/div/div[1]/input[1]' ).click() try: - WebDriverWait(self.web_driver, 5).until( - EC.url_to_be(f"{self.live_server_url}/admin/config/device/") + device_changelist_url = self.live_server_url + reverse( + f"admin:{self.config_app_label}_device_changelist" ) + WebDriverWait(self.web_driver, 5).until(EC.url_to_be(device_changelist_url)) except TimeoutException: self.fail("Deleted device was not restored") @@ -114,200 +116,92 @@ def test_restoring_deleted_device(self): "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade", return_value=True, ) - @patch( - "openwisp_controller.connection.models.DeviceConnection.connect", - return_value=True, - ) def test_device_firmware_upgrade_options(self, *args): - def save_device(): - self.find_element( - by=By.XPATH, value='//*[@id="device_form"]/div/div[1]/input[3]' - ).click() - self.wait_for_visibility(By.CSS_SELECTOR, "#devicefirmware-group") - self.hide_loading_overlay() + with patch(self._mock_connect, return_value=True): - _, _, _, _, _, image, device = self._set_up_env() - self.login() - self.open( - "{}#devicefirmware-group".format( - reverse( - f"admin:{self.config_app_label}_device_change", args=[device.id] - ) - ) - ) - self.hide_loading_overlay() - # JSONSchema Editor should not be rendered without a change in the image field - self.wait_for_invisibility( - By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" - ) - image_select = self._get_device_firmware_dropdown_select() - image_select.select_by_value(str(image.pk)) - # JSONSchema configuration editor should not be rendered - self.wait_for_invisibility( - By.XPATH, - '//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]/div/h3/span[4]/input', - ) - # Select "None" image should hide JSONSchema Editor - image_select.select_by_value("") - self.wait_for_invisibility( - By.CSS_SELECTOR, "#id_devicefirmware-0-upgrade_options_jsoneditor" - ) - - # Select "build2" image - image_select.select_by_value(str(image.pk)) - # Enable '-c' option - self.find_element( - by=By.XPATH, - value='//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]' - "/div/div[2]/div/div/div[1]/div/div[1]/label/input", - ).click() - # Enable '-F' option - self.find_element( - by=By.XPATH, - value='//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]' - "/div/div[2]/div/div/div[7]/div/div[1]/label/input", - ).click() - save_device() + def save_device(): + self.find_element( + by=By.XPATH, value='//*[@id="device_form"]/div/div[1]/input[3]' + ).click() + self.wait_for_visibility(By.CSS_SELECTOR, '#devicefirmware-group') + self.hide_loading_overlay() - # Delete DeviceFirmware - self.find_element(By.CSS_SELECTOR, "#id_devicefirmware-0-DELETE").click() - save_device() - - # When adding firmware to the device for the first time, - # JSONSchema editor should be rendered only when the image - # is selected - self.find_element( - by=By.XPATH, value='//*[@id="devicefirmware-group"]/fieldset/div[2]/a' - ).click() - # JSONSchema Editor should not be rendered without a change in the image field - self.wait_for_invisibility( - By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" - ) - image_select = self._get_device_firmware_dropdown_select() - image_select.select_by_index(1) - self.wait_for_visibility( - By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" - ) - save_device() - - @capture_any_output() - @patch( - "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade", - return_value=True, - ) - @patch( - "openwisp_controller.connection.models.DeviceConnection.connect", - return_value=True, - ) - def test_batch_upgrade_upgrade_options(self, *args): - _, _, _, build2, _, _, _ = self._set_up_env() - self.login() - self.open( - reverse(f"admin:{self.firmware_app_label}_build_change", args=[build2.id]) - ) - # Launch mass upgrade operation - self.find_element( - by=By.CSS_SELECTOR, - value='.title-wrapper .object-tools form button[type="submit"]', - ).click() - - # Ensure JSONSchema form is rendered - self.wait_for_visibility(By.CSS_SELECTOR, ".jsoneditor-wrapper") - # JSONSchema configuration editor should not be rendered - self.wait_for_invisibility( - By.XPATH, - '//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]/div/h3/span[4]/input', - ) - # Disable -c flag - self.find_element( - by=By.XPATH, - value='//*[@id="id_upgrade_options_jsoneditor"]/div/div[2]/div/div/div[1]/div/div[1]/label/input', - ).click() - # Enable -n flag - self.find_element( - by=By.XPATH, - value='//*[@id="id_upgrade_options_jsoneditor"]/div/div[2]/div/div/div[3]/div/div[1]/label/input', - ).click() - # Upgrade all devices - self.find_element(by=By.CSS_SELECTOR, value='input[name="upgrade_all"]').click() - try: - WebDriverWait(self.web_driver, 5).until( - EC.url_contains("batchupgradeoperation") - ) - except TimeoutException: - self.fail("User was not redirected to Mass upgrade operations page") - self.assertEqual( - BatchUpgradeOperation.objects.filter( - upgrade_options={ - "c": False, - "o": False, - "n": True, - "u": False, - "p": False, - "k": False, - "F": False, - } - ).count(), - 1, - ) - self.assertEqual( - UpgradeOperation.objects.filter( - upgrade_options={ - "c": False, - "o": False, - "n": True, - "u": False, - "p": False, - "k": False, - "F": False, - } - ).count(), - 1, - ) - - @capture_any_output() - @patch( - "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade", - return_value=True, - ) - @patch( - "openwisp_controller.connection.models.DeviceConnection.connect", - return_value=True, - ) - @patch.object(OpenWrt, "SCHEMA", None) - def test_upgrader_with_unsupported_upgrade_options(self, *args): - org, category, build1, build2, image1, image2, device = self._set_up_env() - self.login() - - with self.subTest("Test DeviceFirmware"): + _, _, _, _, _, image, device = self._set_up_env() + self.login() self.open( - "{}#devicefirmware-group".format( + '{}#devicefirmware-group'.format( reverse( - f"admin:{self.config_app_label}_device_change", args=[device.id] + f'admin:{self.config_app_label}_device_change', args=[device.id] ) ) ) self.hide_loading_overlay() + # JSONSchema Editor should not be rendered without a change in the image field + self.wait_for_invisibility( + By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + ) image_select = self._get_device_firmware_dropdown_select() - image_select.select_by_value(str(image2.pk)) - # Ensure JSONSchema editor is not rendered because - # the upgrader does not define a schema + image_select.select_by_value(str(image.pk)) + # JSONSchema configuration editor should not be rendered self.wait_for_invisibility( - By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" + By.XPATH, + '//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]/div/h3/span[4]/input', ) + # Select "None" image should hide JSONSchema Editor + image_select.select_by_value('') + self.wait_for_invisibility( + By.CSS_SELECTOR, '#id_devicefirmware-0-upgrade_options_jsoneditor' + ) + + # Select "build2" image + image_select.select_by_value(str(image.pk)) + # Enable '-c' option self.find_element( - by=By.XPATH, value='//*[@id="device_form"]/div/div[1]/input[3]' + by=By.XPATH, + value='//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]' + '/div/div[2]/div/div/div[1]/div/div[1]/label/input', ).click() - self.assertEqual( - UpgradeOperation.objects.filter(upgrade_options={}).count(), 1 + # Enable '-F' option + self.find_element( + by=By.XPATH, + value='//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]' + '/div/div[2]/div/div/div[7]/div/div[1]/label/input', + ).click() + save_device() + + # Delete DeviceFirmware + self.find_element(By.CSS_SELECTOR, '#id_devicefirmware-0-DELETE').click() + save_device() + + # When adding firmware to the device for the first time, + # JSONSchema editor should be rendered only when the image + # is selected + self.find_element( + by=By.XPATH, value='//*[@id="devicefirmware-group"]/fieldset/div[2]/a' + ).click() + # JSONSchema Editor should not be rendered without a change in the image field + self.wait_for_invisibility( + By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + ) + image_select = self._get_device_firmware_dropdown_select() + image_select.select_by_index(1) + self.wait_for_visibility( + By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' ) - DeviceFirmware.objects.all().delete() - UpgradeOperation.objects.all().delete() + save_device() - with self.subTest("Test BatchUpgradeOperation"): + @capture_any_output() + @patch( + "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade", + return_value=True, + ) + def test_batch_upgrade_upgrade_options(self, *args): + with patch(self._mock_connect, return_value=True): + _, _, _, build2, _, _, _ = self._set_up_env() + self.login() self.open( reverse( - f"admin:{self.firmware_app_label}_build_change", args=[build2.id] + f'admin:{self.firmware_app_label}_build_change', args=[build2.id] ) ) # Launch mass upgrade operation @@ -315,15 +209,123 @@ def test_upgrader_with_unsupported_upgrade_options(self, *args): by=By.CSS_SELECTOR, value='.title-wrapper .object-tools form button[type="submit"]', ).click() - # Ensure JSONSchema editor is not rendered because - # the upgrader does not define a schema + + # Ensure JSONSchema form is rendered + self.wait_for_visibility(By.CSS_SELECTOR, '.jsoneditor-wrapper') + # JSONSchema configuration editor should not be rendered self.wait_for_invisibility( - By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" + By.XPATH, + '//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]/div/h3/span[4]/input', ) + # Disable -c flag + self.find_element( + by=By.XPATH, + value='//*[@id="id_upgrade_options_jsoneditor"]' + "/div/div[2]/div/div/div[1]/div/div[1]/label/input", + ).click() + # Enable -n flag + self.find_element( + by=By.XPATH, + value='//*[@id="id_upgrade_options_jsoneditor"]' + "/div/div[2]/div/div/div[3]/div/div[1]/label/input", + ).click() # Upgrade all devices self.find_element( by=By.CSS_SELECTOR, value='input[name="upgrade_all"]' ).click() + try: + WebDriverWait(self.web_driver, 5).until( + EC.url_contains('batchupgradeoperation') + ) + except TimeoutException: + self.fail('User was not redirected to Mass upgrade operations page') self.assertEqual( - UpgradeOperation.objects.filter(upgrade_options={}).count(), 1 + BatchUpgradeOperation.objects.filter( + upgrade_options={ + 'c': False, + 'o': False, + 'n': True, + 'u': False, + 'p': False, + 'k': False, + 'F': False, + } + ).count(), + 1, ) + self.assertEqual( + UpgradeOperation.objects.filter( + upgrade_options={ + 'c': False, + 'o': False, + 'n': True, + 'u': False, + 'p': False, + 'k': False, + 'F': False, + } + ).count(), + 1, + ) + + @capture_any_output() + @patch( + "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade", + return_value=True, + ) + @patch.object(OpenWrt, 'SCHEMA', None) + def test_upgrader_with_unsupported_upgrade_options(self, *args): + with patch(self._mock_connect, return_value=True): + org, category, build1, build2, image1, image2, device = self._set_up_env() + self.login() + + with self.subTest('Test DeviceFirmware'): + self.open( + '{}#devicefirmware-group'.format( + reverse( + f'admin:{self.config_app_label}_device_change', + args=[device.id], + ) + ) + ) + self.hide_loading_overlay() + image_select = self._get_device_firmware_dropdown_select() + image_select.select_by_value(str(image2.pk)) + # Ensure JSONSchema editor is not rendered because + # the upgrader does not define a schema + self.wait_for_invisibility( + By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + ) + self.find_element( + by=By.XPATH, value='//*[@id="device_form"]/div/div[1]/input[3]' + ).click() + self.assertEqual( + UpgradeOperation.objects.filter(upgrade_options={}).count(), 1 + ) + DeviceFirmware.objects.all().delete() + UpgradeOperation.objects.all().delete() + + with self.subTest('Test BatchUpgradeOperation'): + self.open( + reverse( + f'admin:{self.firmware_app_label}_build_change', + args=[build2.id], + ) + ) + # Launch mass upgrade operation + self.find_element( + by=By.CSS_SELECTOR, + value='.title-wrapper .object-tools form button[type="submit"]', + ).click() + # Ensure JSONSchema editor is not rendered because + # the upgrader does not define a schema + self.wait_for_invisibility( + By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + ) + # Upgrade all devices + self.find_element( + by=By.CSS_SELECTOR, value='input[name="upgrade_all"]' + ).click() + self.assertEqual( + UpgradeOperation.objects.filter(upgrade_options={}).count(), 1 + ) diff --git a/openwisp_firmware_upgrader/tests/test_tasks.py b/openwisp_firmware_upgrader/tests/test_tasks.py index 3908573cd..3673bd603 100644 --- a/openwisp_firmware_upgrader/tests/test_tasks.py +++ b/openwisp_firmware_upgrader/tests/test_tasks.py @@ -18,51 +18,51 @@ class TestTasks(TestUpgraderMixin, TransactionTestCase): _mock_connect = "openwisp_controller.connection.models.DeviceConnection.connect" @mock.patch(_mock_upgrade, side_effect=SoftTimeLimitExceeded()) - @mock.patch(_mock_connect, return_value=True) @mock.patch( "openwisp_firmware_upgrader.base.models.AbstractUpgradeOperation.upgrade", side_effect=SoftTimeLimitExceeded(), ) @capture_any_output() def test_upgrade_firmware_timeout(self, *args): - device_fw = self._create_device_firmware(upgrade=True) - self.assertEqual(UpgradeOperation.objects.count(), 1) - uo = device_fw.image.upgradeoperation_set.first() - self.assertEqual(uo.status, "failed") - self.assertIn("Operation timed out.", uo.log) + with mock.patch(self._mock_connect, return_value=True): + device_fw = self._create_device_firmware(upgrade=True) + self.assertEqual(UpgradeOperation.objects.count(), 1) + uo = device_fw.image.upgradeoperation_set.first() + self.assertEqual(uo.status, 'failed') + self.assertIn('Operation timed out.', uo.log) @mock.patch(_mock_upgrade, return_value=True) - @mock.patch(_mock_connect, return_value=True) @mock.patch( "openwisp_firmware_upgrader.base.models.AbstractDeviceFirmware.create_upgrade_operation", side_effect=SoftTimeLimitExceeded(), ) @capture_any_output() def test_batch_upgrade_timeout(self, *args): - env = self._create_upgrade_env() - batch = BatchUpgradeOperation.objects.create(build=env["build2"]) - # will be executed synchronously due to CELERY_IS_EAGER = True - tasks.batch_upgrade_operation.delay(batch_id=batch.pk, firmwareless=False) - self.assertEqual(BatchUpgradeOperation.objects.count(), 1) - batch = BatchUpgradeOperation.objects.first() - self.assertEqual(batch.status, "failed") + with mock.patch(self._mock_connect, return_value=True): + env = self._create_upgrade_env() + batch = BatchUpgradeOperation.objects.create(build=env['build2']) + # will be executed synchronously due to CELERY_IS_EAGER = True + tasks.batch_upgrade_operation.delay(batch_id=batch.pk, firmwareless=False) + self.assertEqual(BatchUpgradeOperation.objects.count(), 1) + batch = BatchUpgradeOperation.objects.first() + self.assertEqual(batch.status, 'failed') @mock.patch(_mock_upgrade, return_value=True) - @mock.patch(_mock_connect, return_value=True) - @mock.patch("logging.Logger.warning") + @mock.patch('logging.Logger.warning') def test_upgrade_firmware_resilience(self, mocked_logger, *args): - upgrade_op_id = UpgradeOperation().id - tasks.upgrade_firmware.run(upgrade_op_id) - mocked_logger.assert_called_with( - f"The UpgradeOperation object with id {upgrade_op_id} has been deleted" - ) + with mock.patch(self._mock_connect, return_value=True): + upgrade_op_id = UpgradeOperation().id + tasks.upgrade_firmware.run(upgrade_op_id) + mocked_logger.assert_called_with( + f'The UpgradeOperation object with id {upgrade_op_id} has been deleted' + ) @mock.patch(_mock_upgrade, return_value=True) - @mock.patch(_mock_connect, return_value=True) - @mock.patch("logging.Logger.warning") + @mock.patch('logging.Logger.warning') def test_batch_upgrade_operation_resilience(self, mocked_logger, *args): - batch_id = BatchUpgradeOperation().id - tasks.batch_upgrade_operation.run(batch_id=batch_id, firmwareless=False) - mocked_logger.assert_called_with( - f"The BatchUpgradeOperation object with id {batch_id} has been deleted" - ) + with mock.patch(self._mock_connect, return_value=True): + batch_id = BatchUpgradeOperation().id + tasks.batch_upgrade_operation.run(batch_id=batch_id, firmwareless=False) + mocked_logger.assert_called_with( + f'The BatchUpgradeOperation object with id {batch_id} has been deleted' + ) diff --git a/tests/media/.gitignore b/tests/media/.gitignore new file mode 100644 index 000000000..ae48598e8 --- /dev/null +++ b/tests/media/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!floorplan.jpg diff --git a/tests/media/floorplan.jpg b/tests/media/floorplan.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openwisp2/sample_config/__init__.py b/tests/openwisp2/sample_config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openwisp2/sample_config/admin.py b/tests/openwisp2/sample_config/admin.py new file mode 100644 index 000000000..5cc36ad3f --- /dev/null +++ b/tests/openwisp2/sample_config/admin.py @@ -0,0 +1,12 @@ +from openwisp_controller.config.admin import ( + DeviceAdmin, + DeviceGroupAdmin, + TemplateAdmin, + VpnAdmin, +) + +# Monkey Patching done only for testing purposes +DeviceAdmin.fields += ["details"] +TemplateAdmin.fields += ["details"] +VpnAdmin.fields += ["details"] +DeviceGroupAdmin.fields += ["details"] diff --git a/tests/openwisp2/sample_config/api/views.py b/tests/openwisp2/sample_config/api/views.py new file mode 100644 index 000000000..daa8e2feb --- /dev/null +++ b/tests/openwisp2/sample_config/api/views.py @@ -0,0 +1,112 @@ +from openwisp_controller.config.api.download_views import ( + DownloadDeviceView as BaseDownloadDeviceView, +) +from openwisp_controller.config.api.download_views import ( + DownloadTemplateconfiguration as BaseDownloadTemplateconfiguration, +) +from openwisp_controller.config.api.download_views import ( + DownloadVpnView as BaseDownloadVpnView, +) +from openwisp_controller.config.api.views import ( + DeviceActivateView as BaseDeviceActivateView, +) +from openwisp_controller.config.api.views import ( + DeviceDeactivateView as BaseDeviceDeactivateView, +) +from openwisp_controller.config.api.views import ( + DeviceDetailView as BaseDeviceDetailView, +) +from openwisp_controller.config.api.views import ( + DeviceGroupCommonName as BaseDeviceGroupCommonName, +) +from openwisp_controller.config.api.views import ( + DeviceGroupDetailView as BaseDeviceGroupDetailView, +) +from openwisp_controller.config.api.views import ( + DeviceGroupListCreateView as BaseDeviceGroupListCreateView, +) +from openwisp_controller.config.api.views import ( + DeviceListCreateView as BaseDeviceListCreateView, +) +from openwisp_controller.config.api.views import ( + TemplateDetailView as BaseTemplateDetailView, +) +from openwisp_controller.config.api.views import ( + TemplateListCreateView as BaseTemplateListCreateView, +) +from openwisp_controller.config.api.views import VpnDetailView as BaseVpnDetailView +from openwisp_controller.config.api.views import ( + VpnListCreateView as BaseVpnListCreateView, +) + + +class TemplateListCreateView(BaseTemplateListCreateView): + pass + + +class TemplateDetailView(BaseTemplateDetailView): + pass + + +class DownloadTemplateconfiguration(BaseDownloadTemplateconfiguration): + pass + + +class VpnListCreateView(BaseVpnListCreateView): + pass + + +class VpnDetailView(BaseVpnDetailView): + pass + + +class DownloadVpnView(BaseDownloadVpnView): + pass + + +class DeviceListCreateView(BaseDeviceListCreateView): + pass + + +class DeviceDetailView(BaseDeviceDetailView): + pass + + +class DeviceActivateView(BaseDeviceActivateView): + pass + + +class DeviceDeactivateView(BaseDeviceDeactivateView): + pass + + +class DeviceGroupListCreateView(BaseDeviceGroupListCreateView): + pass + + +class DeviceGroupDetailView(BaseDeviceGroupDetailView): + pass + + +class DeviceGroupCommonName(BaseDeviceGroupCommonName): + pass + + +class DownloadDeviceView(BaseDownloadDeviceView): + pass + + +template_list = TemplateListCreateView.as_view() +template_detail = TemplateDetailView.as_view() +download_template_config = DownloadTemplateconfiguration.as_view() +vpn_list = VpnListCreateView.as_view() +vpn_detail = VpnDetailView.as_view() +download_vpn_config = DownloadVpnView.as_view() +device_list = DeviceListCreateView.as_view() +device_detail = DeviceDetailView.as_view() +device_activate = DeviceActivateView.as_view() +device_deactivate = DeviceDeactivateView.as_view() +download_device_config = DownloadDeviceView().as_view() +devicegroup_list = DeviceGroupListCreateView.as_view() +devicegroup_detail = DeviceGroupDetailView.as_view() +devicegroup_commonname = DeviceGroupCommonName.as_view() diff --git a/tests/openwisp2/sample_config/apps.py b/tests/openwisp2/sample_config/apps.py new file mode 100644 index 000000000..993967a7e --- /dev/null +++ b/tests/openwisp2/sample_config/apps.py @@ -0,0 +1,9 @@ +from openwisp_controller.config.apps import ConfigConfig + + +class SampleConfigConfig(ConfigConfig): + name = "openwisp2.sample_config" + label = "sample_config" + + +del ConfigConfig diff --git a/tests/openwisp2/sample_config/fixtures/test_templates.json b/tests/openwisp2/sample_config/fixtures/test_templates.json new file mode 100644 index 000000000..b33fc2a75 --- /dev/null +++ b/tests/openwisp2/sample_config/fixtures/test_templates.json @@ -0,0 +1,50 @@ +[ + { + "pk": "d083b494-8e16-4054-9537-fb9eba914861", + "model": "sample_config.template", + "fields": { + "name": "dhcp", + "backend": "netjsonconfig.OpenWrt", + "config": { + "interfaces": [ + { + "name": "eth0", + "type": "ethernet", + "addresses": [ + { + "proto": "dhcp", + "family": "ipv4" + } + ] + } + ] + }, + "created": "2015-05-16T20:02:52.483Z", + "modified": "2015-05-16T19:33:41.621Z" + } + }, + { + "pk": "d083b494-8e16-4054-9537-fb9eba914862", + "model": "sample_config.template", + "fields": { + "name": "radio0", + "backend": "netjsonconfig.OpenWrt", + "config": { + "radios": [ + { + "name": "radio0", + "phy": "phy0", + "driver": "mac80211", + "protocol": "802.11n", + "channel": 11, + "channel_width": 20, + "tx_power": 8, + "country": "IT" + } + ] + }, + "created": "2015-05-16T20:02:52.483Z", + "modified": "2015-05-16T19:33:41.621Z" + } + } +] diff --git a/tests/openwisp2/sample_config/migrations/0001_initial.py b/tests/openwisp2/sample_config/migrations/0001_initial.py new file mode 100644 index 000000000..13b368684 --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0001_initial.py @@ -0,0 +1,1048 @@ +# Generated by Django 3.0.7 on 2020-06-27 11:16 +import collections +import re +import uuid + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import jsonfield.fields +import model_utils.fields +import swapper +import taggit.managers +from django.conf import settings +from django.db import migrations, models + +import openwisp_controller.config.base.template +import openwisp_users.mixins +import openwisp_utils.base +import openwisp_utils.utils +from openwisp_controller.config import settings as app_settings +from openwisp_controller.config.base.template import default_auto_cert + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("pki", "0007_default_groups_permissions"), + swapper.dependency( + *swapper.split(settings.AUTH_USER_MODEL), version="0004_default_groups" + ), + migrations.swappable_dependency(settings.OPENWISP_IPAM_IPADDRESS_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Config", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "backend", + models.CharField( + choices=app_settings.BACKENDS, + help_text=( + 'Select netjsonconfig backend' + ), + max_length=128, + verbose_name="backend", + ), + ), + ( + "config", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"ensure_ascii": False, "indent": 4}, + help_text="configuration in NetJSON DeviceConfiguration format", + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="configuration", + ), + ), + ( + "status", + model_utils.fields.StatusField( + choices=[ + ("modified", "modified"), + ("applied", "applied"), + ("error", "error"), + ], + default="modified", + help_text=( + '"modified" means the configuration is not applied yet; \n' + '"applied" means the configuration is applied successfully;' + " \n" + '"error" means the configuration caused issues ' + "and it was rolled back;" + ), + max_length=100, + no_check_for_status=True, + verbose_name="configuration status", + ), + ), + ( + "error_reason", + models.CharField( + blank=True, + help_text="Error reason reported by the device", + max_length=1024, + verbose_name="error reason", + ), + ), + ( + "context", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"ensure_ascii": False, "indent": 4}, + help_text=( + 'Additional ' + "context (configuration variables) in JSON format" + ), + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ], + options={ + "verbose_name": "configuration", + "verbose_name_plural": "configurations", + "abstract": False, + }, + ), + migrations.CreateModel( + name="TaggedTemplate", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "object_id", + models.UUIDField(db_index=True, verbose_name="object ID"), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s_tagged_items", + to="contenttypes.ContentType", + verbose_name="content type", + ), + ), + ], + options={ + "verbose_name": "Tagged item", + "verbose_name_plural": "Tags", + "abstract": False, + }, + ), + migrations.CreateModel( + name="TemplateTag", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "name", + models.CharField(max_length=100, unique=True, verbose_name="name"), + ), + ( + "slug", + models.SlugField( + allow_unicode=True, + max_length=100, + unique=True, + verbose_name="slug", + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ], + options={ + "verbose_name": "Tag", + "verbose_name_plural": "Tags", + "abstract": False, + }, + ), + migrations.CreateModel( + name="Vpn", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("name", models.CharField(db_index=True, max_length=64, unique=True)), + ( + "config", + jsonfield.fields.JSONField( + default=dict, + dump_kwargs={"ensure_ascii": False, "indent": 4}, + help_text="configuration in NetJSON DeviceConfiguration format", + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="configuration", + ), + ), + ( + "host", + models.CharField( + help_text="VPN server hostname or ip address", max_length=64 + ), + ), + ( + "key", + openwisp_utils.base.KeyField( + db_index=True, + default=openwisp_utils.utils.get_random_key, + help_text=None, + max_length=64, + validators=[ + django.core.validators.RegexValidator( + re.compile("^[^\\s/\\.]+$"), + code="invalid", + message=( + "This value must not contain spaces, " + "dots or slashes." + ), + ) + ], + ), + ), + ( + "backend", + models.CharField( + choices=[ + ("openwisp_controller.vpn_backends.OpenVpn", "OpenVPN"), + ("openwisp_controller.vpn_backends.Wireguard", "WireGuard"), + ( + "openwisp_controller.vpn_backends.VxlanWireguard", + "VXLAN over WireGuard", + ), + ("openwisp_controller.vpn_backends.ZeroTier", "ZeroTier"), + ], + help_text="Select VPN configuration backend", + max_length=128, + verbose_name="VPN backend", + ), + ), + ("notes", models.TextField(blank=True)), + ("dh", models.TextField(blank=True)), + ("public_key", models.CharField(blank=True, max_length=44)), + ("private_key", models.CharField(blank=True, max_length=44)), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "ca", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="pki.ca", + verbose_name="Certification Authority", + ), + ), + ( + "cert", + models.ForeignKey( + blank=True, + help_text="leave blank to create automatically", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="pki.cert", + verbose_name="x509 Certificate", + ), + ), + ( + "ip", + models.ForeignKey( + blank=True, + help_text=( + "Internal IP address of the VPN " + "server interface, if applicable" + ), + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.OPENWISP_IPAM_IPADDRESS_MODEL, + verbose_name="Internal IP", + ), + ), + ( + "organization", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name("openwisp_users", "Organization"), + verbose_name="organization", + ), + ), + ( + "subnet", + models.ForeignKey( + blank=True, + help_text=( + "Subnet IP addresses used by VPN clients, if applicable" + ), + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.OPENWISP_IPAM_SUBNET_MODEL, + verbose_name="Subnet", + ), + ), + ( + "auth_token", + models.CharField( + blank=True, + help_text=( + "Authentication token used for triggering " + '"Webhook Endpoint" or for calling "ZerotierService" API' + ), + max_length=128, + null=True, + verbose_name="Webhook AuthToken", + ), + ), + ( + "webhook_endpoint", + models.URLField( + blank=True, + help_text=( + "Webhook to trigger for updating server configuration " + "(e.g. https://openwisp2.mydomain.com:8081/trigger-update)" + ), + null=True, + verbose_name="Webhook Endpoint", + ), + ), + ("node_id", models.CharField(blank=True, max_length=10)), + ("network_id", models.CharField(blank=True, max_length=16)), + ], + options={ + "verbose_name": "VPN server", + "verbose_name_plural": "VPN servers", + "abstract": False, + }, + bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), + ), + migrations.CreateModel( + name="VpnClient", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("auto_cert", models.BooleanField(default=False)), + ( + "public_key", + models.CharField(blank=True, max_length=44), + ), + ( + "private_key", + models.CharField(blank=True, max_length=44), + ), + ( + "vni", + models.PositiveIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(16777216), + ], + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "cert", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="pki.cert", + ), + ), + ( + "config", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sample_config.config", + ), + ), + ( + "ip", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="openwisp_ipam.ipaddress", + ), + ), + ( + "vpn", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sample_config.vpn", + ), + ), + ("secret", models.TextField(blank=True)), + ], + options={ + "verbose_name": "VPN client", + "verbose_name_plural": "VPN clients", + "abstract": False, + "unique_together": {("config", "vpn")}, + }, + ), + migrations.CreateModel( + name="Template", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("name", models.CharField(db_index=True, max_length=64, unique=True)), + ( + "backend", + models.CharField( + choices=app_settings.BACKENDS, + help_text=( + 'Select netjsonconfig backend' + ), + max_length=128, + verbose_name="backend", + ), + ), + ( + "config", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"ensure_ascii": False, "indent": 4}, + help_text="configuration in NetJSON DeviceConfiguration format", + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="configuration", + ), + ), + ( + "type", + models.CharField( + choices=[("generic", "Generic"), ("vpn", "VPN-client")], + db_index=True, + default="generic", + help_text=( + "template type, determines which features are available" + ), + max_length=16, + verbose_name="type", + ), + ), + ( + "default", + models.BooleanField( + db_index=True, + default=False, + help_text=( + "whether new configurations will have this " + "template enabled by default" + ), + verbose_name="enabled by default", + ), + ), + ( + "required", + models.BooleanField( + db_index=True, + default=False, + help_text=( + "if checked, will force the assignment of this template to " + "all the devices of the organization (if no organization " + "is selected, it will be required for every device " + "in the system)" + ), + verbose_name="required", + ), + ), + ( + "auto_cert", + models.BooleanField( + db_index=True, + default=default_auto_cert, + help_text=( + "whether tunnel specific configuration (cryptographic " + "keys, ip addresses, etc) should be automatically " + "generated and managed behind the scenes for each " + "configuration using this template, valid only for " + "the VPN type" + ), + verbose_name="automatic tunnel provisioning", + ), + ), + ( + "default_values", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"ensure_ascii": False, "indent": 4}, + help_text=( + "A dictionary containing the default values for the " + "variables used by this template; these default variables " + "will be used during schema validation." + ), + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="Default Values", + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "organization", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name("openwisp_users", "Organization"), + verbose_name="organization", + ), + ), + ( + "tags", + taggit.managers.TaggableManager( + blank=True, + help_text=( + "A comma-separated list of template tags, may be used " + "to ease auto configuration with specific settings " + "(eg: 4G, mesh, WDS, VPN, ecc.)" + ), + through="sample_config.TaggedTemplate", + to="sample_config.TemplateTag", + verbose_name="Tags", + ), + ), + ( + "vpn", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="sample_config.vpn", + verbose_name="VPN", + ), + ), + ], + options={ + "verbose_name": "template", + "verbose_name_plural": "templates", + "abstract": False, + "unique_together": {("organization", "name")}, + }, + bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), + ), + migrations.AddField( + model_name="taggedtemplate", + name="tag", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s_items", + to="sample_config.TemplateTag", + ), + ), + migrations.AddField( + model_name="vpnclient", + name="template", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sample_config.template", + ), + ), + migrations.CreateModel( + name="OrganizationConfigSettings", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "registration_enabled", + models.BooleanField( + default=True, + help_text=( + "Whether automatic registration of " + "devices is enabled or not" + ), + verbose_name="auto-registration enabled", + ), + ), + ( + "shared_secret", + openwisp_utils.base.KeyField( + db_index=True, + default=openwisp_utils.utils.get_random_key, + help_text="used for automatic registration of devices", + max_length=32, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile("^[^\\s/\\.]+$"), + code="invalid", + message=( + "This value must not contain spaces, " + "dots or slashes." + ), + ) + ], + verbose_name="shared secret", + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "organization", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="config_settings", + to=swapper.get_model_name("openwisp_users", "Organization"), + verbose_name="organization", + ), + ), + ( + "context", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"indent": 4}, + help_text=( + 'This field can be used to add "Configuration Variables"' + " to the devices." + ), + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="Configuration Variables", + ), + ), + ], + options={ + "verbose_name": "Configuration management settings", + "verbose_name_plural": "Configuration management settings", + "abstract": False, + }, + ), + migrations.CreateModel( + name="DeviceGroup", + fields=[ + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("name", models.CharField(max_length=60)), + ( + "description", + models.TextField(blank=True, help_text="internal notes"), + ), + ( + "meta_data", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"ensure_ascii": False, "indent": 4}, + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + help_text=( + "Group meta data, use this field to store data which is" + " related to this group and can be retrieved via the" + " REST API." + ), + verbose_name="Metadata", + ), + ), + ( + "context", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"ensure_ascii": False, "indent": 4}, + help_text=( + "This field can be used to add meta data for the group" + ' or to add "Configuration Variables" to the devices.' + ), + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="Configuration Variables", + ), + ), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name("openwisp_users", "Organization"), + verbose_name="organization", + ), + ), + ], + options={ + "verbose_name": "Device Group", + "verbose_name_plural": "Device Groups", + "abstract": False, + "swappable": "CONFIG_DEVICEGROUP_MODEL", + }, + bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), + ), + migrations.AlterUniqueTogether( + name="devicegroup", + unique_together={("organization", "name")}, + ), + migrations.CreateModel( + name="Device", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "name", + models.CharField( + db_index=True, + max_length=64, + validators=[ + django.core.validators.RegexValidator( + re.compile( + "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}" + "[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9]" + "[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$|^" + "([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" + ), + code="invalid", + message=( + "Must be either a valid hostname or mac address." + ), + ) + ], + help_text=("must be either a valid hostname or mac address"), + ), + ), + ( + "mac_address", + models.CharField( + db_index=True, + help_text="primary mac address", + max_length=17, + validators=[ + django.core.validators.RegexValidator( + re.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"), + code="invalid", + message="Must be a valid mac address.", + ) + ], + ), + ), + ( + "group", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=swapper.get_model_name("config", "DeviceGroup"), + verbose_name="group", + ), + ), + ( + "key", + openwisp_utils.base.KeyField( + blank=True, + db_index=True, + default=None, + help_text="unique device key", + max_length=64, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile("^[^\\s/\\.]+$"), + code="invalid", + message=( + "This value must not contain spaces, " + "dots or slashes." + ), + ) + ], + ), + ), + ( + "model", + models.CharField( + blank=True, + db_index=True, + help_text="device model and manufacturer", + max_length=64, + ), + ), + ( + "os", + models.CharField( + blank=True, + db_index=True, + help_text="operating system identifier", + max_length=128, + verbose_name="operating system", + ), + ), + ( + "system", + models.CharField( + blank=True, + db_index=True, + help_text="system on chip or CPU info", + max_length=128, + verbose_name="SOC / CPU", + ), + ), + ("notes", models.TextField(blank=True, help_text="internal notes")), + ( + "last_ip", + models.GenericIPAddressField( + blank=True, + db_index=True, + help_text=( + "indicates the IP address logged from the last " + "request coming from the device" + ), + null=True, + ), + ), + ( + "management_ip", + models.GenericIPAddressField( + blank=True, + db_index=True, + help_text=( + "IP address used by the system to reach the device when " + "performing any type of push operation or active check. " + "The value of this field is generally sent by the device " + "and hence does not need to be changed, but can be " + "changed or cleared manually if needed." + ), + null=True, + ), + ), + ( + "hardware_id", + models.CharField( + blank=True, + help_text="Serial number of this device", + max_length=32, + null=True, + verbose_name="Serial number", + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name("openwisp_users", "Organization"), + verbose_name="organization", + ), + ), + ], + options={ + "abstract": False, + "unique_together": { + ("mac_address", "organization"), + ("hardware_id", "organization"), + }, + "verbose_name": app_settings.DEVICE_VERBOSE_NAME[0], + "verbose_name_plural": app_settings.DEVICE_VERBOSE_NAME[1], + }, + bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), + ), + migrations.CreateModel( + name="OrganizationLimits", + fields=[ + ( + "device_limit", + models.BigIntegerField( + default=0, + blank=True, + null=True, + help_text=( + "Maximum number of devices allowed for this organization." + ' "0" means unlimited.' + ), + verbose_name="device limit", + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "organization", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + related_name="config_limits", + to="openwisp_users.organization", + verbose_name="organization", + ), + ), + ], + options={ + "verbose_name": "controller limits", + "verbose_name_plural": "controller limits", + "abstract": False, + }, + ), + migrations.AddField( + model_name="config", + name="device", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="sample_config.device" + ), + ), + migrations.AddField( + model_name="config", + name="templates", + field=openwisp_controller.config.sortedm2m.fields.SortedManyToManyField( + blank=True, + help_text="configuration templates, applied from first to last", + related_name="config_relations", + to="sample_config.Template", + verbose_name="templates", + ), + ), + migrations.AddField( + model_name="config", + name="vpn", + field=models.ManyToManyField( + blank=True, + related_name="vpn_relations", + through="sample_config.VpnClient", + to="sample_config.Vpn", + ), + ), + ] diff --git a/tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py b/tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py new file mode 100644 index 000000000..b29ddb496 --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py @@ -0,0 +1,25 @@ +from django.db import migrations + +from openwisp_controller.config.migrations import ( + assign_devicegroup_permissions_to_groups, + assign_organization_config_settings_permissions_to_groups, + assign_permissions_to_groups, +) + + +class Migration(migrations.Migration): + dependencies = [("sample_config", "0001_initial")] + + operations = [ + migrations.RunPython( + assign_permissions_to_groups, reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + assign_devicegroup_permissions_to_groups, + reverse_code=migrations.RunPython.noop, + ), + migrations.RunPython( + code=assign_organization_config_settings_permissions_to_groups, + reverse_code=migrations.operations.special.RunPython.noop, + ), + ] diff --git a/tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py b/tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py new file mode 100644 index 000000000..02ddb02df --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.6 on 2021-02-11 22:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("sample_config", "0002_default_groups_permissions"), + ] + + operations = [ + migrations.AlterField( + model_name="template", + name="name", + field=models.CharField(db_index=True, max_length=64), + ), + migrations.AlterField( + model_name="vpn", + name="name", + field=models.CharField(db_index=True, max_length=64), + ), + migrations.AlterUniqueTogether( + name="vpn", unique_together={("organization", "name")} + ), + ] diff --git a/tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py b/tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py new file mode 100644 index 000000000..845e5bb57 --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py @@ -0,0 +1,31 @@ +# Generated by Django 4.0.4 on 2022-05-06 13:51 + +from django.db import migrations + +import openwisp_controller.config.sortedm2m.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("sample_config", "0003_name_unique_per_organization"), + ] + + operations = [ + migrations.AddField( + model_name="devicegroup", + name="templates", + field=openwisp_controller.config.sortedm2m.fields.SortedManyToManyField( + blank=True, + help_text=( + "These templates are automatically assigned to the devices " + "that are part of the group. Default and required templates " + "are excluded from this list. If the group of the device is " + "changed, these templates will be automatically removed and " + "the templates of the new group will be assigned." + ), + related_name="device_group_relations", + to="sample_config.template", + verbose_name="templates", + ), + ), + ] diff --git a/tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py b/tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py new file mode 100644 index 000000000..e94d35da8 --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2022-09-16 11:45 + + +from django.db import migrations + +from openwisp_controller.config.migrations import populate_organization_allowed_device + + +class Migration(migrations.Migration): + dependencies = [ + ("sample_config", "0004_devicegroup_templates"), + ] + + operations = [ + migrations.RunPython( + populate_organization_allowed_device, migrations.RunPython.noop + ) + ] diff --git a/tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py b/tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py new file mode 100644 index 000000000..37adfa228 --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-03-01 16:37 + +import model_utils.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("sample_config", "0005_add_organizationalloweddevice"), + ] + + operations = [ + migrations.AddField( + model_name="device", + name="_is_deactivated", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="config", + name="status", + field=model_utils.fields.StatusField( + choices=[ + ("modified", "modified"), + ("applied", "applied"), + ("error", "error"), + ("deactivating", "deactivating"), + ("deactivated", "deactivated"), + ], + default="modified", + help_text=( + '"modified" means the configuration is not applied yet;' + ' \n"applied" means the configuration is applied successfully;' + ' \n"error" means the configuration caused issues and it was' + " rolled back;" + ), + max_length=100, + no_check_for_status=True, + verbose_name="configuration status", + ), + ), + ] diff --git a/tests/openwisp2/sample_config/migrations/0007_alter_config_status.py b/tests/openwisp2/sample_config/migrations/0007_alter_config_status.py new file mode 100644 index 000000000..e66e888bd --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0007_alter_config_status.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.11 on 2024-03-07 17:10 + +import model_utils.fields +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("sample_config", "0006_device__is_deactivated_alter_config_status"), + ] + + operations = [ + migrations.AlterField( + model_name="config", + name="status", + field=model_utils.fields.StatusField( + choices=[ + ("modified", "modified"), + ("applied", "applied"), + ("error", "error"), + ("deactivating", "deactivating"), + ("deactivated", "deactivated"), + ], + default="modified", + help_text=( + '"modified" means the configuration is not applied yet; \n' + '"applied" means the configuration is applied successfully; \n' + '"error" means the configuration caused issues and it was' + ' rolled back; \n"deactivating" means the device has been' + " deactivated and the configuration is being removed; \n" + '"deactivated" means the configuration has been removed ' + "from the device;" + ), + max_length=100, + no_check_for_status=True, + verbose_name="configuration status", + ), + ), + ] diff --git a/tests/openwisp2/sample_config/migrations/__init__.py b/tests/openwisp2/sample_config/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openwisp2/sample_config/models.py b/tests/openwisp2/sample_config/models.py new file mode 100644 index 000000000..8fec929db --- /dev/null +++ b/tests/openwisp2/sample_config/models.py @@ -0,0 +1,113 @@ +from django.db import models + +from openwisp_controller.config.base.config import AbstractConfig +from openwisp_controller.config.base.device import AbstractDevice +from openwisp_controller.config.base.device_group import AbstractDeviceGroup +from openwisp_controller.config.base.multitenancy import ( + AbstractOrganizationConfigSettings, + AbstractOrganizationLimits, +) +from openwisp_controller.config.base.tag import ( + AbstractTaggedTemplate, + AbstractTemplateTag, +) +from openwisp_controller.config.base.template import AbstractTemplate +from openwisp_controller.config.base.vpn import AbstractVpn, AbstractVpnClient + + +class DetailsModel(models.Model): + details = models.CharField(max_length=64, blank=True, null=True) + + class Meta: + abstract = True + + +class Device(DetailsModel, AbstractDevice): + """ + Concrete Device model + """ + + class Meta(AbstractDevice.Meta): + abstract = False + + +class DeviceGroup(DetailsModel, AbstractDeviceGroup): + """ + Concrete Device model + """ + + class Meta(AbstractDeviceGroup.Meta): + abstract = False + + +class Config(DetailsModel, AbstractConfig): + """ + Concrete Config model + """ + + class Meta(AbstractConfig.Meta): + abstract = False + + +class TemplateTag(DetailsModel, AbstractTemplateTag): + """ + openwisp-controller TemplateTag model + """ + + class Meta(AbstractTemplateTag.Meta): + abstract = False + + +class TaggedTemplate(DetailsModel, AbstractTaggedTemplate): + """ + openwisp-controller TaggedTemplate model + """ + + class Meta(AbstractTaggedTemplate.Meta): + abstract = False + + +class Template(DetailsModel, AbstractTemplate): + """ + openwisp-controller Template model + """ + + class Meta(AbstractTemplate.Meta): + abstract = False + + +class Vpn(DetailsModel, AbstractVpn): + """ + openwisp-controller VPN model + """ + + class Meta(AbstractVpn.Meta): + abstract = False + + +class VpnClient(DetailsModel, AbstractVpnClient): + """ + m2m through model + """ + + class Meta(AbstractVpnClient.Meta): + abstract = False + + +class OrganizationConfigSettings(DetailsModel, AbstractOrganizationConfigSettings): + """ + Configuration management settings + specific to each organization + """ + + class Meta(AbstractOrganizationConfigSettings.Meta): + abstract = False + + +class OrganizationLimits(DetailsModel, AbstractOrganizationLimits): + """ + Number of allowed devices specific to each organization + """ + + class Meta(AbstractOrganizationLimits.Meta): + abstract = False diff --git a/tests/openwisp2/sample_config/pytest.py b/tests/openwisp2/sample_config/pytest.py new file mode 100644 index 000000000..06a938300 --- /dev/null +++ b/tests/openwisp2/sample_config/pytest.py @@ -0,0 +1,10 @@ +from openwisp_controller.config.tests.pytest import ( + TestDeviceConsumer as BaseTestDeviceConsumer, +) + + +class TestDeviceConsumer(BaseTestDeviceConsumer): + pass + + +del BaseTestDeviceConsumer diff --git a/tests/openwisp2/sample_config/tests.py b/tests/openwisp2/sample_config/tests.py new file mode 100644 index 000000000..6b5caef9f --- /dev/null +++ b/tests/openwisp2/sample_config/tests.py @@ -0,0 +1,143 @@ +from openwisp_controller.config.tests.test_admin import TestAdmin as BaseTestAdmin +from openwisp_controller.config.tests.test_admin import ( + TestDeviceGroupAdmin as BaseTestDeviceGroupAdmin, +) +from openwisp_controller.config.tests.test_admin import ( + TestDeviceGroupAdminTransaction as BaseTestDeviceGroupAdminTransaction, +) +from openwisp_controller.config.tests.test_admin import ( + TestTransactionAdmin as BaseTestTransactionAdmin, +) +from openwisp_controller.config.tests.test_api import TestConfigApi as BaseTestConfigApi +from openwisp_controller.config.tests.test_apps import TestApps as BaseTestApps +from openwisp_controller.config.tests.test_config import TestConfig as BaseTestConfig +from openwisp_controller.config.tests.test_config import ( + TestTransactionConfig as BaseTestTransactionConfig, +) +from openwisp_controller.config.tests.test_controller import ( + TestController as BaseTestController, +) +from openwisp_controller.config.tests.test_device import TestDevice as BaseTestDevice +from openwisp_controller.config.tests.test_device_group import ( + TestDeviceGroup as BaseTestDeviceGroup, +) +from openwisp_controller.config.tests.test_notifications import ( + TestNotifications as BaseTestNotifications, +) +from openwisp_controller.config.tests.test_tag import TestTag as BaseTestTag +from openwisp_controller.config.tests.test_template import ( + TestTemplate as BaseTestTemplate, +) +from openwisp_controller.config.tests.test_template import ( + TestTemplateTransaction as BaseTestTemplateTransaction, +) +from openwisp_controller.config.tests.test_views import TestViews as BaseTestViews +from openwisp_controller.config.tests.test_vpn import TestVpn as BaseTestVpn +from openwisp_controller.config.tests.test_vpn import ( + TestVpnTransaction as BaseTestVpnTransaction, +) +from openwisp_controller.config.tests.test_vpn import TestVxlan as BaseTestVxlan +from openwisp_controller.config.tests.test_vpn import TestWireguard as BaseTestWireguard + + +class TestAdmin(BaseTestAdmin): + app_label = "sample_config" + + +class TestTransactionAdmin(BaseTestTransactionAdmin): + app_label = "sample_config" + _deactivated_device_expected_readonly_fields = 23 + + +class TestDeviceGroupAdmin(BaseTestDeviceGroupAdmin): + app_label = "sample_config" + + +class TestDeviceGroupAdminTransaction(BaseTestDeviceGroupAdminTransaction): + app_label = "sample_config" + + +class TestConfig(BaseTestConfig): + pass + + +class TestTransactionConfig(BaseTestTransactionConfig): + pass + + +class TestController(BaseTestController): + pass + + +class TestDevice(BaseTestDevice): + pass + + +class TestDeviceGroup(BaseTestDeviceGroup): + pass + + +class TestTag(BaseTestTag): + pass + + +class TestTemplate(BaseTestTemplate): + pass + + +class TestTemplateTransaction(BaseTestTemplateTransaction): + pass + + +class TestNotifications(BaseTestNotifications): + app_label = "sample_config" + + +class TestViews(BaseTestViews): + pass + + +class TestVpn(BaseTestVpn): + pass + + +class TestVpnTransaction(BaseTestVpnTransaction): + pass + + +class TestApps(BaseTestApps): + pass + + +class TestConfigApi(BaseTestConfigApi): + pass + + +class TestWireguard(BaseTestWireguard): + pass + + +class TestVxlan(BaseTestVxlan): + pass + + +del BaseTestAdmin +del BaseTestTransactionAdmin +del BaseTestDeviceGroupAdmin +del BaseTestDeviceGroupAdminTransaction +del BaseTestConfig +del BaseTestTransactionConfig +del BaseTestController +del BaseTestDevice +del BaseTestDeviceGroup +del BaseTestTag +del BaseTestTemplate +del BaseTestTemplateTransaction +del BaseTestNotifications +del BaseTestViews +del BaseTestVpn +del BaseTestVpnTransaction +del BaseTestApps +del BaseTestConfigApi +del BaseTestWireguard +del BaseTestVxlan diff --git a/tests/openwisp2/sample_config/views.py b/tests/openwisp2/sample_config/views.py new file mode 100644 index 000000000..c120149cf --- /dev/null +++ b/tests/openwisp2/sample_config/views.py @@ -0,0 +1,65 @@ +from swapper import load_model + +from openwisp_controller.config.controller.views import ( + DeviceChecksumView as BaseDeviceChecksumView, +) +from openwisp_controller.config.controller.views import ( + DeviceDownloadConfigView as BaseDeviceDownloadConfigView, +) +from openwisp_controller.config.controller.views import ( + DeviceRegisterView as BaseDeviceRegisterView, +) +from openwisp_controller.config.controller.views import ( + DeviceReportStatusView as BaseDeviceReportStatusView, +) +from openwisp_controller.config.controller.views import ( + DeviceUpdateInfoView as BaseDeviceUpdateInfoView, +) +from openwisp_controller.config.controller.views import ( + VpnChecksumView as BaseVpnChecksumView, +) +from openwisp_controller.config.controller.views import ( + VpnDownloadConfigView as BaseVpnDownloadConfigView, +) + +Device = load_model("config", "Device") +OrganizationConfigSettings = load_model("config", "OrganizationConfigSettings") +Vpn = load_model("config", "Vpn") + + +class DeviceChecksumView(BaseDeviceChecksumView): + model = Device + + +class DeviceDownloadConfigView(BaseDeviceDownloadConfigView): + model = Device + + +class DeviceUpdateInfoView(BaseDeviceUpdateInfoView): + model = Device + + +class DeviceReportStatusView(BaseDeviceReportStatusView): + model = Device + + +class DeviceRegisterView(BaseDeviceRegisterView): + model = Device + org_config_settings_model = OrganizationConfigSettings + + +class VpnChecksumView(BaseVpnChecksumView): + model = Vpn + + +class VpnDownloadConfigView(BaseVpnDownloadConfigView): + model = Vpn + + +device_checksum = DeviceChecksumView.as_view() +device_download_config = DeviceDownloadConfigView.as_view() +device_update_info = DeviceUpdateInfoView.as_view() +device_report_status = DeviceReportStatusView.as_view() +device_register = DeviceRegisterView.as_view() +vpn_checksum = VpnChecksumView.as_view() +vpn_download_config = VpnDownloadConfigView.as_view() diff --git a/tests/openwisp2/sample_connection/__init__.py b/tests/openwisp2/sample_connection/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openwisp2/sample_connection/admin.py b/tests/openwisp2/sample_connection/admin.py new file mode 100644 index 000000000..5e9199435 --- /dev/null +++ b/tests/openwisp2/sample_connection/admin.py @@ -0,0 +1 @@ +from openwisp_controller.connection import admin # noqa diff --git a/tests/openwisp2/sample_connection/api/__init__.py b/tests/openwisp2/sample_connection/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openwisp2/sample_connection/api/views.py b/tests/openwisp2/sample_connection/api/views.py new file mode 100644 index 000000000..411cecd58 --- /dev/null +++ b/tests/openwisp2/sample_connection/api/views.py @@ -0,0 +1,50 @@ +from openwisp_controller.connection.api.views import ( + CommandDetailsView as BaseCommandDetailsView, +) +from openwisp_controller.connection.api.views import ( + CommandListCreateView as BaseCommandListCreateView, +) +from openwisp_controller.connection.api.views import ( + CredentialDetailView as BaseCredentialDetailView, +) +from openwisp_controller.connection.api.views import ( + CredentialListCreateView as BaseCredentialListCreateView, +) +from openwisp_controller.connection.api.views import ( + DeviceConnectionDetailView as BaseDeviceConnectionDetailView, +) +from openwisp_controller.connection.api.views import ( + DeviceConnenctionListCreateView as BaseDeviceConnenctionListCreateView, +) + + +class CommandDetailsView(BaseCommandDetailsView): + pass + + +class CommandListCreateView(BaseCommandListCreateView): + pass + + +class CredentialListCreateView(BaseCredentialListCreateView): + pass + + +class CredentialDetailView(BaseCredentialDetailView): + pass + + +class DeviceConnenctionListCreateView(BaseDeviceConnenctionListCreateView): + pass + + +class DeviceConnectionDetailView(BaseDeviceConnectionDetailView): + pass + + +command_list_create_view = CommandListCreateView.as_view() +command_details_view = CommandDetailsView.as_view() +credential_list_create_view = CredentialListCreateView.as_view() +credential_detail_view = CredentialDetailView.as_view() +deviceconnection_list_create_view = DeviceConnenctionListCreateView.as_view() +deviceconnection_details_view = DeviceConnectionDetailView.as_view() diff --git a/tests/openwisp2/sample_connection/apps.py b/tests/openwisp2/sample_connection/apps.py new file mode 100644 index 000000000..1cd087c9c --- /dev/null +++ b/tests/openwisp2/sample_connection/apps.py @@ -0,0 +1,9 @@ +from openwisp_controller.connection.apps import ConnectionConfig + + +class SampleConnectionConfig(ConnectionConfig): + name = "openwisp2.sample_connection" + label = "sample_connection" + + +del ConnectionConfig diff --git a/tests/openwisp2/sample_connection/migrations/0001_initial.py b/tests/openwisp2/sample_connection/migrations/0001_initial.py new file mode 100644 index 000000000..ab73a253d --- /dev/null +++ b/tests/openwisp2/sample_connection/migrations/0001_initial.py @@ -0,0 +1,292 @@ +# Generated by Django 3.0.6 on 2020-05-10 18:11 + +import collections +import uuid + +import django +import django.db.models.deletion +import django.utils.timezone +import jsonfield.fields +import model_utils.fields +import swapper +from django.conf import settings +from django.db import migrations, models + +import openwisp_controller.connection.base.models +import openwisp_users.mixins +from openwisp_controller.connection import settings as connection_settings +from openwisp_controller.connection.commands import COMMAND_CHOICES, get_command_choices + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("sample_config", "0001_initial"), + swapper.dependency( + *swapper.split(settings.AUTH_USER_MODEL), version="0004_default_groups" + ), + swapper.dependency("config", "Device"), + ] + + operations = [ + migrations.CreateModel( + name="Credentials", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("name", models.CharField(db_index=True, max_length=64, unique=True)), + ( + "connector", + models.CharField( + choices=connection_settings.CONNECTORS, + db_index=True, + max_length=128, + verbose_name="connection type", + ), + ), + ( + "params", + jsonfield.fields.JSONField( + default=dict, + dump_kwargs={"indent": 4}, + help_text="global connection parameters", + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="parameters", + ), + ), + ( + "auto_add", + models.BooleanField( + default=False, + help_text=( + "automatically add these credentials to the " + "devices of this organization; if no organization is " + "specified will be added to all the new devices" + ), + verbose_name="auto add", + ), + ), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "organization", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name("openwisp_users", "Organization"), + verbose_name="organization", + ), + ), + ], + options={ + "verbose_name": "Access credentials", + "verbose_name_plural": "Access credentials", + "abstract": False, + }, + bases=( + openwisp_controller.connection.base.models.ConnectorMixin, + openwisp_users.mixins.ValidateOrgMixin, + models.Model, + ), + ), + migrations.CreateModel( + name="DeviceConnection", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "update_strategy", + models.CharField( + blank=True, + choices=connection_settings.UPDATE_STRATEGIES, + db_index=True, + help_text="leave blank to determine automatically", + max_length=128, + verbose_name="update strategy", + ), + ), + ("enabled", models.BooleanField(db_index=True, default=True)), + ( + "params", + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={"indent": 4}, + help_text=( + "local connection parameters (will override the " + "global parameters if specified)" + ), + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + verbose_name="parameters", + ), + ), + ( + "is_working", + models.BooleanField(blank=True, default=None, null=True), + ), + ( + "failure_reason", + models.TextField(blank=True, verbose_name="reason of failure"), + ), + ("last_attempt", models.DateTimeField(blank=True, null=True)), + ("details", models.CharField(blank=True, max_length=64, null=True)), + ( + "credentials", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sample_connection.Credentials", + ), + ), + ( + "device", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sample_config.Device", + ), + ), + ], + options={ + "verbose_name": "Device connection", + "verbose_name_plural": "Device connections", + "unique_together": {("device", "credentials")}, + "abstract": False, + }, + bases=( + openwisp_controller.connection.base.models.ConnectorMixin, + models.Model, + ), + ), + migrations.CreateModel( + name="Command", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("in-progress", "in progress"), + ("success", "success"), + ("failed", "failed"), + ], + default="in-progress", + max_length=12, + ), + ), + ( + "type", + models.CharField( + choices=( + COMMAND_CHOICES + if django.VERSION < (5, 0) + else get_command_choices + ), + max_length=16, + ), + ), + ( + "input", + jsonfield.fields.JSONField( + blank=True, + dump_kwargs={"indent": 4}, + load_kwargs={"object_pairs_hook": collections.OrderedDict}, + null=True, + ), + ), + ("output", models.TextField(blank=True)), + ( + "connection", + models.ForeignKey( + null=True, + blank=True, + on_delete=django.db.models.deletion.SET_NULL, + to=swapper.get_model_name("connection", "DeviceConnection"), + ), + ), + ( + "device", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name("config", "Device"), + ), + ), + ], + options={ + "verbose_name": "Command", + "verbose_name_plural": "Commands", + "ordering": ("created",), + "abstract": False, + "swappable": swapper.swappable_setting("connection", "Command"), + }, + ), + ] diff --git a/tests/openwisp2/sample_connection/migrations/0002_default_group_permissions.py b/tests/openwisp2/sample_connection/migrations/0002_default_group_permissions.py new file mode 100644 index 000000000..469610b20 --- /dev/null +++ b/tests/openwisp2/sample_connection/migrations/0002_default_group_permissions.py @@ -0,0 +1,19 @@ +from django.db import migrations + +from openwisp_controller.connection.migrations import ( + assign_command_permissions_to_groups, + assign_permissions_to_groups, +) + + +class Migration(migrations.Migration): + dependencies = [("sample_connection", "0001_initial")] + + operations = [ + migrations.RunPython( + assign_permissions_to_groups, reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + assign_command_permissions_to_groups, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/tests/openwisp2/sample_connection/migrations/0003_name_unique_per_organization.py b/tests/openwisp2/sample_connection/migrations/0003_name_unique_per_organization.py new file mode 100644 index 000000000..ff3b8dbed --- /dev/null +++ b/tests/openwisp2/sample_connection/migrations/0003_name_unique_per_organization.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.6 on 2021-02-11 22:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("sample_connection", "0002_default_group_permissions"), + ] + + operations = [ + migrations.AlterField( + model_name="credentials", + name="name", + field=models.CharField(db_index=True, max_length=64), + ), + migrations.AlterUniqueTogether( + name="credentials", unique_together={("name", "organization")} + ), + ] diff --git a/tests/openwisp2/sample_connection/migrations/__init__.py b/tests/openwisp2/sample_connection/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openwisp2/sample_connection/models.py b/tests/openwisp2/sample_connection/models.py new file mode 100644 index 000000000..7964c5471 --- /dev/null +++ b/tests/openwisp2/sample_connection/models.py @@ -0,0 +1,29 @@ +from django.db import models + +from openwisp_controller.connection.base.models import ( + AbstractCommand, + AbstractCredentials, + AbstractDeviceConnection, +) + + +class DetailsModel(models.Model): + details = models.CharField(max_length=64, blank=True, null=True) + + class Meta: + abstract = True + + +class Credentials(DetailsModel, AbstractCredentials): + class Meta(AbstractCredentials.Meta): + abstract = False + + +class DeviceConnection(DetailsModel, AbstractDeviceConnection): + class Meta(AbstractDeviceConnection.Meta): + abstract = False + + +class Command(AbstractCommand): + class Meta(AbstractCommand.Meta): + abstract = False diff --git a/tests/openwisp2/sample_connection/pytest.py b/tests/openwisp2/sample_connection/pytest.py new file mode 100644 index 000000000..910268306 --- /dev/null +++ b/tests/openwisp2/sample_connection/pytest.py @@ -0,0 +1,10 @@ +from openwisp_controller.connection.tests.pytest import ( + TestCommandsConsumer as BaseTestCommandsConsumer, +) + + +class TestCommandsConsumer(BaseTestCommandsConsumer): + pass + + +del BaseTestCommandsConsumer diff --git a/tests/openwisp2/sample_connection/tests.py b/tests/openwisp2/sample_connection/tests.py new file mode 100644 index 000000000..cc225d2ab --- /dev/null +++ b/tests/openwisp2/sample_connection/tests.py @@ -0,0 +1,71 @@ +from openwisp_controller.connection.tests.test_admin import ( + TestCommandInlines as BaseTestCommandInlines, +) +from openwisp_controller.connection.tests.test_admin import ( + TestConnectionAdmin as BaseTestConnectionAdmin, +) +from openwisp_controller.connection.tests.test_api import ( + TestConnectionApi as BaseTestConnectionApi, +) +from openwisp_controller.connection.tests.test_models import ( + TestModels as BaseTestModels, +) +from openwisp_controller.connection.tests.test_models import ( + TestModelsTransaction as BaseTestModelsTransaction, +) +from openwisp_controller.connection.tests.test_notifications import ( + TestNotifications as BaseTestNotifications, +) +from openwisp_controller.connection.tests.test_notifications import ( + TestNotificationTransaction as BaseTestNotificationTransaction, +) +from openwisp_controller.connection.tests.test_ssh import TestSsh as BaseTestSsh +from openwisp_controller.connection.tests.test_tasks import TestTasks as BaseTestTasks + + +class TestConnectionAdmin(BaseTestConnectionAdmin): + config_app_label = "sample_config" + app_label = "sample_connection" + + +class TestCommandInlines(BaseTestCommandInlines): + config_app_label = "sample_config" + + +class TestModels(BaseTestModels): + app_label = "sample_connection" + + +class TestModelsTransaction(BaseTestModelsTransaction): + app_label = "sample_connection" + + +class TestTasks(BaseTestTasks): + pass + + +class TestSsh(BaseTestSsh): + pass + + +class TestNotifications(BaseTestNotifications): + app_label = "sample_connection" + + +class TestNotificationTransaction(BaseTestNotificationTransaction): + app_label = "sample_connection" + + +class TestConnectionApi(BaseTestConnectionApi): + pass + + +del BaseTestCommandInlines +del BaseTestConnectionAdmin +del BaseTestModels +del BaseTestModelsTransaction +del BaseTestSsh +del BaseTestTasks +del BaseTestNotifications +del BaseTestNotificationTransaction +del BaseTestConnectionApi diff --git a/tests/openwisp2/sample_firmware_upgrader/tests.py b/tests/openwisp2/sample_firmware_upgrader/tests.py index 5abbdbefb..713a572ae 100644 --- a/tests/openwisp2/sample_firmware_upgrader/tests.py +++ b/tests/openwisp2/sample_firmware_upgrader/tests.py @@ -30,6 +30,9 @@ from openwisp_firmware_upgrader.tests.test_private_storage import ( TestPrivateStorage as BaseTestPrivateStorage, ) +from openwisp_firmware_upgrader.tests.test_selenium import ( + TestDeviceAdmin as BaseTestDeviceAdmin, +) from openwisp_firmware_upgrader.tests.test_tasks import TestTasks as BaseTestTasks BatchUpgradeOperation = load_model("BatchUpgradeOperation") @@ -42,6 +45,7 @@ class TestAdmin(BaseTestAdmin): app_label = "sample_firmware_upgrader" + config_app_label = "sample_config" build_list_url = reverse(f"admin:{app_label}_build_changelist") def test_category_details(self): @@ -73,7 +77,9 @@ def test_firmware_image_details(self): def test_device_firmware_details(self): self._login() device_fw = self._create_device_firmware(details="sample device_fw details") - path = reverse("admin:config_device_change", args=[device_fw.device_id]) + path = reverse( + f"admin:{self.config_app_label}_device_change", args=[device_fw.device_id] + ) r = self.client.get(path) self.assertContains( r, @@ -101,13 +107,17 @@ def test_upgrede_operation_details(self): uo = UpgradeOperation.objects.first() uo.details = "Test Upgrade device details" uo.save() - url = reverse("admin:config_device_change", args=[device_fw.device.pk]) + url = reverse( + f"admin:{self.config_app_label}_device_change", args=[device_fw.device.pk] + ) r = self.client.get(url) self.assertContains(r, '
      Test Upgrade device details') class TestAdminTransaction(BaseTestAdminTransaction): app_label = "sample_firmware_upgrader" + config_app_label = "sample_config" + _mock_connect = "openwisp2.sample_connection.models.DeviceConnection.connect" build_list_url = reverse(f"admin:{app_label}_build_changelist") @@ -116,6 +126,7 @@ class TestModels(BaseTestModels): class TestModelsTransaction(BaseTestModelsTransaction): + _mock_connect = "openwisp2.sample_connection.models.DeviceConnection.connect" pass @@ -127,7 +138,15 @@ class TestPrivateStorage(BaseTestPrivateStorage): pass +class TestDeviceAdmin(BaseTestDeviceAdmin): + config_app_label = "sample_config" + firmware_app_label = "sample_firmware_upgrader" + _mock_connect = "openwisp2.sample_connection.models.DeviceConnection.connect" + pass + + class TestTasks(BaseTestTasks): + _mock_connect = "openwisp2.sample_connection.models.DeviceConnection.connect" pass @@ -158,6 +177,7 @@ class TestOrgAPIMixin(BaseTestOrgAPIMixin): del BaseTestModelsTransaction del BaseTestOpenwrtUpgrader del BaseTestPrivateStorage +del BaseTestDeviceAdmin del BaseTestTasks del BaseTestBuildViews del BaseTestCategoryViews diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index 0fd7b294b..d8b0e64aa 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -115,7 +115,6 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(os.path.dirname(BASE_DIR), "templates")], "OPTIONS": { "loaders": [ "django.template.loaders.filesystem.Loader", @@ -217,6 +216,35 @@ "sample_firmware_upgrader.UpgradeOperation" ) + # For controller extended apps: + # Replace Config + config_index = INSTALLED_APPS.index("openwisp_controller.config") + INSTALLED_APPS.remove("openwisp_controller.config") + INSTALLED_APPS.insert(config_index, "openwisp2.sample_config") + # Replace Connection + connection_index = INSTALLED_APPS.index("openwisp_controller.connection") + INSTALLED_APPS.remove("openwisp_controller.connection") + INSTALLED_APPS.insert(connection_index, "openwisp2.sample_connection") + # Extended apps + EXTENDED_APPS.append("openwisp_controller.config") + EXTENDED_APPS.append("openwisp_controller.connection") + # Swapper + CONFIG_DEVICE_MODEL = "sample_config.Device" + CONFIG_DEVICEGROUP_MODEL = "sample_config.DeviceGroup" + CONFIG_CONFIG_MODEL = "sample_config.Config" + CONFIG_TEMPLATETAG_MODEL = "sample_config.TemplateTag" + CONFIG_TAGGEDTEMPLATE_MODEL = "sample_config.TaggedTemplate" + CONFIG_TEMPLATE_MODEL = "sample_config.Template" + CONFIG_VPN_MODEL = "sample_config.Vpn" + CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient" + CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings" + CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits" + + CONNECTION_CREDENTIALS_MODEL = "sample_connection.Credentials" + CONNECTION_DEVICECONNECTION_MODEL = "sample_connection.DeviceConnection" + CONNECTION_COMMAND_MODEL = "sample_connection.Command" + + TEST_RUNNER = "openwisp_utils.tests.TimeLoggingTestRunner" # local settings must be imported before test runner otherwise they'll be ignored diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index 11c91c14b..d34fd525b 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -1,3 +1,5 @@ +import os + from django.conf import settings from django.conf.urls.static import static from django.contrib import admin @@ -5,14 +7,58 @@ from django.urls import include, path, reverse_lazy from django.views.generic import RedirectView +from openwisp_controller.config.api.urls import get_api_urls as get_config_api_urls +from openwisp_controller.config.utils import get_controller_urls +from openwisp_controller.connection.api.urls import ( + get_api_urls as get_connection_api_urls, +) from openwisp_users.api.urls import get_api_urls +from .sample_config import views as config_views +from .sample_config.api import views as config_api_views +from .sample_connection.api import views as connection_api_views + redirect_view = RedirectView.as_view(url=reverse_lazy("admin:index")) -urlpatterns = [ +urlpatterns = [] + +if os.environ.get("SAMPLE_APP", False): + urlpatterns += [ + path( + "controller/", + include( + (get_controller_urls(config_views), "controller"), + namespace="controller", + ), + ), + path( + "", + include(("openwisp_controller.config.urls", "config"), namespace="config"), + ), + path( + "api/v1/", + include( + (get_config_api_urls(config_api_views), "config_api"), + namespace="config_api", + ), + ), + path( + "api/v1/", + include( + ( + get_connection_api_urls(connection_api_views), + "connection_api", + ), + namespace="connection_api", + ), + ), + ] + +urlpatterns += [ path("admin/", admin.site.urls), path("", redirect_view, name="index"), path("", include("openwisp_controller.urls")), + path("accounts/", include("openwisp_users.accounts.urls")), # NEEDED OR NOT ? path("", include("openwisp_firmware_upgrader.urls")), # token auth API path("api/v1/", include((get_api_urls(), "users"), namespace="users")), From 706966eb2411b223fabee215f8508bb28d6e4d2d Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Mon, 1 Dec 2025 15:27:48 +0100 Subject: [PATCH 2/8] [change] Adjust tests and sample apps --- .../migrations/0001_initial.py | 4 +- tests/openwisp2/sample_connection/tests.py | 62 ++++++++++++++++++- .../sample_firmware_upgrader/tests.py | 6 +- tests/openwisp2/settings.py | 28 ++++----- tests/openwisp2/urls.py | 32 +++++----- 5 files changed, 94 insertions(+), 38 deletions(-) diff --git a/tests/openwisp2/sample_connection/migrations/0001_initial.py b/tests/openwisp2/sample_connection/migrations/0001_initial.py index ab73a253d..d84865ad3 100644 --- a/tests/openwisp2/sample_connection/migrations/0001_initial.py +++ b/tests/openwisp2/sample_connection/migrations/0001_initial.py @@ -22,7 +22,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("sample_config", "0001_initial"), + ("config", "0001_squashed_0002_config_settings_uuid"), swapper.dependency( *swapper.split(settings.AUTH_USER_MODEL), version="0004_default_groups" ), @@ -188,7 +188,7 @@ class Migration(migrations.Migration): "device", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - to="sample_config.Device", + to="config.Device", ), ), ], diff --git a/tests/openwisp2/sample_connection/tests.py b/tests/openwisp2/sample_connection/tests.py index cc225d2ab..ed18b250c 100644 --- a/tests/openwisp2/sample_connection/tests.py +++ b/tests/openwisp2/sample_connection/tests.py @@ -24,12 +24,12 @@ class TestConnectionAdmin(BaseTestConnectionAdmin): - config_app_label = "sample_config" + config_app_label = "config" app_label = "sample_connection" class TestCommandInlines(BaseTestCommandInlines): - config_app_label = "sample_config" + config_app_label = "config" class TestModels(BaseTestModels): @@ -47,17 +47,73 @@ class TestTasks(BaseTestTasks): class TestSsh(BaseTestSsh): pass +import os +from django.urls import reverse +from swapper import load_model + +Notification = load_model("openwisp_notifications", "Notification") + class TestNotifications(BaseTestNotifications): app_label = "sample_connection" + def _generic_notification_test( + self, exp_level, exp_type, exp_verb, exp_message, exp_email_subject + ): + n = Notification.objects.first() + config_app = ( + "config" + ) + device_url_path = reverse(f"admin:{config_app}_device_change", args=[self.d.id]) + exp_target_link = f"https://example.com{device_url_path}" + + self.assertEqual(n.type, exp_type) + self.assertEqual(n.level, exp_level) + self.assertEqual(n.verb, exp_verb) + self.assertEqual(n.actor, self.d.deviceconnection_set.first()) + self.assertEqual(n.target, self.d) + self.assertIn(exp_message.format(n=n, target_link=exp_target_link), n.message) + self.assertEqual(n.email_subject, exp_email_subject.format(n=n)) class TestNotificationTransaction(BaseTestNotificationTransaction): app_label = "sample_connection" + def _generic_notification_test( + self, exp_level, exp_type, exp_verb, exp_message, exp_email_subject + ): + n = Notification.objects.first() + config_app = ( + "config" + ) + device_url_path = reverse(f"admin:{config_app}_device_change", args=[self.d.id]) + exp_target_link = f"https://example.com{device_url_path}" + + self.assertEqual(n.type, exp_type) + self.assertEqual(n.level, exp_level) + self.assertEqual(n.verb, exp_verb) + self.assertEqual(n.actor, self.d.deviceconnection_set.first()) + self.assertEqual(n.target, self.d) + self.assertIn(exp_message.format(n=n, target_link=exp_target_link), n.message) + self.assertEqual(n.email_subject, exp_email_subject.format(n=n)) + + +from openwisp_controller.connection import settings as conn_settings + class TestConnectionApi(BaseTestConnectionApi): - pass + def test_post_deviceconnection_list(self): + d1 = self._create_device() + self._create_config(device=d1) + path = reverse("connection_api:deviceconnection_list", args=(d1.pk,)) + data = { + "credentials": self._get_credentials().pk, + "update_strategy": conn_settings.UPDATE_STRATEGIES[0][0], + "enabled": True, + "failure_reason": "", + } + with self.assertNumQueries(13): + response = self.client.post(path, data, content_type="application/json") + self.assertEqual(response.status_code, 201) del BaseTestCommandInlines diff --git a/tests/openwisp2/sample_firmware_upgrader/tests.py b/tests/openwisp2/sample_firmware_upgrader/tests.py index 713a572ae..07e90d14a 100644 --- a/tests/openwisp2/sample_firmware_upgrader/tests.py +++ b/tests/openwisp2/sample_firmware_upgrader/tests.py @@ -45,7 +45,7 @@ class TestAdmin(BaseTestAdmin): app_label = "sample_firmware_upgrader" - config_app_label = "sample_config" + config_app_label = "config" build_list_url = reverse(f"admin:{app_label}_build_changelist") def test_category_details(self): @@ -116,7 +116,7 @@ def test_upgrede_operation_details(self): class TestAdminTransaction(BaseTestAdminTransaction): app_label = "sample_firmware_upgrader" - config_app_label = "sample_config" + config_app_label = "config" _mock_connect = "openwisp2.sample_connection.models.DeviceConnection.connect" build_list_url = reverse(f"admin:{app_label}_build_changelist") @@ -139,7 +139,7 @@ class TestPrivateStorage(BaseTestPrivateStorage): class TestDeviceAdmin(BaseTestDeviceAdmin): - config_app_label = "sample_config" + config_app_label = "config" firmware_app_label = "sample_firmware_upgrader" _mock_connect = "openwisp2.sample_connection.models.DeviceConnection.connect" pass diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index d8b0e64aa..3e53108e7 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -218,27 +218,27 @@ # For controller extended apps: # Replace Config - config_index = INSTALLED_APPS.index("openwisp_controller.config") - INSTALLED_APPS.remove("openwisp_controller.config") - INSTALLED_APPS.insert(config_index, "openwisp2.sample_config") + # config_index = INSTALLED_APPS.index("openwisp_controller.config") + # INSTALLED_APPS.remove("openwisp_controller.config") + # INSTALLED_APPS.insert(config_index, "openwisp2.sample_config") # Replace Connection connection_index = INSTALLED_APPS.index("openwisp_controller.connection") INSTALLED_APPS.remove("openwisp_controller.connection") INSTALLED_APPS.insert(connection_index, "openwisp2.sample_connection") # Extended apps - EXTENDED_APPS.append("openwisp_controller.config") + # EXTENDED_APPS.append("openwisp_controller.config") EXTENDED_APPS.append("openwisp_controller.connection") # Swapper - CONFIG_DEVICE_MODEL = "sample_config.Device" - CONFIG_DEVICEGROUP_MODEL = "sample_config.DeviceGroup" - CONFIG_CONFIG_MODEL = "sample_config.Config" - CONFIG_TEMPLATETAG_MODEL = "sample_config.TemplateTag" - CONFIG_TAGGEDTEMPLATE_MODEL = "sample_config.TaggedTemplate" - CONFIG_TEMPLATE_MODEL = "sample_config.Template" - CONFIG_VPN_MODEL = "sample_config.Vpn" - CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient" - CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings" - CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits" + # CONFIG_DEVICE_MODEL = "sample_config.Device" + # CONFIG_DEVICEGROUP_MODEL = "sample_config.DeviceGroup" + # CONFIG_CONFIG_MODEL = "sample_config.Config" + # CONFIG_TEMPLATETAG_MODEL = "sample_config.TemplateTag" + # CONFIG_TAGGEDTEMPLATE_MODEL = "sample_config.TaggedTemplate" + # CONFIG_TEMPLATE_MODEL = "sample_config.Template" + # CONFIG_VPN_MODEL = "sample_config.Vpn" + # CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient" + # CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings" + # CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits" CONNECTION_CREDENTIALS_MODEL = "sample_connection.Credentials" CONNECTION_DEVICECONNECTION_MODEL = "sample_connection.DeviceConnection" diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index d34fd525b..5d81c9e04 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -14,8 +14,8 @@ ) from openwisp_users.api.urls import get_api_urls -from .sample_config import views as config_views -from .sample_config.api import views as config_api_views +# from .sample_config import views as config_views +# from .sample_config.api import views as config_api_views from .sample_connection.api import views as connection_api_views redirect_view = RedirectView.as_view(url=reverse_lazy("admin:index")) @@ -24,24 +24,24 @@ if os.environ.get("SAMPLE_APP", False): urlpatterns += [ - path( - "controller/", - include( - (get_controller_urls(config_views), "controller"), - namespace="controller", - ), - ), + # path( + # "controller/", + # include( + # (get_controller_urls(config_views), "controller"), + # namespace="controller", + # ), + # ), path( "", include(("openwisp_controller.config.urls", "config"), namespace="config"), ), - path( - "api/v1/", - include( - (get_config_api_urls(config_api_views), "config_api"), - namespace="config_api", - ), - ), + # path( + # "api/v1/", + # include( + # (get_config_api_urls(config_api_views), "config_api"), + # namespace="config_api", + # ), + # ), path( "api/v1/", include( From 72e9d2ccc660b265108e364e3868ad59a33c2cc2 Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Mon, 1 Dec 2025 15:39:15 +0100 Subject: [PATCH 3/8] [change] Remove sample_config for now --- tests/openwisp2/sample_config/__init__.py | 0 tests/openwisp2/sample_config/admin.py | 12 - tests/openwisp2/sample_config/api/views.py | 112 -- tests/openwisp2/sample_config/apps.py | 9 - .../fixtures/test_templates.json | 50 - .../sample_config/migrations/0001_initial.py | 1048 ----------------- .../0002_default_groups_permissions.py | 25 - .../0003_name_unique_per_organization.py | 25 - .../migrations/0004_devicegroup_templates.py | 31 - .../0005_add_organizationalloweddevice.py | 18 - ...ice__is_deactivated_alter_config_status.py | 41 - .../migrations/0007_alter_config_status.py | 39 - .../sample_config/migrations/__init__.py | 0 tests/openwisp2/sample_config/models.py | 113 -- tests/openwisp2/sample_config/pytest.py | 10 - tests/openwisp2/sample_config/tests.py | 143 --- tests/openwisp2/sample_config/views.py | 65 - 17 files changed, 1741 deletions(-) delete mode 100644 tests/openwisp2/sample_config/__init__.py delete mode 100644 tests/openwisp2/sample_config/admin.py delete mode 100644 tests/openwisp2/sample_config/api/views.py delete mode 100644 tests/openwisp2/sample_config/apps.py delete mode 100644 tests/openwisp2/sample_config/fixtures/test_templates.json delete mode 100644 tests/openwisp2/sample_config/migrations/0001_initial.py delete mode 100644 tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py delete mode 100644 tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py delete mode 100644 tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py delete mode 100644 tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py delete mode 100644 tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py delete mode 100644 tests/openwisp2/sample_config/migrations/0007_alter_config_status.py delete mode 100644 tests/openwisp2/sample_config/migrations/__init__.py delete mode 100644 tests/openwisp2/sample_config/models.py delete mode 100644 tests/openwisp2/sample_config/pytest.py delete mode 100644 tests/openwisp2/sample_config/tests.py delete mode 100644 tests/openwisp2/sample_config/views.py diff --git a/tests/openwisp2/sample_config/__init__.py b/tests/openwisp2/sample_config/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/openwisp2/sample_config/admin.py b/tests/openwisp2/sample_config/admin.py deleted file mode 100644 index 5cc36ad3f..000000000 --- a/tests/openwisp2/sample_config/admin.py +++ /dev/null @@ -1,12 +0,0 @@ -from openwisp_controller.config.admin import ( - DeviceAdmin, - DeviceGroupAdmin, - TemplateAdmin, - VpnAdmin, -) - -# Monkey Patching done only for testing purposes -DeviceAdmin.fields += ["details"] -TemplateAdmin.fields += ["details"] -VpnAdmin.fields += ["details"] -DeviceGroupAdmin.fields += ["details"] diff --git a/tests/openwisp2/sample_config/api/views.py b/tests/openwisp2/sample_config/api/views.py deleted file mode 100644 index daa8e2feb..000000000 --- a/tests/openwisp2/sample_config/api/views.py +++ /dev/null @@ -1,112 +0,0 @@ -from openwisp_controller.config.api.download_views import ( - DownloadDeviceView as BaseDownloadDeviceView, -) -from openwisp_controller.config.api.download_views import ( - DownloadTemplateconfiguration as BaseDownloadTemplateconfiguration, -) -from openwisp_controller.config.api.download_views import ( - DownloadVpnView as BaseDownloadVpnView, -) -from openwisp_controller.config.api.views import ( - DeviceActivateView as BaseDeviceActivateView, -) -from openwisp_controller.config.api.views import ( - DeviceDeactivateView as BaseDeviceDeactivateView, -) -from openwisp_controller.config.api.views import ( - DeviceDetailView as BaseDeviceDetailView, -) -from openwisp_controller.config.api.views import ( - DeviceGroupCommonName as BaseDeviceGroupCommonName, -) -from openwisp_controller.config.api.views import ( - DeviceGroupDetailView as BaseDeviceGroupDetailView, -) -from openwisp_controller.config.api.views import ( - DeviceGroupListCreateView as BaseDeviceGroupListCreateView, -) -from openwisp_controller.config.api.views import ( - DeviceListCreateView as BaseDeviceListCreateView, -) -from openwisp_controller.config.api.views import ( - TemplateDetailView as BaseTemplateDetailView, -) -from openwisp_controller.config.api.views import ( - TemplateListCreateView as BaseTemplateListCreateView, -) -from openwisp_controller.config.api.views import VpnDetailView as BaseVpnDetailView -from openwisp_controller.config.api.views import ( - VpnListCreateView as BaseVpnListCreateView, -) - - -class TemplateListCreateView(BaseTemplateListCreateView): - pass - - -class TemplateDetailView(BaseTemplateDetailView): - pass - - -class DownloadTemplateconfiguration(BaseDownloadTemplateconfiguration): - pass - - -class VpnListCreateView(BaseVpnListCreateView): - pass - - -class VpnDetailView(BaseVpnDetailView): - pass - - -class DownloadVpnView(BaseDownloadVpnView): - pass - - -class DeviceListCreateView(BaseDeviceListCreateView): - pass - - -class DeviceDetailView(BaseDeviceDetailView): - pass - - -class DeviceActivateView(BaseDeviceActivateView): - pass - - -class DeviceDeactivateView(BaseDeviceDeactivateView): - pass - - -class DeviceGroupListCreateView(BaseDeviceGroupListCreateView): - pass - - -class DeviceGroupDetailView(BaseDeviceGroupDetailView): - pass - - -class DeviceGroupCommonName(BaseDeviceGroupCommonName): - pass - - -class DownloadDeviceView(BaseDownloadDeviceView): - pass - - -template_list = TemplateListCreateView.as_view() -template_detail = TemplateDetailView.as_view() -download_template_config = DownloadTemplateconfiguration.as_view() -vpn_list = VpnListCreateView.as_view() -vpn_detail = VpnDetailView.as_view() -download_vpn_config = DownloadVpnView.as_view() -device_list = DeviceListCreateView.as_view() -device_detail = DeviceDetailView.as_view() -device_activate = DeviceActivateView.as_view() -device_deactivate = DeviceDeactivateView.as_view() -download_device_config = DownloadDeviceView().as_view() -devicegroup_list = DeviceGroupListCreateView.as_view() -devicegroup_detail = DeviceGroupDetailView.as_view() -devicegroup_commonname = DeviceGroupCommonName.as_view() diff --git a/tests/openwisp2/sample_config/apps.py b/tests/openwisp2/sample_config/apps.py deleted file mode 100644 index 993967a7e..000000000 --- a/tests/openwisp2/sample_config/apps.py +++ /dev/null @@ -1,9 +0,0 @@ -from openwisp_controller.config.apps import ConfigConfig - - -class SampleConfigConfig(ConfigConfig): - name = "openwisp2.sample_config" - label = "sample_config" - - -del ConfigConfig diff --git a/tests/openwisp2/sample_config/fixtures/test_templates.json b/tests/openwisp2/sample_config/fixtures/test_templates.json deleted file mode 100644 index b33fc2a75..000000000 --- a/tests/openwisp2/sample_config/fixtures/test_templates.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "pk": "d083b494-8e16-4054-9537-fb9eba914861", - "model": "sample_config.template", - "fields": { - "name": "dhcp", - "backend": "netjsonconfig.OpenWrt", - "config": { - "interfaces": [ - { - "name": "eth0", - "type": "ethernet", - "addresses": [ - { - "proto": "dhcp", - "family": "ipv4" - } - ] - } - ] - }, - "created": "2015-05-16T20:02:52.483Z", - "modified": "2015-05-16T19:33:41.621Z" - } - }, - { - "pk": "d083b494-8e16-4054-9537-fb9eba914862", - "model": "sample_config.template", - "fields": { - "name": "radio0", - "backend": "netjsonconfig.OpenWrt", - "config": { - "radios": [ - { - "name": "radio0", - "phy": "phy0", - "driver": "mac80211", - "protocol": "802.11n", - "channel": 11, - "channel_width": 20, - "tx_power": 8, - "country": "IT" - } - ] - }, - "created": "2015-05-16T20:02:52.483Z", - "modified": "2015-05-16T19:33:41.621Z" - } - } -] diff --git a/tests/openwisp2/sample_config/migrations/0001_initial.py b/tests/openwisp2/sample_config/migrations/0001_initial.py deleted file mode 100644 index 13b368684..000000000 --- a/tests/openwisp2/sample_config/migrations/0001_initial.py +++ /dev/null @@ -1,1048 +0,0 @@ -# Generated by Django 3.0.7 on 2020-06-27 11:16 -import collections -import re -import uuid - -import django.core.validators -import django.db.models.deletion -import django.utils.timezone -import jsonfield.fields -import model_utils.fields -import swapper -import taggit.managers -from django.conf import settings -from django.db import migrations, models - -import openwisp_controller.config.base.template -import openwisp_users.mixins -import openwisp_utils.base -import openwisp_utils.utils -from openwisp_controller.config import settings as app_settings -from openwisp_controller.config.base.template import default_auto_cert - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("contenttypes", "0002_remove_content_type_name"), - ("pki", "0007_default_groups_permissions"), - swapper.dependency( - *swapper.split(settings.AUTH_USER_MODEL), version="0004_default_groups" - ), - migrations.swappable_dependency(settings.OPENWISP_IPAM_IPADDRESS_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="Config", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ( - "backend", - models.CharField( - choices=app_settings.BACKENDS, - help_text=( - 'Select netjsonconfig backend' - ), - max_length=128, - verbose_name="backend", - ), - ), - ( - "config", - jsonfield.fields.JSONField( - blank=True, - default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - verbose_name="configuration", - ), - ), - ( - "status", - model_utils.fields.StatusField( - choices=[ - ("modified", "modified"), - ("applied", "applied"), - ("error", "error"), - ], - default="modified", - help_text=( - '"modified" means the configuration is not applied yet; \n' - '"applied" means the configuration is applied successfully;' - " \n" - '"error" means the configuration caused issues ' - "and it was rolled back;" - ), - max_length=100, - no_check_for_status=True, - verbose_name="configuration status", - ), - ), - ( - "error_reason", - models.CharField( - blank=True, - help_text="Error reason reported by the device", - max_length=1024, - verbose_name="error reason", - ), - ), - ( - "context", - jsonfield.fields.JSONField( - blank=True, - default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - help_text=( - 'Additional ' - "context (configuration variables) in JSON format" - ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - ), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ], - options={ - "verbose_name": "configuration", - "verbose_name_plural": "configurations", - "abstract": False, - }, - ), - migrations.CreateModel( - name="TaggedTemplate", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "object_id", - models.UUIDField(db_index=True, verbose_name="object ID"), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "content_type", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="%(app_label)s_%(class)s_tagged_items", - to="contenttypes.ContentType", - verbose_name="content type", - ), - ), - ], - options={ - "verbose_name": "Tagged item", - "verbose_name_plural": "Tags", - "abstract": False, - }, - ), - migrations.CreateModel( - name="TemplateTag", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "name", - models.CharField(max_length=100, unique=True, verbose_name="name"), - ), - ( - "slug", - models.SlugField( - allow_unicode=True, - max_length=100, - unique=True, - verbose_name="slug", - ), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ], - options={ - "verbose_name": "Tag", - "verbose_name_plural": "Tags", - "abstract": False, - }, - ), - migrations.CreateModel( - name="Vpn", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("name", models.CharField(db_index=True, max_length=64, unique=True)), - ( - "config", - jsonfield.fields.JSONField( - default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - verbose_name="configuration", - ), - ), - ( - "host", - models.CharField( - help_text="VPN server hostname or ip address", max_length=64 - ), - ), - ( - "key", - openwisp_utils.base.KeyField( - db_index=True, - default=openwisp_utils.utils.get_random_key, - help_text=None, - max_length=64, - validators=[ - django.core.validators.RegexValidator( - re.compile("^[^\\s/\\.]+$"), - code="invalid", - message=( - "This value must not contain spaces, " - "dots or slashes." - ), - ) - ], - ), - ), - ( - "backend", - models.CharField( - choices=[ - ("openwisp_controller.vpn_backends.OpenVpn", "OpenVPN"), - ("openwisp_controller.vpn_backends.Wireguard", "WireGuard"), - ( - "openwisp_controller.vpn_backends.VxlanWireguard", - "VXLAN over WireGuard", - ), - ("openwisp_controller.vpn_backends.ZeroTier", "ZeroTier"), - ], - help_text="Select VPN configuration backend", - max_length=128, - verbose_name="VPN backend", - ), - ), - ("notes", models.TextField(blank=True)), - ("dh", models.TextField(blank=True)), - ("public_key", models.CharField(blank=True, max_length=44)), - ("private_key", models.CharField(blank=True, max_length=44)), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "ca", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="pki.ca", - verbose_name="Certification Authority", - ), - ), - ( - "cert", - models.ForeignKey( - blank=True, - help_text="leave blank to create automatically", - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="pki.cert", - verbose_name="x509 Certificate", - ), - ), - ( - "ip", - models.ForeignKey( - blank=True, - help_text=( - "Internal IP address of the VPN " - "server interface, if applicable" - ), - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.OPENWISP_IPAM_IPADDRESS_MODEL, - verbose_name="Internal IP", - ), - ), - ( - "organization", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=swapper.get_model_name("openwisp_users", "Organization"), - verbose_name="organization", - ), - ), - ( - "subnet", - models.ForeignKey( - blank=True, - help_text=( - "Subnet IP addresses used by VPN clients, if applicable" - ), - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.OPENWISP_IPAM_SUBNET_MODEL, - verbose_name="Subnet", - ), - ), - ( - "auth_token", - models.CharField( - blank=True, - help_text=( - "Authentication token used for triggering " - '"Webhook Endpoint" or for calling "ZerotierService" API' - ), - max_length=128, - null=True, - verbose_name="Webhook AuthToken", - ), - ), - ( - "webhook_endpoint", - models.URLField( - blank=True, - help_text=( - "Webhook to trigger for updating server configuration " - "(e.g. https://openwisp2.mydomain.com:8081/trigger-update)" - ), - null=True, - verbose_name="Webhook Endpoint", - ), - ), - ("node_id", models.CharField(blank=True, max_length=10)), - ("network_id", models.CharField(blank=True, max_length=16)), - ], - options={ - "verbose_name": "VPN server", - "verbose_name_plural": "VPN servers", - "abstract": False, - }, - bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), - ), - migrations.CreateModel( - name="VpnClient", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("auto_cert", models.BooleanField(default=False)), - ( - "public_key", - models.CharField(blank=True, max_length=44), - ), - ( - "private_key", - models.CharField(blank=True, max_length=44), - ), - ( - "vni", - models.PositiveIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(16777216), - ], - ), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "cert", - models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="pki.cert", - ), - ), - ( - "config", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="sample_config.config", - ), - ), - ( - "ip", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="openwisp_ipam.ipaddress", - ), - ), - ( - "vpn", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="sample_config.vpn", - ), - ), - ("secret", models.TextField(blank=True)), - ], - options={ - "verbose_name": "VPN client", - "verbose_name_plural": "VPN clients", - "abstract": False, - "unique_together": {("config", "vpn")}, - }, - ), - migrations.CreateModel( - name="Template", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("name", models.CharField(db_index=True, max_length=64, unique=True)), - ( - "backend", - models.CharField( - choices=app_settings.BACKENDS, - help_text=( - 'Select netjsonconfig backend' - ), - max_length=128, - verbose_name="backend", - ), - ), - ( - "config", - jsonfield.fields.JSONField( - blank=True, - default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - verbose_name="configuration", - ), - ), - ( - "type", - models.CharField( - choices=[("generic", "Generic"), ("vpn", "VPN-client")], - db_index=True, - default="generic", - help_text=( - "template type, determines which features are available" - ), - max_length=16, - verbose_name="type", - ), - ), - ( - "default", - models.BooleanField( - db_index=True, - default=False, - help_text=( - "whether new configurations will have this " - "template enabled by default" - ), - verbose_name="enabled by default", - ), - ), - ( - "required", - models.BooleanField( - db_index=True, - default=False, - help_text=( - "if checked, will force the assignment of this template to " - "all the devices of the organization (if no organization " - "is selected, it will be required for every device " - "in the system)" - ), - verbose_name="required", - ), - ), - ( - "auto_cert", - models.BooleanField( - db_index=True, - default=default_auto_cert, - help_text=( - "whether tunnel specific configuration (cryptographic " - "keys, ip addresses, etc) should be automatically " - "generated and managed behind the scenes for each " - "configuration using this template, valid only for " - "the VPN type" - ), - verbose_name="automatic tunnel provisioning", - ), - ), - ( - "default_values", - jsonfield.fields.JSONField( - blank=True, - default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - help_text=( - "A dictionary containing the default values for the " - "variables used by this template; these default variables " - "will be used during schema validation." - ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - verbose_name="Default Values", - ), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "organization", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=swapper.get_model_name("openwisp_users", "Organization"), - verbose_name="organization", - ), - ), - ( - "tags", - taggit.managers.TaggableManager( - blank=True, - help_text=( - "A comma-separated list of template tags, may be used " - "to ease auto configuration with specific settings " - "(eg: 4G, mesh, WDS, VPN, ecc.)" - ), - through="sample_config.TaggedTemplate", - to="sample_config.TemplateTag", - verbose_name="Tags", - ), - ), - ( - "vpn", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="sample_config.vpn", - verbose_name="VPN", - ), - ), - ], - options={ - "verbose_name": "template", - "verbose_name_plural": "templates", - "abstract": False, - "unique_together": {("organization", "name")}, - }, - bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), - ), - migrations.AddField( - model_name="taggedtemplate", - name="tag", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="%(app_label)s_%(class)s_items", - to="sample_config.TemplateTag", - ), - ), - migrations.AddField( - model_name="vpnclient", - name="template", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="sample_config.template", - ), - ), - migrations.CreateModel( - name="OrganizationConfigSettings", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "registration_enabled", - models.BooleanField( - default=True, - help_text=( - "Whether automatic registration of " - "devices is enabled or not" - ), - verbose_name="auto-registration enabled", - ), - ), - ( - "shared_secret", - openwisp_utils.base.KeyField( - db_index=True, - default=openwisp_utils.utils.get_random_key, - help_text="used for automatic registration of devices", - max_length=32, - unique=True, - validators=[ - django.core.validators.RegexValidator( - re.compile("^[^\\s/\\.]+$"), - code="invalid", - message=( - "This value must not contain spaces, " - "dots or slashes." - ), - ) - ], - verbose_name="shared secret", - ), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "organization", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="config_settings", - to=swapper.get_model_name("openwisp_users", "Organization"), - verbose_name="organization", - ), - ), - ( - "context", - jsonfield.fields.JSONField( - blank=True, - default=dict, - dump_kwargs={"indent": 4}, - help_text=( - 'This field can be used to add "Configuration Variables"' - " to the devices." - ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - verbose_name="Configuration Variables", - ), - ), - ], - options={ - "verbose_name": "Configuration management settings", - "verbose_name_plural": "Configuration management settings", - "abstract": False, - }, - ), - migrations.CreateModel( - name="DeviceGroup", - fields=[ - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("name", models.CharField(max_length=60)), - ( - "description", - models.TextField(blank=True, help_text="internal notes"), - ), - ( - "meta_data", - jsonfield.fields.JSONField( - blank=True, - default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - help_text=( - "Group meta data, use this field to store data which is" - " related to this group and can be retrieved via the" - " REST API." - ), - verbose_name="Metadata", - ), - ), - ( - "context", - jsonfield.fields.JSONField( - blank=True, - default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - help_text=( - "This field can be used to add meta data for the group" - ' or to add "Configuration Variables" to the devices.' - ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - verbose_name="Configuration Variables", - ), - ), - ( - "organization", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=swapper.get_model_name("openwisp_users", "Organization"), - verbose_name="organization", - ), - ), - ], - options={ - "verbose_name": "Device Group", - "verbose_name_plural": "Device Groups", - "abstract": False, - "swappable": "CONFIG_DEVICEGROUP_MODEL", - }, - bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), - ), - migrations.AlterUniqueTogether( - name="devicegroup", - unique_together={("organization", "name")}, - ), - migrations.CreateModel( - name="Device", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ( - "name", - models.CharField( - db_index=True, - max_length=64, - validators=[ - django.core.validators.RegexValidator( - re.compile( - "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}" - "[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9]" - "[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$|^" - "([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" - ), - code="invalid", - message=( - "Must be either a valid hostname or mac address." - ), - ) - ], - help_text=("must be either a valid hostname or mac address"), - ), - ), - ( - "mac_address", - models.CharField( - db_index=True, - help_text="primary mac address", - max_length=17, - validators=[ - django.core.validators.RegexValidator( - re.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"), - code="invalid", - message="Must be a valid mac address.", - ) - ], - ), - ), - ( - "group", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=swapper.get_model_name("config", "DeviceGroup"), - verbose_name="group", - ), - ), - ( - "key", - openwisp_utils.base.KeyField( - blank=True, - db_index=True, - default=None, - help_text="unique device key", - max_length=64, - unique=True, - validators=[ - django.core.validators.RegexValidator( - re.compile("^[^\\s/\\.]+$"), - code="invalid", - message=( - "This value must not contain spaces, " - "dots or slashes." - ), - ) - ], - ), - ), - ( - "model", - models.CharField( - blank=True, - db_index=True, - help_text="device model and manufacturer", - max_length=64, - ), - ), - ( - "os", - models.CharField( - blank=True, - db_index=True, - help_text="operating system identifier", - max_length=128, - verbose_name="operating system", - ), - ), - ( - "system", - models.CharField( - blank=True, - db_index=True, - help_text="system on chip or CPU info", - max_length=128, - verbose_name="SOC / CPU", - ), - ), - ("notes", models.TextField(blank=True, help_text="internal notes")), - ( - "last_ip", - models.GenericIPAddressField( - blank=True, - db_index=True, - help_text=( - "indicates the IP address logged from the last " - "request coming from the device" - ), - null=True, - ), - ), - ( - "management_ip", - models.GenericIPAddressField( - blank=True, - db_index=True, - help_text=( - "IP address used by the system to reach the device when " - "performing any type of push operation or active check. " - "The value of this field is generally sent by the device " - "and hence does not need to be changed, but can be " - "changed or cleared manually if needed." - ), - null=True, - ), - ), - ( - "hardware_id", - models.CharField( - blank=True, - help_text="Serial number of this device", - max_length=32, - null=True, - verbose_name="Serial number", - ), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "organization", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=swapper.get_model_name("openwisp_users", "Organization"), - verbose_name="organization", - ), - ), - ], - options={ - "abstract": False, - "unique_together": { - ("mac_address", "organization"), - ("hardware_id", "organization"), - }, - "verbose_name": app_settings.DEVICE_VERBOSE_NAME[0], - "verbose_name_plural": app_settings.DEVICE_VERBOSE_NAME[1], - }, - bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), - ), - migrations.CreateModel( - name="OrganizationLimits", - fields=[ - ( - "device_limit", - models.BigIntegerField( - default=0, - blank=True, - null=True, - help_text=( - "Maximum number of devices allowed for this organization." - ' "0" means unlimited.' - ), - verbose_name="device limit", - ), - ), - ("details", models.CharField(blank=True, max_length=64, null=True)), - ( - "organization", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - primary_key=True, - serialize=False, - related_name="config_limits", - to="openwisp_users.organization", - verbose_name="organization", - ), - ), - ], - options={ - "verbose_name": "controller limits", - "verbose_name_plural": "controller limits", - "abstract": False, - }, - ), - migrations.AddField( - model_name="config", - name="device", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, to="sample_config.device" - ), - ), - migrations.AddField( - model_name="config", - name="templates", - field=openwisp_controller.config.sortedm2m.fields.SortedManyToManyField( - blank=True, - help_text="configuration templates, applied from first to last", - related_name="config_relations", - to="sample_config.Template", - verbose_name="templates", - ), - ), - migrations.AddField( - model_name="config", - name="vpn", - field=models.ManyToManyField( - blank=True, - related_name="vpn_relations", - through="sample_config.VpnClient", - to="sample_config.Vpn", - ), - ), - ] diff --git a/tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py b/tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py deleted file mode 100644 index b29ddb496..000000000 --- a/tests/openwisp2/sample_config/migrations/0002_default_groups_permissions.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - -from openwisp_controller.config.migrations import ( - assign_devicegroup_permissions_to_groups, - assign_organization_config_settings_permissions_to_groups, - assign_permissions_to_groups, -) - - -class Migration(migrations.Migration): - dependencies = [("sample_config", "0001_initial")] - - operations = [ - migrations.RunPython( - assign_permissions_to_groups, reverse_code=migrations.RunPython.noop - ), - migrations.RunPython( - assign_devicegroup_permissions_to_groups, - reverse_code=migrations.RunPython.noop, - ), - migrations.RunPython( - code=assign_organization_config_settings_permissions_to_groups, - reverse_code=migrations.operations.special.RunPython.noop, - ), - ] diff --git a/tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py b/tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py deleted file mode 100644 index 02ddb02df..000000000 --- a/tests/openwisp2/sample_config/migrations/0003_name_unique_per_organization.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.1.6 on 2021-02-11 22:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("sample_config", "0002_default_groups_permissions"), - ] - - operations = [ - migrations.AlterField( - model_name="template", - name="name", - field=models.CharField(db_index=True, max_length=64), - ), - migrations.AlterField( - model_name="vpn", - name="name", - field=models.CharField(db_index=True, max_length=64), - ), - migrations.AlterUniqueTogether( - name="vpn", unique_together={("organization", "name")} - ), - ] diff --git a/tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py b/tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py deleted file mode 100644 index 845e5bb57..000000000 --- a/tests/openwisp2/sample_config/migrations/0004_devicegroup_templates.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-06 13:51 - -from django.db import migrations - -import openwisp_controller.config.sortedm2m.fields - - -class Migration(migrations.Migration): - dependencies = [ - ("sample_config", "0003_name_unique_per_organization"), - ] - - operations = [ - migrations.AddField( - model_name="devicegroup", - name="templates", - field=openwisp_controller.config.sortedm2m.fields.SortedManyToManyField( - blank=True, - help_text=( - "These templates are automatically assigned to the devices " - "that are part of the group. Default and required templates " - "are excluded from this list. If the group of the device is " - "changed, these templates will be automatically removed and " - "the templates of the new group will be assigned." - ), - related_name="device_group_relations", - to="sample_config.template", - verbose_name="templates", - ), - ), - ] diff --git a/tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py b/tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py deleted file mode 100644 index e94d35da8..000000000 --- a/tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.15 on 2022-09-16 11:45 - - -from django.db import migrations - -from openwisp_controller.config.migrations import populate_organization_allowed_device - - -class Migration(migrations.Migration): - dependencies = [ - ("sample_config", "0004_devicegroup_templates"), - ] - - operations = [ - migrations.RunPython( - populate_organization_allowed_device, migrations.RunPython.noop - ) - ] diff --git a/tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py b/tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py deleted file mode 100644 index 37adfa228..000000000 --- a/tests/openwisp2/sample_config/migrations/0006_device__is_deactivated_alter_config_status.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2.10 on 2024-03-01 16:37 - -import model_utils.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("sample_config", "0005_add_organizationalloweddevice"), - ] - - operations = [ - migrations.AddField( - model_name="device", - name="_is_deactivated", - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name="config", - name="status", - field=model_utils.fields.StatusField( - choices=[ - ("modified", "modified"), - ("applied", "applied"), - ("error", "error"), - ("deactivating", "deactivating"), - ("deactivated", "deactivated"), - ], - default="modified", - help_text=( - '"modified" means the configuration is not applied yet;' - ' \n"applied" means the configuration is applied successfully;' - ' \n"error" means the configuration caused issues and it was' - " rolled back;" - ), - max_length=100, - no_check_for_status=True, - verbose_name="configuration status", - ), - ), - ] diff --git a/tests/openwisp2/sample_config/migrations/0007_alter_config_status.py b/tests/openwisp2/sample_config/migrations/0007_alter_config_status.py deleted file mode 100644 index e66e888bd..000000000 --- a/tests/openwisp2/sample_config/migrations/0007_alter_config_status.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.11 on 2024-03-07 17:10 - -import model_utils.fields -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("sample_config", "0006_device__is_deactivated_alter_config_status"), - ] - - operations = [ - migrations.AlterField( - model_name="config", - name="status", - field=model_utils.fields.StatusField( - choices=[ - ("modified", "modified"), - ("applied", "applied"), - ("error", "error"), - ("deactivating", "deactivating"), - ("deactivated", "deactivated"), - ], - default="modified", - help_text=( - '"modified" means the configuration is not applied yet; \n' - '"applied" means the configuration is applied successfully; \n' - '"error" means the configuration caused issues and it was' - ' rolled back; \n"deactivating" means the device has been' - " deactivated and the configuration is being removed; \n" - '"deactivated" means the configuration has been removed ' - "from the device;" - ), - max_length=100, - no_check_for_status=True, - verbose_name="configuration status", - ), - ), - ] diff --git a/tests/openwisp2/sample_config/migrations/__init__.py b/tests/openwisp2/sample_config/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/openwisp2/sample_config/models.py b/tests/openwisp2/sample_config/models.py deleted file mode 100644 index 8fec929db..000000000 --- a/tests/openwisp2/sample_config/models.py +++ /dev/null @@ -1,113 +0,0 @@ -from django.db import models - -from openwisp_controller.config.base.config import AbstractConfig -from openwisp_controller.config.base.device import AbstractDevice -from openwisp_controller.config.base.device_group import AbstractDeviceGroup -from openwisp_controller.config.base.multitenancy import ( - AbstractOrganizationConfigSettings, - AbstractOrganizationLimits, -) -from openwisp_controller.config.base.tag import ( - AbstractTaggedTemplate, - AbstractTemplateTag, -) -from openwisp_controller.config.base.template import AbstractTemplate -from openwisp_controller.config.base.vpn import AbstractVpn, AbstractVpnClient - - -class DetailsModel(models.Model): - details = models.CharField(max_length=64, blank=True, null=True) - - class Meta: - abstract = True - - -class Device(DetailsModel, AbstractDevice): - """ - Concrete Device model - """ - - class Meta(AbstractDevice.Meta): - abstract = False - - -class DeviceGroup(DetailsModel, AbstractDeviceGroup): - """ - Concrete Device model - """ - - class Meta(AbstractDeviceGroup.Meta): - abstract = False - - -class Config(DetailsModel, AbstractConfig): - """ - Concrete Config model - """ - - class Meta(AbstractConfig.Meta): - abstract = False - - -class TemplateTag(DetailsModel, AbstractTemplateTag): - """ - openwisp-controller TemplateTag model - """ - - class Meta(AbstractTemplateTag.Meta): - abstract = False - - -class TaggedTemplate(DetailsModel, AbstractTaggedTemplate): - """ - openwisp-controller TaggedTemplate model - """ - - class Meta(AbstractTaggedTemplate.Meta): - abstract = False - - -class Template(DetailsModel, AbstractTemplate): - """ - openwisp-controller Template model - """ - - class Meta(AbstractTemplate.Meta): - abstract = False - - -class Vpn(DetailsModel, AbstractVpn): - """ - openwisp-controller VPN model - """ - - class Meta(AbstractVpn.Meta): - abstract = False - - -class VpnClient(DetailsModel, AbstractVpnClient): - """ - m2m through model - """ - - class Meta(AbstractVpnClient.Meta): - abstract = False - - -class OrganizationConfigSettings(DetailsModel, AbstractOrganizationConfigSettings): - """ - Configuration management settings - specific to each organization - """ - - class Meta(AbstractOrganizationConfigSettings.Meta): - abstract = False - - -class OrganizationLimits(DetailsModel, AbstractOrganizationLimits): - """ - Number of allowed devices specific to each organization - """ - - class Meta(AbstractOrganizationLimits.Meta): - abstract = False diff --git a/tests/openwisp2/sample_config/pytest.py b/tests/openwisp2/sample_config/pytest.py deleted file mode 100644 index 06a938300..000000000 --- a/tests/openwisp2/sample_config/pytest.py +++ /dev/null @@ -1,10 +0,0 @@ -from openwisp_controller.config.tests.pytest import ( - TestDeviceConsumer as BaseTestDeviceConsumer, -) - - -class TestDeviceConsumer(BaseTestDeviceConsumer): - pass - - -del BaseTestDeviceConsumer diff --git a/tests/openwisp2/sample_config/tests.py b/tests/openwisp2/sample_config/tests.py deleted file mode 100644 index 6b5caef9f..000000000 --- a/tests/openwisp2/sample_config/tests.py +++ /dev/null @@ -1,143 +0,0 @@ -from openwisp_controller.config.tests.test_admin import TestAdmin as BaseTestAdmin -from openwisp_controller.config.tests.test_admin import ( - TestDeviceGroupAdmin as BaseTestDeviceGroupAdmin, -) -from openwisp_controller.config.tests.test_admin import ( - TestDeviceGroupAdminTransaction as BaseTestDeviceGroupAdminTransaction, -) -from openwisp_controller.config.tests.test_admin import ( - TestTransactionAdmin as BaseTestTransactionAdmin, -) -from openwisp_controller.config.tests.test_api import TestConfigApi as BaseTestConfigApi -from openwisp_controller.config.tests.test_apps import TestApps as BaseTestApps -from openwisp_controller.config.tests.test_config import TestConfig as BaseTestConfig -from openwisp_controller.config.tests.test_config import ( - TestTransactionConfig as BaseTestTransactionConfig, -) -from openwisp_controller.config.tests.test_controller import ( - TestController as BaseTestController, -) -from openwisp_controller.config.tests.test_device import TestDevice as BaseTestDevice -from openwisp_controller.config.tests.test_device_group import ( - TestDeviceGroup as BaseTestDeviceGroup, -) -from openwisp_controller.config.tests.test_notifications import ( - TestNotifications as BaseTestNotifications, -) -from openwisp_controller.config.tests.test_tag import TestTag as BaseTestTag -from openwisp_controller.config.tests.test_template import ( - TestTemplate as BaseTestTemplate, -) -from openwisp_controller.config.tests.test_template import ( - TestTemplateTransaction as BaseTestTemplateTransaction, -) -from openwisp_controller.config.tests.test_views import TestViews as BaseTestViews -from openwisp_controller.config.tests.test_vpn import TestVpn as BaseTestVpn -from openwisp_controller.config.tests.test_vpn import ( - TestVpnTransaction as BaseTestVpnTransaction, -) -from openwisp_controller.config.tests.test_vpn import TestVxlan as BaseTestVxlan -from openwisp_controller.config.tests.test_vpn import TestWireguard as BaseTestWireguard - - -class TestAdmin(BaseTestAdmin): - app_label = "sample_config" - - -class TestTransactionAdmin(BaseTestTransactionAdmin): - app_label = "sample_config" - _deactivated_device_expected_readonly_fields = 23 - - -class TestDeviceGroupAdmin(BaseTestDeviceGroupAdmin): - app_label = "sample_config" - - -class TestDeviceGroupAdminTransaction(BaseTestDeviceGroupAdminTransaction): - app_label = "sample_config" - - -class TestConfig(BaseTestConfig): - pass - - -class TestTransactionConfig(BaseTestTransactionConfig): - pass - - -class TestController(BaseTestController): - pass - - -class TestDevice(BaseTestDevice): - pass - - -class TestDeviceGroup(BaseTestDeviceGroup): - pass - - -class TestTag(BaseTestTag): - pass - - -class TestTemplate(BaseTestTemplate): - pass - - -class TestTemplateTransaction(BaseTestTemplateTransaction): - pass - - -class TestNotifications(BaseTestNotifications): - app_label = "sample_config" - - -class TestViews(BaseTestViews): - pass - - -class TestVpn(BaseTestVpn): - pass - - -class TestVpnTransaction(BaseTestVpnTransaction): - pass - - -class TestApps(BaseTestApps): - pass - - -class TestConfigApi(BaseTestConfigApi): - pass - - -class TestWireguard(BaseTestWireguard): - pass - - -class TestVxlan(BaseTestVxlan): - pass - - -del BaseTestAdmin -del BaseTestTransactionAdmin -del BaseTestDeviceGroupAdmin -del BaseTestDeviceGroupAdminTransaction -del BaseTestConfig -del BaseTestTransactionConfig -del BaseTestController -del BaseTestDevice -del BaseTestDeviceGroup -del BaseTestTag -del BaseTestTemplate -del BaseTestTemplateTransaction -del BaseTestNotifications -del BaseTestViews -del BaseTestVpn -del BaseTestVpnTransaction -del BaseTestApps -del BaseTestConfigApi -del BaseTestWireguard -del BaseTestVxlan diff --git a/tests/openwisp2/sample_config/views.py b/tests/openwisp2/sample_config/views.py deleted file mode 100644 index c120149cf..000000000 --- a/tests/openwisp2/sample_config/views.py +++ /dev/null @@ -1,65 +0,0 @@ -from swapper import load_model - -from openwisp_controller.config.controller.views import ( - DeviceChecksumView as BaseDeviceChecksumView, -) -from openwisp_controller.config.controller.views import ( - DeviceDownloadConfigView as BaseDeviceDownloadConfigView, -) -from openwisp_controller.config.controller.views import ( - DeviceRegisterView as BaseDeviceRegisterView, -) -from openwisp_controller.config.controller.views import ( - DeviceReportStatusView as BaseDeviceReportStatusView, -) -from openwisp_controller.config.controller.views import ( - DeviceUpdateInfoView as BaseDeviceUpdateInfoView, -) -from openwisp_controller.config.controller.views import ( - VpnChecksumView as BaseVpnChecksumView, -) -from openwisp_controller.config.controller.views import ( - VpnDownloadConfigView as BaseVpnDownloadConfigView, -) - -Device = load_model("config", "Device") -OrganizationConfigSettings = load_model("config", "OrganizationConfigSettings") -Vpn = load_model("config", "Vpn") - - -class DeviceChecksumView(BaseDeviceChecksumView): - model = Device - - -class DeviceDownloadConfigView(BaseDeviceDownloadConfigView): - model = Device - - -class DeviceUpdateInfoView(BaseDeviceUpdateInfoView): - model = Device - - -class DeviceReportStatusView(BaseDeviceReportStatusView): - model = Device - - -class DeviceRegisterView(BaseDeviceRegisterView): - model = Device - org_config_settings_model = OrganizationConfigSettings - - -class VpnChecksumView(BaseVpnChecksumView): - model = Vpn - - -class VpnDownloadConfigView(BaseVpnDownloadConfigView): - model = Vpn - - -device_checksum = DeviceChecksumView.as_view() -device_download_config = DeviceDownloadConfigView.as_view() -device_update_info = DeviceUpdateInfoView.as_view() -device_report_status = DeviceReportStatusView.as_view() -device_register = DeviceRegisterView.as_view() -vpn_checksum = VpnChecksumView.as_view() -vpn_download_config = VpnDownloadConfigView.as_view() From 7c1f2a78ce80f927f9e1468cb897dde0a787cc5c Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Mon, 29 Dec 2025 15:07:42 +0100 Subject: [PATCH 4/8] [change] Formatting --- .../tests/test_admin.py | 76 ++++++++--------- .../tests/test_selenium.py | 84 +++++++++---------- .../tests/test_tasks.py | 16 ++-- tests/openwisp2/sample_connection/tests.py | 19 ++--- tests/openwisp2/urls.py | 4 +- 5 files changed, 97 insertions(+), 102 deletions(-) diff --git a/openwisp_firmware_upgrader/tests/test_admin.py b/openwisp_firmware_upgrader/tests/test_admin.py index f9378f9f7..546aefee0 100644 --- a/openwisp_firmware_upgrader/tests/test_admin.py +++ b/openwisp_firmware_upgrader/tests/test_admin.py @@ -46,9 +46,9 @@ class MockRequest: class BaseTestAdmin(TestMultitenantAdminMixin, TestUpgraderMixin): - app_label = 'firmware_upgrader' - config_app_label = 'config' - connection_app_label = 'connection' + app_label = "firmware_upgrader" + config_app_label = "config" + connection_app_label = "connection" _device_params = TestConfigAdmin._device_params.copy() _device_params.update( { @@ -109,7 +109,7 @@ def setUp(self, *args, **kwargs): def make_device_admin_request(self, pk): return self.factory.get( - reverse(f'admin:{self.config_app_label}_device_change', args=[pk]) + reverse(f"admin:{self.config_app_label}_device_change", args=[pk]) ) @property @@ -189,9 +189,9 @@ def test_view_device_administrator(self): device_fw = self._create_device_firmware() org = self._get_org() self._create_administrator(organizations=[org]) - self._login(username='administrator', password='tester') + self._login(username="administrator", password="tester") url = reverse( - f'admin:{self.config_app_label}_device_change', args=[device_fw.device_id] + f"admin:{self.config_app_label}_device_change", args=[device_fw.device_id] ) r = self.client.get(url) self.assertContains(r, str(device_fw.image_id)) @@ -365,7 +365,7 @@ def test_save_device_with_deleted_devicefirmware(self): ) FirmwareImage.objects.all().delete() response = self.client.post( - reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]), + reverse(f"admin:{self.config_app_label}_device_change", args=[device.id]), data=device_params, follow=True, ) @@ -383,7 +383,7 @@ def test_device_firmware_upgrade_without_device_connection( device = device_fw.device device.deviceconnection_set.all().delete() response = self.client.get( - reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) + reverse(f"admin:{self.config_app_label}_device_change", args=[device.id]) ) self.assertNotIn( "'NoneType' object has no attribute 'update_strategy'", @@ -397,7 +397,7 @@ def test_deactivated_firmware_image_inline(self): device = self._create_config(organization=self._get_org()).device device.deactivate() response = self.client.get( - reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) + reverse(f"admin:{self.config_app_label}_device_change", args=[device.id]) ) # Check that it is not possible to add a DeviceFirmwareImage to a # deactivated device in the admin interface. @@ -408,7 +408,7 @@ def test_deactivated_firmware_image_inline(self): ) self._create_device_firmware(device=device) response = self.client.get( - reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) + reverse(f"admin:{self.config_app_label}_device_change", args=[device.id]) ) # Ensure that a deactivated device's existing DeviceFirmwareImage # is displayed as readonly in the admin interface. @@ -429,7 +429,7 @@ def test_device_upgrade_shared_firmware(self, *args): device = self._create_device_with_connection() device_conn = device.deviceconnection_set.first() device_params = self._get_device_params(device, device_conn, shared_image) - path = reverse(f'admin:{self.config_app_label}_device_change', args=[device.id]) + path = reverse(f"admin:{self.config_app_label}_device_change", args=[device.id]) with self.subTest("Test with administrator account"): self.client.force_login(administrator) @@ -519,8 +519,8 @@ def test_admin_multitenancy(self): class TestAdminTransaction( BaseTestAdmin, AdminActionPermTestMixin, TransactionTestCase ): - _mock_upgrade = 'openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade' - _mock_connect = 'openwisp_controller.connection.models.DeviceConnection.connect' + _mock_upgrade = "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade" + _mock_connect = "openwisp_controller.connection.models.DeviceConnection.connect" @mock.patch(_mock_upgrade, return_value=True) def test_upgrade_selected_action_perms(self, *args): @@ -602,7 +602,7 @@ def test_upgrade_related(self, *args): self.assertContains(r, "track the progress") self.assertEqual( UpgradeOperation.objects.filter( - upgrade_options={'c': True} + upgrade_options={"c": True} ).count(), 2, ) @@ -657,7 +657,7 @@ def test_upgrade_all(self, *args): self.assertContains(response, "track the progress") self.assertEqual( UpgradeOperation.objects.filter( - upgrade_options={'c': True} + upgrade_options={"c": True} ).count(), 3, ) @@ -730,7 +730,7 @@ def test_massive_upgrade_operation_page(self, *args): self.test_upgrade_all() uo = UpgradeOperation.objects.first() url = reverse( - f'admin:{self.app_label}_batchupgradeoperation_change', + f"admin:{self.app_label}_batchupgradeoperation_change", args=[uo.batch.pk], ) response = self.client.get(url) @@ -744,7 +744,7 @@ def test_recent_upgrades(self, *args): self._login() env = self._create_upgrade_env() url = reverse( - f'admin:{self.config_app_label}_device_change', args=[env['d2'].pk] + f"admin:{self.config_app_label}_device_change", args=[env["d2"].pk] ) r = self.client.get(url) self.assertNotContains(r, "Recent Firmware Upgrades") @@ -812,7 +812,7 @@ def test_device_firmware_upgrade_options(self, *args): ) response = self.client.post( reverse( - f'admin:{self.config_app_label}_device_change', args=[device.id] + f"admin:{self.config_app_label}_device_change", args=[device.id] ), data=device_params, follow=True, @@ -874,18 +874,18 @@ def test_using_upgrade_options_with_unsupported_upgrader(self, *args): ) device_params.update( { - 'model': device.model, - 'devicefirmware-0-image': str(image.id), - 'devicefirmware-0-id': str(device_fw.id), - 'devicefirmware-0-upgrade_options': json.dumps(upgrade_options), - 'organization': str(device.organization.id), - 'config-0-id': str(device.config.pk), - 'config-0-device': str(device.id), - 'deviceconnection_set-0-credentials': str( + "model": device.model, + "devicefirmware-0-image": str(image.id), + "devicefirmware-0-id": str(device_fw.id), + "devicefirmware-0-upgrade_options": json.dumps(upgrade_options), + "organization": str(device.organization.id), + "config-0-id": str(device.config.pk), + "config-0-device": str(device.id), + "deviceconnection_set-0-credentials": str( device_conn.credentials_id ), - 'deviceconnection_set-0-id': str(device_conn.id), - 'deviceconnection_set-0-update_strategy': ( + "deviceconnection_set-0-id": str(device_conn.id), + "deviceconnection_set-0-update_strategy": ( conn_settings.DEFAULT_UPDATE_STRATEGIES[1][0] ), "deviceconnection_set-0-enabled": True, @@ -899,20 +899,20 @@ def test_using_upgrade_options_with_unsupported_upgrader(self, *args): } ) - with self.subTest('Test DeviceFirmwareInline does not have schema defined'): + with self.subTest("Test DeviceFirmwareInline does not have schema defined"): response = self.client.get( reverse( - f'admin:{self.config_app_label}_device_change', args=[device.id] + f"admin:{self.config_app_label}_device_change", args=[device.id] ) ) self.assertContains( - response, '' + response, "" ) - with self.subTest('Test using upgrade options with unsupported upgrader'): + with self.subTest("Test using upgrade options with unsupported upgrader"): response = self.client.post( reverse( - f'admin:{self.config_app_label}_device_change', args=[device.id] + f"admin:{self.config_app_label}_device_change", args=[device.id] ), data=device_params, follow=True, @@ -921,15 +921,15 @@ def test_using_upgrade_options_with_unsupported_upgrader(self, *args): response, ( '
      • Using upgrade ' - 'options is not allowed with this upgrader.
      ' + "options is not allowed with this upgrader.
    " ), ) - with self.subTest('Test upgrading without upgrade options'): - del device_params['devicefirmware-0-upgrade_options'] + with self.subTest("Test upgrading without upgrade options"): + del device_params["devicefirmware-0-upgrade_options"] response = self.client.post( reverse( - f'admin:{self.config_app_label}_device_change', args=[device.id] + f"admin:{self.config_app_label}_device_change", args=[device.id] ), data=device_params, follow=True, @@ -938,7 +938,7 @@ def test_using_upgrade_options_with_unsupported_upgrader(self, *args): response, ( '
    Upgrade options are ' - 'not supported for this upgrader.
    ' + "not supported for this upgrader.
    " ), ) diff --git a/openwisp_firmware_upgrader/tests/test_selenium.py b/openwisp_firmware_upgrader/tests/test_selenium.py index 729f26612..ba469bab7 100644 --- a/openwisp_firmware_upgrader/tests/test_selenium.py +++ b/openwisp_firmware_upgrader/tests/test_selenium.py @@ -27,11 +27,11 @@ @tag("selenium_tests") class TestDeviceAdmin(TestUpgraderMixin, SeleniumTestMixin, StaticLiveServerTestCase): - config_app_label = 'config' - firmware_app_label = 'firmware_upgrader' - os = 'OpenWrt 19.07-SNAPSHOT r11061-6ffd4d8a4d' - image_type = REVERSE_FIRMWARE_IMAGE_MAP['YunCore XD3200'] - _mock_connect = 'openwisp_controller.connection.models.DeviceConnection.connect' + config_app_label = "config" + firmware_app_label = "firmware_upgrader" + os = "OpenWrt 19.07-SNAPSHOT r11061-6ffd4d8a4d" + image_type = REVERSE_FIRMWARE_IMAGE_MAP["YunCore XD3200"] + _mock_connect = "openwisp_controller.connection.models.DeviceConnection.connect" def _set_up_env(self): org = self._get_org() @@ -123,22 +123,22 @@ def save_device(): self.find_element( by=By.XPATH, value='//*[@id="device_form"]/div/div[1]/input[3]' ).click() - self.wait_for_visibility(By.CSS_SELECTOR, '#devicefirmware-group') + self.wait_for_visibility(By.CSS_SELECTOR, "#devicefirmware-group") self.hide_loading_overlay() _, _, _, _, _, image, device = self._set_up_env() self.login() self.open( - '{}#devicefirmware-group'.format( + "{}#devicefirmware-group".format( reverse( - f'admin:{self.config_app_label}_device_change', args=[device.id] + f"admin:{self.config_app_label}_device_change", args=[device.id] ) ) ) self.hide_loading_overlay() # JSONSchema Editor should not be rendered without a change in the image field self.wait_for_invisibility( - By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" ) image_select = self._get_device_firmware_dropdown_select() image_select.select_by_value(str(image.pk)) @@ -148,9 +148,9 @@ def save_device(): '//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]/div/h3/span[4]/input', ) # Select "None" image should hide JSONSchema Editor - image_select.select_by_value('') + image_select.select_by_value("") self.wait_for_invisibility( - By.CSS_SELECTOR, '#id_devicefirmware-0-upgrade_options_jsoneditor' + By.CSS_SELECTOR, "#id_devicefirmware-0-upgrade_options_jsoneditor" ) # Select "build2" image @@ -159,18 +159,18 @@ def save_device(): self.find_element( by=By.XPATH, value='//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]' - '/div/div[2]/div/div/div[1]/div/div[1]/label/input', + "/div/div[2]/div/div/div[1]/div/div[1]/label/input", ).click() # Enable '-F' option self.find_element( by=By.XPATH, value='//*[@id="id_devicefirmware-0-upgrade_options_jsoneditor"]' - '/div/div[2]/div/div/div[7]/div/div[1]/label/input', + "/div/div[2]/div/div/div[7]/div/div[1]/label/input", ).click() save_device() # Delete DeviceFirmware - self.find_element(By.CSS_SELECTOR, '#id_devicefirmware-0-DELETE').click() + self.find_element(By.CSS_SELECTOR, "#id_devicefirmware-0-DELETE").click() save_device() # When adding firmware to the device for the first time, @@ -181,12 +181,12 @@ def save_device(): ).click() # JSONSchema Editor should not be rendered without a change in the image field self.wait_for_invisibility( - By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" ) image_select = self._get_device_firmware_dropdown_select() image_select.select_by_index(1) self.wait_for_visibility( - By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" ) save_device() @@ -201,7 +201,7 @@ def test_batch_upgrade_upgrade_options(self, *args): self.login() self.open( reverse( - f'admin:{self.firmware_app_label}_build_change', args=[build2.id] + f"admin:{self.firmware_app_label}_build_change", args=[build2.id] ) ) # Launch mass upgrade operation @@ -211,7 +211,7 @@ def test_batch_upgrade_upgrade_options(self, *args): ).click() # Ensure JSONSchema form is rendered - self.wait_for_visibility(By.CSS_SELECTOR, '.jsoneditor-wrapper') + self.wait_for_visibility(By.CSS_SELECTOR, ".jsoneditor-wrapper") # JSONSchema configuration editor should not be rendered self.wait_for_invisibility( By.XPATH, @@ -235,20 +235,20 @@ def test_batch_upgrade_upgrade_options(self, *args): ).click() try: WebDriverWait(self.web_driver, 5).until( - EC.url_contains('batchupgradeoperation') + EC.url_contains("batchupgradeoperation") ) except TimeoutException: - self.fail('User was not redirected to Mass upgrade operations page') + self.fail("User was not redirected to Mass upgrade operations page") self.assertEqual( BatchUpgradeOperation.objects.filter( upgrade_options={ - 'c': False, - 'o': False, - 'n': True, - 'u': False, - 'p': False, - 'k': False, - 'F': False, + "c": False, + "o": False, + "n": True, + "u": False, + "p": False, + "k": False, + "F": False, } ).count(), 1, @@ -256,13 +256,13 @@ def test_batch_upgrade_upgrade_options(self, *args): self.assertEqual( UpgradeOperation.objects.filter( upgrade_options={ - 'c': False, - 'o': False, - 'n': True, - 'u': False, - 'p': False, - 'k': False, - 'F': False, + "c": False, + "o": False, + "n": True, + "u": False, + "p": False, + "k": False, + "F": False, } ).count(), 1, @@ -273,17 +273,17 @@ def test_batch_upgrade_upgrade_options(self, *args): "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade", return_value=True, ) - @patch.object(OpenWrt, 'SCHEMA', None) + @patch.object(OpenWrt, "SCHEMA", None) def test_upgrader_with_unsupported_upgrade_options(self, *args): with patch(self._mock_connect, return_value=True): org, category, build1, build2, image1, image2, device = self._set_up_env() self.login() - with self.subTest('Test DeviceFirmware'): + with self.subTest("Test DeviceFirmware"): self.open( - '{}#devicefirmware-group'.format( + "{}#devicefirmware-group".format( reverse( - f'admin:{self.config_app_label}_device_change', + f"admin:{self.config_app_label}_device_change", args=[device.id], ) ) @@ -294,7 +294,7 @@ def test_upgrader_with_unsupported_upgrade_options(self, *args): # Ensure JSONSchema editor is not rendered because # the upgrader does not define a schema self.wait_for_invisibility( - By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" ) self.find_element( by=By.XPATH, value='//*[@id="device_form"]/div/div[1]/input[3]' @@ -305,10 +305,10 @@ def test_upgrader_with_unsupported_upgrade_options(self, *args): DeviceFirmware.objects.all().delete() UpgradeOperation.objects.all().delete() - with self.subTest('Test BatchUpgradeOperation'): + with self.subTest("Test BatchUpgradeOperation"): self.open( reverse( - f'admin:{self.firmware_app_label}_build_change', + f"admin:{self.firmware_app_label}_build_change", args=[build2.id], ) ) @@ -320,7 +320,7 @@ def test_upgrader_with_unsupported_upgrade_options(self, *args): # Ensure JSONSchema editor is not rendered because # the upgrader does not define a schema self.wait_for_invisibility( - By.CSS_SELECTOR, '#devicefirmware-group .jsoneditor-wrapper' + By.CSS_SELECTOR, "#devicefirmware-group .jsoneditor-wrapper" ) # Upgrade all devices self.find_element( diff --git a/openwisp_firmware_upgrader/tests/test_tasks.py b/openwisp_firmware_upgrader/tests/test_tasks.py index 3673bd603..4d98d7cd2 100644 --- a/openwisp_firmware_upgrader/tests/test_tasks.py +++ b/openwisp_firmware_upgrader/tests/test_tasks.py @@ -28,8 +28,8 @@ def test_upgrade_firmware_timeout(self, *args): device_fw = self._create_device_firmware(upgrade=True) self.assertEqual(UpgradeOperation.objects.count(), 1) uo = device_fw.image.upgradeoperation_set.first() - self.assertEqual(uo.status, 'failed') - self.assertIn('Operation timed out.', uo.log) + self.assertEqual(uo.status, "failed") + self.assertIn("Operation timed out.", uo.log) @mock.patch(_mock_upgrade, return_value=True) @mock.patch( @@ -40,29 +40,29 @@ def test_upgrade_firmware_timeout(self, *args): def test_batch_upgrade_timeout(self, *args): with mock.patch(self._mock_connect, return_value=True): env = self._create_upgrade_env() - batch = BatchUpgradeOperation.objects.create(build=env['build2']) + batch = BatchUpgradeOperation.objects.create(build=env["build2"]) # will be executed synchronously due to CELERY_IS_EAGER = True tasks.batch_upgrade_operation.delay(batch_id=batch.pk, firmwareless=False) self.assertEqual(BatchUpgradeOperation.objects.count(), 1) batch = BatchUpgradeOperation.objects.first() - self.assertEqual(batch.status, 'failed') + self.assertEqual(batch.status, "failed") @mock.patch(_mock_upgrade, return_value=True) - @mock.patch('logging.Logger.warning') + @mock.patch("logging.Logger.warning") def test_upgrade_firmware_resilience(self, mocked_logger, *args): with mock.patch(self._mock_connect, return_value=True): upgrade_op_id = UpgradeOperation().id tasks.upgrade_firmware.run(upgrade_op_id) mocked_logger.assert_called_with( - f'The UpgradeOperation object with id {upgrade_op_id} has been deleted' + f"The UpgradeOperation object with id {upgrade_op_id} has been deleted" ) @mock.patch(_mock_upgrade, return_value=True) - @mock.patch('logging.Logger.warning') + @mock.patch("logging.Logger.warning") def test_batch_upgrade_operation_resilience(self, mocked_logger, *args): with mock.patch(self._mock_connect, return_value=True): batch_id = BatchUpgradeOperation().id tasks.batch_upgrade_operation.run(batch_id=batch_id, firmwareless=False) mocked_logger.assert_called_with( - f'The BatchUpgradeOperation object with id {batch_id} has been deleted' + f"The BatchUpgradeOperation object with id {batch_id} has been deleted" ) diff --git a/tests/openwisp2/sample_connection/tests.py b/tests/openwisp2/sample_connection/tests.py index ed18b250c..3d0f691d1 100644 --- a/tests/openwisp2/sample_connection/tests.py +++ b/tests/openwisp2/sample_connection/tests.py @@ -1,3 +1,7 @@ +from django.urls import reverse +from swapper import load_model + +from openwisp_controller.connection import settings as conn_settings from openwisp_controller.connection.tests.test_admin import ( TestCommandInlines as BaseTestCommandInlines, ) @@ -47,9 +51,6 @@ class TestTasks(BaseTestTasks): class TestSsh(BaseTestSsh): pass -import os -from django.urls import reverse -from swapper import load_model Notification = load_model("openwisp_notifications", "Notification") @@ -61,9 +62,7 @@ def _generic_notification_test( self, exp_level, exp_type, exp_verb, exp_message, exp_email_subject ): n = Notification.objects.first() - config_app = ( - "config" - ) + config_app = "config" device_url_path = reverse(f"admin:{config_app}_device_change", args=[self.d.id]) exp_target_link = f"https://example.com{device_url_path}" @@ -75,17 +74,15 @@ def _generic_notification_test( self.assertIn(exp_message.format(n=n, target_link=exp_target_link), n.message) self.assertEqual(n.email_subject, exp_email_subject.format(n=n)) + class TestNotificationTransaction(BaseTestNotificationTransaction): app_label = "sample_connection" - def _generic_notification_test( self, exp_level, exp_type, exp_verb, exp_message, exp_email_subject ): n = Notification.objects.first() - config_app = ( - "config" - ) + config_app = "config" device_url_path = reverse(f"admin:{config_app}_device_change", args=[self.d.id]) exp_target_link = f"https://example.com{device_url_path}" @@ -98,8 +95,6 @@ def _generic_notification_test( self.assertEqual(n.email_subject, exp_email_subject.format(n=n)) -from openwisp_controller.connection import settings as conn_settings - class TestConnectionApi(BaseTestConnectionApi): def test_post_deviceconnection_list(self): d1 = self._create_device() diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index 5d81c9e04..4c8385c74 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -7,8 +7,8 @@ from django.urls import include, path, reverse_lazy from django.views.generic import RedirectView -from openwisp_controller.config.api.urls import get_api_urls as get_config_api_urls -from openwisp_controller.config.utils import get_controller_urls +# from openwisp_controller.config.api.urls import get_api_urls as get_config_api_urls +# from openwisp_controller.config.utils import get_controller_urls from openwisp_controller.connection.api.urls import ( get_api_urls as get_connection_api_urls, ) From df6a7dc8baaf062299d7dff781ece9b178c7eccf Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Tue, 30 Dec 2025 15:11:06 +0100 Subject: [PATCH 5/8] [change] Remove comments and commented accounts/ in tests/openwisp/urls.py --- tests/openwisp2/urls.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index 4c8385c74..0c0350413 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -7,15 +7,11 @@ from django.urls import include, path, reverse_lazy from django.views.generic import RedirectView -# from openwisp_controller.config.api.urls import get_api_urls as get_config_api_urls -# from openwisp_controller.config.utils import get_controller_urls from openwisp_controller.connection.api.urls import ( get_api_urls as get_connection_api_urls, ) from openwisp_users.api.urls import get_api_urls -# from .sample_config import views as config_views -# from .sample_config.api import views as config_api_views from .sample_connection.api import views as connection_api_views redirect_view = RedirectView.as_view(url=reverse_lazy("admin:index")) @@ -24,24 +20,10 @@ if os.environ.get("SAMPLE_APP", False): urlpatterns += [ - # path( - # "controller/", - # include( - # (get_controller_urls(config_views), "controller"), - # namespace="controller", - # ), - # ), path( "", include(("openwisp_controller.config.urls", "config"), namespace="config"), ), - # path( - # "api/v1/", - # include( - # (get_config_api_urls(config_api_views), "config_api"), - # namespace="config_api", - # ), - # ), path( "api/v1/", include( @@ -58,7 +40,7 @@ path("admin/", admin.site.urls), path("", redirect_view, name="index"), path("", include("openwisp_controller.urls")), - path("accounts/", include("openwisp_users.accounts.urls")), # NEEDED OR NOT ? + # path("accounts/", include("openwisp_users.accounts.urls")), # Not needed in development path("", include("openwisp_firmware_upgrader.urls")), # token auth API path("api/v1/", include((get_api_urls(), "users"), namespace="users")), From 87d9f97c6204eb2bfb51a4007c233965432aa4e3 Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Tue, 30 Dec 2025 15:56:36 +0100 Subject: [PATCH 6/8] [change] Remove comments regarding config from tests settings --- tests/openwisp2/settings.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index 3e53108e7..b59ffc666 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -217,29 +217,13 @@ ) # For controller extended apps: - # Replace Config - # config_index = INSTALLED_APPS.index("openwisp_controller.config") - # INSTALLED_APPS.remove("openwisp_controller.config") - # INSTALLED_APPS.insert(config_index, "openwisp2.sample_config") # Replace Connection connection_index = INSTALLED_APPS.index("openwisp_controller.connection") INSTALLED_APPS.remove("openwisp_controller.connection") INSTALLED_APPS.insert(connection_index, "openwisp2.sample_connection") # Extended apps - # EXTENDED_APPS.append("openwisp_controller.config") EXTENDED_APPS.append("openwisp_controller.connection") # Swapper - # CONFIG_DEVICE_MODEL = "sample_config.Device" - # CONFIG_DEVICEGROUP_MODEL = "sample_config.DeviceGroup" - # CONFIG_CONFIG_MODEL = "sample_config.Config" - # CONFIG_TEMPLATETAG_MODEL = "sample_config.TemplateTag" - # CONFIG_TAGGEDTEMPLATE_MODEL = "sample_config.TaggedTemplate" - # CONFIG_TEMPLATE_MODEL = "sample_config.Template" - # CONFIG_VPN_MODEL = "sample_config.Vpn" - # CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient" - # CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings" - # CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits" - CONNECTION_CREDENTIALS_MODEL = "sample_connection.Credentials" CONNECTION_DEVICECONNECTION_MODEL = "sample_connection.DeviceConnection" CONNECTION_COMMAND_MODEL = "sample_connection.Command" From 0aaba038cf2ba93efd99f60fde9a6d292a684051 Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Mon, 12 Jan 2026 09:41:34 +0100 Subject: [PATCH 7/8] [change] Simplify sample_connection.tests.TestNotifications --- tests/openwisp2/sample_connection/tests.py | 34 ++-------------------- tests/openwisp2/urls.py | 1 - 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/tests/openwisp2/sample_connection/tests.py b/tests/openwisp2/sample_connection/tests.py index 3d0f691d1..946e207c5 100644 --- a/tests/openwisp2/sample_connection/tests.py +++ b/tests/openwisp2/sample_connection/tests.py @@ -57,42 +57,12 @@ class TestSsh(BaseTestSsh): class TestNotifications(BaseTestNotifications): app_label = "sample_connection" - - def _generic_notification_test( - self, exp_level, exp_type, exp_verb, exp_message, exp_email_subject - ): - n = Notification.objects.first() - config_app = "config" - device_url_path = reverse(f"admin:{config_app}_device_change", args=[self.d.id]) - exp_target_link = f"https://example.com{device_url_path}" - - self.assertEqual(n.type, exp_type) - self.assertEqual(n.level, exp_level) - self.assertEqual(n.verb, exp_verb) - self.assertEqual(n.actor, self.d.deviceconnection_set.first()) - self.assertEqual(n.target, self.d) - self.assertIn(exp_message.format(n=n, target_link=exp_target_link), n.message) - self.assertEqual(n.email_subject, exp_email_subject.format(n=n)) + config_app_label = "config" class TestNotificationTransaction(BaseTestNotificationTransaction): app_label = "sample_connection" - - def _generic_notification_test( - self, exp_level, exp_type, exp_verb, exp_message, exp_email_subject - ): - n = Notification.objects.first() - config_app = "config" - device_url_path = reverse(f"admin:{config_app}_device_change", args=[self.d.id]) - exp_target_link = f"https://example.com{device_url_path}" - - self.assertEqual(n.type, exp_type) - self.assertEqual(n.level, exp_level) - self.assertEqual(n.verb, exp_verb) - self.assertEqual(n.actor, self.d.deviceconnection_set.first()) - self.assertEqual(n.target, self.d) - self.assertIn(exp_message.format(n=n, target_link=exp_target_link), n.message) - self.assertEqual(n.email_subject, exp_email_subject.format(n=n)) + config_app_label = "config" class TestConnectionApi(BaseTestConnectionApi): diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index 0c0350413..534d7fbe0 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -40,7 +40,6 @@ path("admin/", admin.site.urls), path("", redirect_view, name="index"), path("", include("openwisp_controller.urls")), - # path("accounts/", include("openwisp_users.accounts.urls")), # Not needed in development path("", include("openwisp_firmware_upgrader.urls")), # token auth API path("api/v1/", include((get_api_urls(), "users"), namespace="users")), From 4bb164cfdd59b6efa98ee7d9109df9eac0764647 Mon Sep 17 00:00:00 2001 From: Alexandre Vincent Date: Wed, 4 Mar 2026 17:05:26 +0100 Subject: [PATCH 8/8] [change] Remove unused connection_app_label in test_admin.BaseTestAdmin --- openwisp_firmware_upgrader/tests/test_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openwisp_firmware_upgrader/tests/test_admin.py b/openwisp_firmware_upgrader/tests/test_admin.py index 6ae536d6a..ee6cb98d7 100644 --- a/openwisp_firmware_upgrader/tests/test_admin.py +++ b/openwisp_firmware_upgrader/tests/test_admin.py @@ -52,7 +52,6 @@ class MockRequest: class BaseTestAdmin(TestMultitenantAdminMixin, TestUpgraderMixin): app_label = Build._meta.app_label config_app_label = Device._meta.app_label - connection_app_label = "connection" _device_params = TestConfigAdmin._device_params.copy() _device_params.update( {