diff --git a/openwisp_firmware_upgrader/tests/test_admin.py b/openwisp_firmware_upgrader/tests/test_admin.py
index 1ce34c8ca..79e551a04 100644
--- a/openwisp_firmware_upgrader/tests/test_admin.py
+++ b/openwisp_firmware_upgrader/tests/test_admin.py
@@ -652,438 +652,373 @@ def test_batch_upgrade_operation_filters(self, *args):
)
-_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
):
- 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}"
- )
- )
- 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."
- ),
- required_perms=["change"],
- extra_payload={
- "upgrade_all": "upgrade_all",
- "upgrade_options": '{"c": true}',
- },
- )
+ _mock_upgrade = "openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade"
+ _mock_connect = "openwisp_controller.connection.models.DeviceConnection.connect"
- 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,),
- },
- follow=True,
- )
- id_attr = (
- ' id="id_upgrade_options_error"' if django.VERSION >= (5, 2) else ""
- )
- self.assertContains(
- response,
- f'
',
+ @mock.patch(_mock_upgrade, return_value=True)
+ def test_upgrade_selected_action_perms(self, *args):
+ 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}"
+ )
)
-
- with self.subTest("Test with valid upgrade_options"):
- r = self.client.post(
- self.build_list_url,
- {
- "action": "upgrade_selected",
- "upgrade_related": "upgrade_related",
+ 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."
+ ),
+ required_perms=["change"],
+ extra_payload={
+ "upgrade_all": "upgrade_all",
"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_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):
- 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 ""
+ 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,
+ (
+ ''
+ '
'
+ "Attempt to preserve all changed files in /etc/ (-c) "
+ '
'
+ "Attempt to preserve all changed files in /, except those from "
+ "packages but including changed confs. (-o) "
+ '
'
+ "Do not save configuration over reflash (-n) "
+ '
'
+ "Skip from backup files that are equal to those in /rom (-u) "
+ '
'
+ "Do not attempt to restore the partition table after flash. (-p) "
+ '
'
+ "Include in backup a list of current installed packages at "
+ "/etc/backup/installed_packages.txt (-k) "
+ '
'
+ "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"):
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,),
+ 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(), 3
- )
- self.assertEqual(fw.count(), 3)
- self.assertContains(
- response,
- (
- ''
- '
'
- "Attempt to preserve all changed files in /etc/ (-c) "
- '
'
- "Attempt to preserve all changed files in /, except those from "
- "packages but including changed confs. (-o) "
- '
'
- "Do not save configuration over reflash (-n) "
- '
'
- "Skip from backup files that are equal to those in /rom (-u) "
- '
'
- "Do not attempt to restore the partition table after flash. (-p) "
- '
'
- "Include in backup a list of current installed packages at "
- "/etc/backup/installed_packages.txt (-k) "
- '
'
- "Flash image even if image checks fail, this is dangerous! (-F)
"
- ),
- html=True,
+ UpgradeOperation.objects.filter(upgrade_options={"c": True}).count(), 2
)
+ self.assertEqual(fw.count(), 2)
- 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)
-
+ @mock.patch(_mock_upgrade, return_value=True)
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")
+ 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],
+ )
+ 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_upgrade_operation_change_breadcrumb_with_batch(self, *args):
- self.test_upgrade_all()
- uo = UpgradeOperation.objects.first()
- url = reverse(f"admin:{self.app_label}_upgradeoperation_change", args=[uo.pk])
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- batch_changelist_url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_changelist"
- )
- batch_change_url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_change", args=[uo.batch.pk]
- )
- self.assertTrue(response.context["batch_has_view_permission"])
- self.assertEqual(response.context["batch"], uo.batch)
- self.assertContains(response, batch_changelist_url)
- self.assertContains(response, batch_change_url)
- self.assertContains(response, str(uo.batch))
- generic_upgrade_changelist_url = reverse(
- f"admin:{self.app_label}_upgradeoperation_changelist"
- )
- self.assertNotContains(response, f'href="{generic_upgrade_changelist_url}"')
+ with mock.patch(self._mock_connect, return_value=True):
+ self.test_upgrade_all()
+ uo = UpgradeOperation.objects.first()
+ url = reverse(f"admin:{self.app_label}_upgradeoperation_change", args=[uo.pk])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ batch_changelist_url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_changelist"
+ )
+ batch_change_url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_change", args=[uo.batch.pk]
+ )
+ self.assertTrue(response.context["batch_has_view_permission"])
+ self.assertEqual(response.context["batch"], uo.batch)
+ self.assertContains(response, batch_changelist_url)
+ self.assertContains(response, batch_change_url)
+ self.assertContains(response, str(uo.batch))
+ generic_upgrade_changelist_url = reverse(
+ f"admin:{self.app_label}_upgradeoperation_changelist"
+ )
+ self.assertNotContains(response, f'href="{generic_upgrade_changelist_url}"')
+ @mock.patch(_mock_upgrade, return_value=True)
def test_upgrade_operation_change_breadcrumb_without_batch(self, *args):
- self._login()
- device_fw = self._create_device_firmware()
- device_fw.save(upgrade=True)
- uo = device_fw.device.upgradeoperation_set.first()
- self.assertIsNone(uo.batch_id)
- url = reverse(f"admin:{self.app_label}_upgradeoperation_change", args=[uo.pk])
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- self.assertIsNone(response.context.get("batch"))
- generic_upgrade_changelist_url = reverse(
- f"admin:{self.app_label}_upgradeoperation_changelist"
- )
- self.assertContains(response, f'href="{generic_upgrade_changelist_url}"')
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ device_fw = self._create_device_firmware()
+ device_fw.save(upgrade=True)
+ uo = device_fw.device.upgradeoperation_set.first()
+ self.assertIsNone(uo.batch_id)
+ url = reverse(f"admin:{self.app_label}_upgradeoperation_change", args=[uo.pk])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(response.context.get("batch"))
+ generic_upgrade_changelist_url = reverse(
+ f"admin:{self.app_label}_upgradeoperation_changelist"
+ )
+ self.assertContains(response, f'href="{generic_upgrade_changelist_url}"')
+ @mock.patch(_mock_upgrade, return_value=True)
def test_upgrade_operation_change_breadcrumb_with_batch_no_permission(self, *args):
- self.test_upgrade_all()
- uo = UpgradeOperation.objects.first()
- url = reverse(f"admin:{self.app_label}_upgradeoperation_change", args=[uo.pk])
- with mock.patch(
- "openwisp_firmware_upgrader.admin.BatchUpgradeOperationAdmin.has_view_permission",
- return_value=False,
- ):
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- batch_changelist_url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_changelist"
- )
- batch_change_url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_change", args=[uo.batch.pk]
- )
- self.assertFalse(response.context["batch_has_view_permission"])
- self.assertEqual(response.context["batch"], uo.batch)
- breadcrumbs = (
- response.content.decode()
- .split('', 1)[1]
- .split("
", 1)[0]
- )
- self.assertNotIn(f'href="{batch_changelist_url}"', breadcrumbs)
- self.assertNotIn(f'href="{batch_change_url}"', breadcrumbs)
- generic_upgrade_changelist_url = reverse(
- f"admin:{self.app_label}_upgradeoperation_changelist"
- )
- self.assertNotIn(f'href="{generic_upgrade_changelist_url}"', breadcrumbs)
- self.assertIn(str(uo.batch), breadcrumbs)
+ with mock.patch(self._mock_connect, return_value=True):
+ self.test_upgrade_all()
+ uo = UpgradeOperation.objects.first()
+ url = reverse(f"admin:{self.app_label}_upgradeoperation_change", args=[uo.pk])
+ with mock.patch(
+ "openwisp_firmware_upgrader.admin.BatchUpgradeOperationAdmin.has_view_permission",
+ return_value=False,
+ ):
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ batch_changelist_url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_changelist"
+ )
+ batch_change_url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_change", args=[uo.batch.pk]
+ )
+ self.assertFalse(response.context["batch_has_view_permission"])
+ self.assertEqual(response.context["batch"], uo.batch)
+ breadcrumbs = (
+ response.content.decode()
+ .split('', 1)[1]
+ .split("
", 1)[0]
+ )
+ self.assertNotIn(f'href="{batch_changelist_url}"', breadcrumbs)
+ self.assertNotIn(f'href="{batch_change_url}"', breadcrumbs)
+ generic_upgrade_changelist_url = reverse(
+ f"admin:{self.app_label}_upgradeoperation_changelist"
+ )
+ self.assertNotIn(f'href="{generic_upgrade_changelist_url}"', breadcrumbs)
+ self.assertIn(str(uo.batch), breadcrumbs)
+ @mock.patch(_mock_upgrade, return_value=True)
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]
- )
- 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")
+ 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]
+ )
+ 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):
- 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 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),
+ )
+ @mock.patch(_mock_upgrade, return_value=True)
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)
-
+ 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):
- 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(f"admin:{self.config_app_label}_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,
- (
- '- '
- '
'
- "Attempt to preserve all changed files in /etc/ (-c) "
- '
'
- "Attempt to preserve all changed files in /, except those from packages "
- "but including changed confs. (-o) "
- '
Do not save configuration over reflash (-n) '
- '
Skip from backup files '
- "that are equal to those in /rom (-u) "
- '
'
- "Do not attempt to restore the partition table after flash. (-p) "
- '
'
- "Include in backup a list of current installed packages at "
- "/etc/backup/installed_packages.txt (-k) "
- '
'
- "Flash image even if image checks fail, this is dangerous! (-F)
"
- ),
- html=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_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,
}
- )
-
- 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]
- )
+ device_params = self._get_device_params(
+ device, device_conn, image, device_fw, json.dumps(upgrade_options)
)
- self.assertContains(
- response, ""
+ )
+
+ 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.
"
+ ),
+ )
+
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_operation_status_filter(self, *args):
"""Test status filtering in batch upgrade operation admin page"""
- self._login()
- env = self._create_upgrade_env()
- env["category"].organization = None
- env["category"].save()
- batch = env["build2"].batch_upgrade(firmwareless=True)
- # Create upgrade operations with different statuses
- upgrade_ops = list(batch.upgradeoperation_set.all())
- if len(upgrade_ops) >= 2:
- upgrade_ops[0].status = "success"
- upgrade_ops[0].save()
- upgrade_ops[1].status = "failed"
- upgrade_ops[1].save()
- url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
- )
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ env = self._create_upgrade_env()
+ env["category"].organization = None
+ env["category"].save()
+ batch = env["build2"].batch_upgrade(firmwareless=True)
+ # Create upgrade operations with different statuses
+ upgrade_ops = list(batch.upgradeoperation_set.all())
+ if len(upgrade_ops) >= 2:
+ upgrade_ops[0].status = "success"
+ upgrade_ops[0].save()
+ upgrade_ops[1].status = "failed"
+ upgrade_ops[1].save()
+ url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
+ )
- with self.subTest("Test no filter - shows all operations"):
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, "ow-filter status")
- self.assertContains(response, "By status")
- self.assertContains(response, "By organization")
+ with self.subTest("Test no filter - shows all operations"):
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "ow-filter status")
+ self.assertContains(response, "By status")
+ self.assertContains(response, "By organization")
- with self.subTest("Test status success filter"):
- response = self.client.get(url + "?status=success")
- self.assertEqual(response.status_code, 200)
- success_ops = batch.upgradeoperation_set.filter(status="success")
- for op in success_ops:
- self.assertContains(response, op.device.name)
+ with self.subTest("Test status success filter"):
+ response = self.client.get(url + "?status=success")
+ self.assertEqual(response.status_code, 200)
+ success_ops = batch.upgradeoperation_set.filter(status="success")
+ for op in success_ops:
+ self.assertContains(response, op.device.name)
- with self.subTest("Test status failed filter"):
- response = self.client.get(url + "?status=failed")
- self.assertEqual(response.status_code, 200)
- failed_ops = batch.upgradeoperation_set.filter(status="failed")
- for op in failed_ops:
- self.assertContains(response, op.device.name)
+ with self.subTest("Test status failed filter"):
+ response = self.client.get(url + "?status=failed")
+ self.assertEqual(response.status_code, 200)
+ failed_ops = batch.upgradeoperation_set.filter(status="failed")
+ for op in failed_ops:
+ self.assertContains(response, op.device.name)
- with self.subTest("Test status idle filter"):
- response = self.client.get(url + "?status=idle")
- self.assertEqual(response.status_code, 200)
- idle_ops = batch.upgradeoperation_set.filter(status="idle")
- for op in idle_ops:
- self.assertContains(response, op.device.name)
+ with self.subTest("Test status idle filter"):
+ response = self.client.get(url + "?status=idle")
+ self.assertEqual(response.status_code, 200)
+ idle_ops = batch.upgradeoperation_set.filter(status="idle")
+ for op in idle_ops:
+ self.assertContains(response, op.device.name)
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_operation_organization_filter(self, *args):
"""Test organization filtering in batch upgrade operation admin page"""
- self._login()
- # Create devices from different organizations
- org1 = self._create_org(name="Org1", slug="org1")
- org2 = self._create_org(name="Org2", slug="org2")
- device1 = self._create_device(organization=org1, name="device1-org-filter")
- device2 = self._create_device(organization=org2, name="device2-org-filter")
- self._create_config(device=device1)
- self._create_config(device=device2)
- cred1 = self._get_credentials(organization=org1)
- cred2 = self._get_credentials(organization=org2)
- self._create_device_connection(device=device1, credentials=cred1)
- self._create_device_connection(device=device2, credentials=cred2)
- shared_category = self._create_category(
- organization=None, name="Shared Category"
- )
- build = self._create_build(category=shared_category)
- image = self._create_firmware_image(build=build)
- self._create_device_firmware(
- device=device1, image=image, device_connection=False
- )
- self._create_device_firmware(
- device=device2, image=image, device_connection=False
- )
- batch = build.batch_upgrade(firmwareless=False)
- url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
- )
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ # Create devices from different organizations
+ org1 = self._create_org(name="Org1", slug="org1")
+ org2 = self._create_org(name="Org2", slug="org2")
+ device1 = self._create_device(organization=org1, name="device1-org-filter")
+ device2 = self._create_device(organization=org2, name="device2-org-filter")
+ self._create_config(device=device1)
+ self._create_config(device=device2)
+ cred1 = self._get_credentials(organization=org1)
+ cred2 = self._get_credentials(organization=org2)
+ self._create_device_connection(device=device1, credentials=cred1)
+ self._create_device_connection(device=device2, credentials=cred2)
+ shared_category = self._create_category(
+ organization=None, name="Shared Category"
+ )
+ build = self._create_build(category=shared_category)
+ image = self._create_firmware_image(build=build)
+ self._create_device_firmware(
+ device=device1, image=image, device_connection=False
+ )
+ self._create_device_firmware(
+ device=device2, image=image, device_connection=False
+ )
+ batch = build.batch_upgrade(firmwareless=False)
+ url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
+ )
- with self.subTest("Test no organization filter - shows all operations"):
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, device1.name)
- self.assertContains(response, device2.name)
- self.assertContains(response, "By organization")
+ with self.subTest("Test no organization filter - shows all operations"):
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, device1.name)
+ self.assertContains(response, device2.name)
+ self.assertContains(response, "By organization")
- with self.subTest("Test organization filter for org1"):
- response = self.client.get(url + f"?organization={org1.id}")
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, device1.name)
- self.assertNotContains(response, device2.name)
+ with self.subTest("Test organization filter for org1"):
+ response = self.client.get(url + f"?organization={org1.id}")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, device1.name)
+ self.assertNotContains(response, device2.name)
- with self.subTest("Test organization filter for org2"):
- response = self.client.get(url + f"?organization={org2.id}")
- self.assertEqual(response.status_code, 200)
- self.assertNotContains(response, device1.name)
- self.assertContains(response, device2.name)
+ with self.subTest("Test organization filter for org2"):
+ response = self.client.get(url + f"?organization={org2.id}")
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, device1.name)
+ self.assertContains(response, device2.name)
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_operation_combined_filters(self, *args):
"""Test combining status and organization filters"""
- self._login()
- # Create devices from different organizations
- org1 = self._create_org(name="Org1", slug="org1")
- org2 = self._create_org(name="Org2", slug="org2")
- device1 = self._create_device(organization=org1, name="device1-combined-filter")
- device2 = self._create_device(organization=org2, name="device2-combined-filter")
- self._create_config(device=device1)
- self._create_config(device=device2)
- cred1 = self._get_credentials(organization=org1)
- cred2 = self._get_credentials(organization=org2)
- self._create_device_connection(device=device1, credentials=cred1)
- self._create_device_connection(device=device2, credentials=cred2)
-
- # Create shared build and batch upgrade that works with any organization
- shared_category = self._create_category(
- organization=None, name="Shared Category"
- )
- build = self._create_build(category=shared_category)
- image = self._create_firmware_image(build=build)
- self._create_device_firmware(
- device=device1, image=image, device_connection=False
- )
- self._create_device_firmware(
- device=device2, image=image, device_connection=False
- )
- batch = build.batch_upgrade(firmwareless=False)
- # Set different statuses for devices from different orgs
- upgrade_ops = list(batch.upgradeoperation_set.all())
- org1_op = (
- upgrade_ops[0]
- if upgrade_ops[0].device.organization == org1
- else upgrade_ops[1]
- )
- org2_op = (
- upgrade_ops[1]
- if upgrade_ops[1].device.organization == org2
- else upgrade_ops[0]
- )
- org1_op.status = "success"
- org1_op.save()
- org2_op.status = "failed"
- org2_op.save()
- url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
- )
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ # Create devices from different organizations
+ org1 = self._create_org(name="Org1", slug="org1")
+ org2 = self._create_org(name="Org2", slug="org2")
+ device1 = self._create_device(organization=org1, name="device1-combined-filter")
+ device2 = self._create_device(organization=org2, name="device2-combined-filter")
+ self._create_config(device=device1)
+ self._create_config(device=device2)
+ cred1 = self._get_credentials(organization=org1)
+ cred2 = self._get_credentials(organization=org2)
+ self._create_device_connection(device=device1, credentials=cred1)
+ self._create_device_connection(device=device2, credentials=cred2)
- with self.subTest("Test combined filter: org1 + success"):
- response = self.client.get(url + f"?organization={org1.id}&status=success")
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, org1_op.device.name)
- self.assertNotContains(response, org2_op.device.name)
+ # Create shared build and batch upgrade that works with any organization
+ shared_category = self._create_category(
+ organization=None, name="Shared Category"
+ )
+ build = self._create_build(category=shared_category)
+ image = self._create_firmware_image(build=build)
+ self._create_device_firmware(
+ device=device1, image=image, device_connection=False
+ )
+ self._create_device_firmware(
+ device=device2, image=image, device_connection=False
+ )
+ batch = build.batch_upgrade(firmwareless=False)
+ # Set different statuses for devices from different orgs
+ upgrade_ops = list(batch.upgradeoperation_set.all())
+ org1_op = (
+ upgrade_ops[0]
+ if upgrade_ops[0].device.organization == org1
+ else upgrade_ops[1]
+ )
+ org2_op = (
+ upgrade_ops[1]
+ if upgrade_ops[1].device.organization == org2
+ else upgrade_ops[0]
+ )
+ org1_op.status = "success"
+ org1_op.save()
+ org2_op.status = "failed"
+ org2_op.save()
+ url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
+ )
- with self.subTest("Test combined filter: org2 + failed"):
- response = self.client.get(url + f"?organization={org2.id}&status=failed")
- self.assertEqual(response.status_code, 200)
- self.assertNotContains(response, org1_op.device.name)
- self.assertContains(response, org2_op.device.name)
+ with self.subTest("Test combined filter: org1 + success"):
+ response = self.client.get(url + f"?organization={org1.id}&status=success")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, org1_op.device.name)
+ self.assertNotContains(response, org2_op.device.name)
- with self.subTest("Test combined filter: org1 + failed (no results)"):
- response = self.client.get(url + f"?organization={org1.id}&status=failed")
- self.assertEqual(response.status_code, 200)
- self.assertNotContains(response, org1_op.device.name)
- self.assertNotContains(response, org2_op.device.name)
+ with self.subTest("Test combined filter: org2 + failed"):
+ response = self.client.get(url + f"?organization={org2.id}&status=failed")
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, org1_op.device.name)
+ self.assertContains(response, org2_op.device.name)
- with self.subTest("Combined filters preserve each other in generated links"):
- response = self.client.get(url + f"?organization={org1.id}&status=success")
- # Organization 'All' should keep status
- self.assertContains(
- response,
- 'All',
- html=True,
- )
- # Status 'All' should keep organization
- self.assertContains(
- response,
- f'All',
- html=True,
- )
+ with self.subTest("Test combined filter: org1 + failed (no results)"):
+ response = self.client.get(url + f"?organization={org1.id}&status=failed")
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, org1_op.device.name)
+ self.assertNotContains(response, org2_op.device.name)
+
+ with self.subTest("Combined filters preserve each other in generated links"):
+ response = self.client.get(url + f"?organization={org1.id}&status=success")
+ # Organization 'All' should keep status
+ self.assertContains(
+ response,
+ 'All',
+ html=True,
+ )
+ # Status 'All' should keep organization
+ self.assertContains(
+ response,
+ f'All',
+ html=True,
+ )
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_operation_filter_search_combination(self, *args):
"""Test combining search with filters"""
- self._login()
- env = self._create_upgrade_env()
- batch = env["build2"].batch_upgrade(firmwareless=True)
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ env = self._create_upgrade_env()
+ batch = env["build2"].batch_upgrade(firmwareless=True)
- upgrade_op = batch.upgradeoperation_set.first()
- upgrade_op.device.name = "unique-test-device"
- upgrade_op.device.save()
- upgrade_op.status = "success"
- upgrade_op.save()
+ upgrade_op = batch.upgradeoperation_set.first()
+ upgrade_op.device.name = "unique-test-device"
+ upgrade_op.device.save()
+ upgrade_op.status = "success"
+ upgrade_op.save()
- url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
- )
- with self.subTest("Test search + status filter"):
- with self.assertNumQueries(25 if django.VERSION < (5, 2) else 23):
- response = self.client.get(url + "?q=unique-test&status=success")
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, "unique-test-device")
- self.assertContains(
- response,
- 'All',
- html=True,
+ url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
)
+ with self.subTest("Test search + status filter"):
+ with self.assertNumQueries(25 if django.VERSION < (5, 2) else 23):
+ response = self.client.get(url + "?q=unique-test&status=success")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "unique-test-device")
+ self.assertContains(
+ response,
+ 'All',
+ html=True,
+ )
- with self.subTest("Test search + status filter (no match)"):
- response = self.client.get(url + "?q=unique-test&status=failed")
- self.assertEqual(response.status_code, 200)
- self.assertNotContains(response, "unique-test-device")
+ with self.subTest("Test search + status filter (no match)"):
+ response = self.client.get(url + "?q=unique-test&status=failed")
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, "unique-test-device")
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_confirmation_form_multitenancy(self, *args):
"""Test BatchUpgradeConfirmationForm multitenancy for organization admin vs superuser."""
- # Setup common objects
- org1 = self._get_org()
- org2 = self._create_org(name="Org 2", slug="org2")
- group1 = self._create_device_group(name="Group Org1", organization=org1)
- group2 = self._create_device_group(name="Group Org2", organization=org2)
- location1 = Location.objects.create(
- name="Location Org1", address="123 Test St", organization=org1
- )
- location2 = Location.objects.create(
- name="Location Org2", address="456 Test St", organization=org2
- )
- category_org1 = self._create_category(organization=org1)
- build_org1 = self._create_build(category=category_org1)
- category_shared = self._create_category(organization=None)
- build_shared = self._create_build(category=category_shared)
- category_org2 = self._create_category(organization=org2)
- build_org2 = self._create_build(category=category_org2)
- superuser = self._get_admin()
- org_admin = self._create_administrator(organizations=[org1])
-
- with self.subTest("Superuser: Org build should shown related org objects"):
- form = BatchUpgradeConfirmationForm(
- initial={"build": build_org1}, user=superuser
- )
- self.assertIn(group1, form.fields["group"].queryset)
- self.assertNotIn(group2, form.fields["group"].queryset)
- self.assertIn(location1, form.fields["location"].queryset)
- self.assertNotIn(location2, form.fields["location"].queryset)
-
- with self.subTest("Superuser: Shared build should show all org objects"):
- form = BatchUpgradeConfirmationForm(
- initial={"build": build_shared}, user=superuser
- )
- self.assertIn(group1, form.fields["group"].queryset)
- self.assertIn(group2, form.fields["group"].queryset)
- self.assertIn(location1, form.fields["location"].queryset)
- self.assertIn(location2, form.fields["location"].queryset)
-
- with self.subTest(
- "Org admin: Shared build should show only managed org objects"
- ):
- form = BatchUpgradeConfirmationForm(
- initial={"build": build_shared}, user=org_admin
- )
- self.assertIn(group1, form.fields["group"].queryset)
- self.assertNotIn(group2, form.fields["group"].queryset)
- self.assertIn(location1, form.fields["location"].queryset)
- self.assertNotIn(location2, form.fields["location"].queryset)
-
- with self.subTest("Org admin: Org build should show only that org objects"):
- form = BatchUpgradeConfirmationForm(
- initial={"build": build_org1}, user=org_admin
+ with mock.patch(self._mock_connect, return_value=True):
+ # Setup common objects
+ org1 = self._get_org()
+ org2 = self._create_org(name="Org 2", slug="org2")
+ group1 = self._create_device_group(name="Group Org1", organization=org1)
+ group2 = self._create_device_group(name="Group Org2", organization=org2)
+ location1 = Location.objects.create(
+ name="Location Org1", address="123 Test St", organization=org1
)
- self.assertIn(group1, form.fields["group"].queryset)
- self.assertNotIn(group2, form.fields["group"].queryset)
- self.assertIn(location1, form.fields["location"].queryset)
- self.assertNotIn(location2, form.fields["location"].queryset)
-
- with self.subTest("Org admin: Different org build should show no objects"):
- form = BatchUpgradeConfirmationForm(
- initial={"build": build_org2}, user=org_admin
+ location2 = Location.objects.create(
+ name="Location Org2", address="456 Test St", organization=org2
)
- self.assertEqual(form.fields["group"].queryset.count(), 0)
- self.assertEqual(form.fields["location"].queryset.count(), 0)
+ category_org1 = self._create_category(organization=org1)
+ build_org1 = self._create_build(category=category_org1)
+ category_shared = self._create_category(organization=None)
+ build_shared = self._create_build(category=category_shared)
+ category_org2 = self._create_category(organization=org2)
+ build_org2 = self._create_build(category=category_org2)
+ superuser = self._get_admin()
+ org_admin = self._create_administrator(organizations=[org1])
+
+ with self.subTest("Superuser: Org build should shown related org objects"):
+ form = BatchUpgradeConfirmationForm(
+ initial={"build": build_org1}, user=superuser
+ )
+ self.assertIn(group1, form.fields["group"].queryset)
+ self.assertNotIn(group2, form.fields["group"].queryset)
+ self.assertIn(location1, form.fields["location"].queryset)
+ self.assertNotIn(location2, form.fields["location"].queryset)
+
+ with self.subTest("Superuser: Shared build should show all org objects"):
+ form = BatchUpgradeConfirmationForm(
+ initial={"build": build_shared}, user=superuser
+ )
+ self.assertIn(group1, form.fields["group"].queryset)
+ self.assertIn(group2, form.fields["group"].queryset)
+ self.assertIn(location1, form.fields["location"].queryset)
+ self.assertIn(location2, form.fields["location"].queryset)
+
+ with self.subTest(
+ "Org admin: Shared build should show only managed org objects"
+ ):
+ form = BatchUpgradeConfirmationForm(
+ initial={"build": build_shared}, user=org_admin
+ )
+ self.assertIn(group1, form.fields["group"].queryset)
+ self.assertNotIn(group2, form.fields["group"].queryset)
+ self.assertIn(location1, form.fields["location"].queryset)
+ self.assertNotIn(location2, form.fields["location"].queryset)
+
+ with self.subTest("Org admin: Org build should show only that org objects"):
+ form = BatchUpgradeConfirmationForm(
+ initial={"build": build_org1}, user=org_admin
+ )
+ self.assertIn(group1, form.fields["group"].queryset)
+ self.assertNotIn(group2, form.fields["group"].queryset)
+ self.assertIn(location1, form.fields["location"].queryset)
+ self.assertNotIn(location2, form.fields["location"].queryset)
+
+ with self.subTest("Org admin: Different org build should show no objects"):
+ form = BatchUpgradeConfirmationForm(
+ initial={"build": build_org2}, user=org_admin
+ )
+ self.assertEqual(form.fields["group"].queryset.count(), 0)
+ self.assertEqual(form.fields["location"].queryset.count(), 0)
- with self.subTest("Location field exists and is not required"):
- form = BatchUpgradeConfirmationForm(
- initial={"build": build_org1}, user=superuser
- )
- self.assertIn("location", form.fields)
- self.assertFalse(form.fields["location"].required)
- self.assertIn("location", form.fields["location"].help_text)
+ with self.subTest("Location field exists and is not required"):
+ form = BatchUpgradeConfirmationForm(
+ initial={"build": build_org1}, user=superuser
+ )
+ self.assertIn("location", form.fields)
+ self.assertFalse(form.fields["location"].required)
+ self.assertIn("location", form.fields["location"].help_text)
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_with_location_admin_action(self, *args):
"""Test mass upgrade admin action with location filtering."""
- self._login()
- org = self._get_org()
- category = self._create_category(organization=org)
- build = self._create_build(category=category)
- image = self._create_firmware_image(build=build)
- # Create location
- location = Location.objects.create(
- name="Test Location", address="123 Test St", organization=org
- )
- # Create devices
- device1 = self._create_device(
- name="Device1-WithLocation",
- organization=org,
- model=image.boards[0],
- mac_address="00:11:22:33:55:71",
- )
- device2 = self._create_device(
- name="Device2-NoLocation",
- organization=org,
- model=image.boards[0],
- mac_address="00:11:22:33:55:72",
- )
- # Set location for device1 only
- DeviceLocation.objects.create(content_object=device1, location=location)
- # Create configs and connections
- self._create_config(device=device1)
- self._create_config(device=device2)
- cred1 = self._get_credentials(organization=org)
- if not DeviceConnection.objects.filter(
- device=device1, credentials=cred1
- ).exists():
- self._create_device_connection(device=device1, credentials=cred1)
- if not DeviceConnection.objects.filter(
- device=device2, credentials=cred1
- ).exists():
- self._create_device_connection(device=device2, credentials=cred1)
- url = reverse(f"admin:{self.app_label}_build_changelist")
- data = {
- ACTION_CHECKBOX_NAME: [build.pk],
- "action": "upgrade_selected",
- "location": location.pk,
- "upgrade_related": "on",
- }
- with self.subTest("Test upgrade confirmation page with location"):
- response = self.client.post(url, data, follow=True)
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, location.name)
- # Submit the actual upgrade with location filter
- data.update(
- {
- "upgrade_all": "on",
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ org = self._get_org()
+ category = self._create_category(organization=org)
+ build = self._create_build(category=category)
+ image = self._create_firmware_image(build=build)
+ # Create location
+ location = Location.objects.create(
+ name="Test Location", address="123 Test St", organization=org
+ )
+ # Create devices
+ device1 = self._create_device(
+ name="Device1-WithLocation",
+ organization=org,
+ model=image.boards[0],
+ mac_address="00:11:22:33:55:71",
+ )
+ device2 = self._create_device(
+ name="Device2-NoLocation",
+ organization=org,
+ model=image.boards[0],
+ mac_address="00:11:22:33:55:72",
+ )
+ # Set location for device1 only
+ DeviceLocation.objects.create(content_object=device1, location=location)
+ # Create configs and connections
+ self._create_config(device=device1)
+ self._create_config(device=device2)
+ cred1 = self._get_credentials(organization=org)
+ if not DeviceConnection.objects.filter(
+ device=device1, credentials=cred1
+ ).exists():
+ self._create_device_connection(device=device1, credentials=cred1)
+ if not DeviceConnection.objects.filter(
+ device=device2, credentials=cred1
+ ).exists():
+ self._create_device_connection(device=device2, credentials=cred1)
+ url = reverse(f"admin:{self.app_label}_build_changelist")
+ data = {
+ ACTION_CHECKBOX_NAME: [build.pk],
+ "action": "upgrade_selected",
"location": location.pk,
+ "upgrade_related": "on",
}
- )
- with self.subTest("Test actual batch upgrade with location"):
- with mock.patch("openwisp_firmware_upgrader.tasks.upgrade_firmware.delay"):
+ with self.subTest("Test upgrade confirmation page with location"):
response = self.client.post(url, data, follow=True)
self.assertEqual(response.status_code, 200)
- # Check that batch was created with location
- batch = BatchUpgradeOperation.objects.first()
- self.assertIsNotNone(batch)
- self.assertEqual(batch.location, location)
-
+ self.assertContains(response, location.name)
+ # Submit the actual upgrade with location filter
+ data.update(
+ {
+ "upgrade_all": "on",
+ "location": location.pk,
+ }
+ )
+ with self.subTest("Test actual batch upgrade with location"):
+ with mock.patch("openwisp_firmware_upgrader.tasks.upgrade_firmware.delay"):
+ response = self.client.post(url, data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ # Check that batch was created with location
+ batch = BatchUpgradeOperation.objects.first()
+ self.assertIsNotNone(batch)
+ self.assertEqual(batch.location, location)
+
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_operation_admin_location_field(self, *args):
"""Test location field in BatchUpgradeOperationAdmin."""
- self._login()
- org = self._get_org()
- category = self._create_category(organization=org)
- build = self._create_build(category=category)
- location = Location.objects.create(
- name="Test Location", address="123 Test St", organization=org
- )
- batch = BatchUpgradeOperation.objects.create(build=build, location=location)
- url = reverse(
- f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
- )
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, location.name)
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ org = self._get_org()
+ category = self._create_category(organization=org)
+ build = self._create_build(category=category)
+ location = Location.objects.create(
+ name="Test Location", address="123 Test St", organization=org
+ )
+ batch = BatchUpgradeOperation.objects.create(build=build, location=location)
+ url = reverse(
+ f"admin:{self.app_label}_batchupgradeoperation_change", args=[batch.pk]
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, location.name)
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_no_devices_error_handling(self, *args):
"""Test admin error handling when filters don't match any devices."""
- self._login()
- org = self._get_org()
- category = self._create_category(organization=org)
- build = self._create_build(category=category, version="error-test")
- # Create location and group but no devices matching both
- location = Location.objects.create(
- name="Empty Location", address="456 Empty St", organization=org
- )
- group = self._create_device_group(name="Empty Group", organization=org)
- url = reverse(f"admin:{self.app_label}_build_changelist")
- data = {
- ACTION_CHECKBOX_NAME: [build.pk],
- "action": "upgrade_selected",
- "location": location.pk,
- "group": group.pk,
- "upgrade_all": "on",
- }
- with self.subTest("Test error message when no devices match filters"):
- response = self.client.post(url, data, follow=True)
- self.assertEqual(response.status_code, 200)
- # Should stay on confirmation page with error message
- self.assertContains(response, "No devices found matching")
- self.assertContains(response, "adjust your group and/or location filters")
- # No batch should be created
- self.assertEqual(BatchUpgradeOperation.objects.count(), 0)
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ org = self._get_org()
+ category = self._create_category(organization=org)
+ build = self._create_build(category=category, version="error-test")
+ # Create location and group but no devices matching both
+ location = Location.objects.create(
+ name="Empty Location", address="456 Empty St", organization=org
+ )
+ group = self._create_device_group(name="Empty Group", organization=org)
+ url = reverse(f"admin:{self.app_label}_build_changelist")
+ data = {
+ ACTION_CHECKBOX_NAME: [build.pk],
+ "action": "upgrade_selected",
+ "location": location.pk,
+ "group": group.pk,
+ "upgrade_all": "on",
+ }
+ with self.subTest("Test error message when no devices match filters"):
+ response = self.client.post(url, data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ # Should stay on confirmation page with error message
+ self.assertContains(response, "No devices found matching")
+ self.assertContains(response, "adjust your group and/or location filters")
+ # No batch should be created
+ self.assertEqual(BatchUpgradeOperation.objects.count(), 0)
+ @mock.patch(_mock_upgrade, return_value=True)
def test_batch_upgrade_operation_list_location_filter(self, *args):
"""Test location filter in BatchUpgradeOperation list view."""
- self._login()
- org = self._get_org()
- category = self._create_category(
- name="Location Filter Test Category", organization=org
- )
- build = self._create_build(category=category, version="location-test-1.0")
- location1 = Location.objects.create(
- name="Location 1", address="123 Main St", organization=org
- )
- location2 = Location.objects.create(
- name="Location 2", address="456 Oak Ave", organization=org
- )
- # Create batch operations with different locations
- batch1 = BatchUpgradeOperation.objects.create(build=build, location=location1)
- batch2 = BatchUpgradeOperation.objects.create(build=build, location=location2)
- batch3 = BatchUpgradeOperation.objects.create(
- build=build, location=None # No location
- )
- url = reverse(f"admin:{self.app_label}_batchupgradeoperation_changelist")
- with self.subTest("Test no location filter - shows all batches"):
- with self.assertNumQueries(5):
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, str(batch1.pk))
- self.assertContains(response, str(batch2.pk))
- self.assertContains(response, str(batch3.pk))
+ with mock.patch(self._mock_connect, return_value=True):
+ self._login()
+ org = self._get_org()
+ category = self._create_category(
+ name="Location Filter Test Category", organization=org
+ )
+ build = self._create_build(category=category, version="location-test-1.0")
+ location1 = Location.objects.create(
+ name="Location 1", address="123 Main St", organization=org
+ )
+ location2 = Location.objects.create(
+ name="Location 2", address="456 Oak Ave", organization=org
+ )
+ # Create batch operations with different locations
+ batch1 = BatchUpgradeOperation.objects.create(build=build, location=location1)
+ batch2 = BatchUpgradeOperation.objects.create(build=build, location=location2)
+ batch3 = BatchUpgradeOperation.objects.create(
+ build=build, location=None # No location
+ )
+ url = reverse(f"admin:{self.app_label}_batchupgradeoperation_changelist")
+ with self.subTest("Test no location filter - shows all batches"):
+ with self.assertNumQueries(5):
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, str(batch1.pk))
+ self.assertContains(response, str(batch2.pk))
+ self.assertContains(response, str(batch3.pk))
- with self.subTest("Test location1 filter"):
- with self.assertNumQueries(4):
- response = self.client.get(url + f"?location={location1.pk}")
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, str(batch1.pk))
- self.assertNotContains(response, str(batch2.pk))
- self.assertNotContains(response, str(batch3.pk))
+ with self.subTest("Test location1 filter"):
+ with self.assertNumQueries(4):
+ response = self.client.get(url + f"?location={location1.pk}")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, str(batch1.pk))
+ self.assertNotContains(response, str(batch2.pk))
+ self.assertNotContains(response, str(batch3.pk))
del TestConfigAdmin
diff --git a/openwisp_firmware_upgrader/tests/test_models.py b/openwisp_firmware_upgrader/tests/test_models.py
index f7cd85d07..e51cb03c2 100644
--- a/openwisp_firmware_upgrader/tests/test_models.py
+++ b/openwisp_firmware_upgrader/tests/test_models.py
@@ -560,69 +560,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):
@@ -634,28 +634,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 6bb5aa800..8fdbf0d2a 100644
--- a/openwisp_firmware_upgrader/tests/test_selenium.py
+++ b/openwisp_firmware_upgrader/tests/test_selenium.py
@@ -43,6 +43,7 @@ class TestDeviceAdmin(TestUpgraderMixin, SeleniumTestMixin, StaticLiveServerTest
firmware_app_label = Build._meta.app_label
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()
@@ -114,9 +115,10 @@ def test_restoring_deleted_device(self, *args):
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")
@@ -128,170 +130,18 @@ def test_restoring_deleted_device(self, *args):
"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()
-
- @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,
- )
-
- @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(
reverse(
@@ -300,24 +150,68 @@ def test_upgrader_with_unsupported_upgrade_options(self, *args):
)
)
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
+ # 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="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.wait_for_visibility(By.CSS_SELECTOR, "#devicefirmware-group")
- 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"
)
- DeviceFirmware.objects.all().delete()
- UpgradeOperation.objects.all().delete()
+ 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()
- with self.subTest("Test BatchUpgradeOperation"):
+ @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]
@@ -328,23 +222,133 @@ 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()
- self.wait_for_presence(By.ID, "batchupgradeoperation_form")
- self.wait_for_presence(By.CSS_SELECTOR, ".messagelist .success")
+ 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(
- BatchUpgradeOperation.objects.filter(upgrade_options={}).count(), 1
+ UpgradeOperation.objects.filter(
+ upgrade_options={
+ "c": False,
+ "o": False,
+ "n": True,
+ "u": False,
+ "p": False,
+ "k": False,
+ "F": False,
+ }
+ ).count(),
+ 1,
+ )
+
+ @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.wait_for_visibility(By.CSS_SELECTOR, "#devicefirmware-group")
+ 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.wait_for_presence(By.ID, "batchupgradeoperation_form")
+ self.wait_for_presence(By.CSS_SELECTOR, ".messagelist .success")
+ self.assertEqual(
+ UpgradeOperation.objects.filter(upgrade_options={}).count(), 1
+ )
+ self.assertEqual(
+ BatchUpgradeOperation.objects.filter(upgrade_options={}).count(), 1
+ )
def test_upgrade_cancel_modal(self):
"""Test upgrade cancel modal functionality"""
diff --git a/openwisp_firmware_upgrader/tests/test_tasks.py b/openwisp_firmware_upgrader/tests/test_tasks.py
index 3908573cd..4d98d7cd2 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")
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")
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_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..d84865ad3
--- /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 = [
+ ("config", "0001_squashed_0002_config_settings_uuid"),
+ 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="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..946e207c5
--- /dev/null
+++ b/tests/openwisp2/sample_connection/tests.py
@@ -0,0 +1,92 @@
+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,
+)
+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 = "config"
+ app_label = "sample_connection"
+
+
+class TestCommandInlines(BaseTestCommandInlines):
+ config_app_label = "config"
+
+
+class TestModels(BaseTestModels):
+ app_label = "sample_connection"
+
+
+class TestModelsTransaction(BaseTestModelsTransaction):
+ app_label = "sample_connection"
+
+
+class TestTasks(BaseTestTasks):
+ pass
+
+
+class TestSsh(BaseTestSsh):
+ pass
+
+
+Notification = load_model("openwisp_notifications", "Notification")
+
+
+class TestNotifications(BaseTestNotifications):
+ app_label = "sample_connection"
+ config_app_label = "config"
+
+
+class TestNotificationTransaction(BaseTestNotificationTransaction):
+ app_label = "sample_connection"
+ config_app_label = "config"
+
+
+class TestConnectionApi(BaseTestConnectionApi):
+ 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
+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..07e90d14a 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 = "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 = "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 = "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 96b318a90..00dce81e5 100644
--- a/tests/openwisp2/settings.py
+++ b/tests/openwisp2/settings.py
@@ -132,7 +132,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",
@@ -240,6 +239,19 @@
"sample_firmware_upgrader.UpgradeOperation"
)
+ # For controller extended apps:
+ # 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.connection")
+ # Swapper
+ 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..534d7fbe0 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,11 +7,36 @@
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView
+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_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(
+ "",
+ include(("openwisp_controller.config.urls", "config"), namespace="config"),
+ ),
+ 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")),