Skip to content

Commit 73c3b0a

Browse files
committed
[tests] Added tests
Signed-off-by: DragnEmperor <[email protected]>
1 parent 5bf8539 commit 73c3b0a

File tree

5 files changed

+222
-34
lines changed

5 files changed

+222
-34
lines changed

openwisp_controller/config/whois/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def trigger_whois_lookup(self):
125125
)
126126
elif whois_info_exists and self.is_whois_enabled:
127127
transaction.on_commit(
128-
manage_fuzzy_locations.delay(
128+
lambda: manage_fuzzy_locations.delay(
129129
self.device.pk, self.device.last_ip, add_existing=True
130130
)
131131
)

openwisp_controller/config/whois/tasks.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ def fetch_whois_details(self, device_pk, initial_ip_address, new_ip_address):
136136
whois_obj.full_clean()
137137
whois_obj.save()
138138
logger.info(f"Successfully fetched WHOIS details for {new_ip_address}.")
139+
location_address = whois_obj.formatted_address
140+
manage_fuzzy_locations.delay(
141+
device_pk,
142+
new_ip_address,
143+
data.location.latitude,
144+
data.location.longitude,
145+
location_address,
146+
)
139147

140148
# the following check ensures that for a case when device last_ip
141149
# is not changed and there is no related WHOIS record, we do not
@@ -192,15 +200,22 @@ def manage_fuzzy_locations(
192200
# if attaching an existing location, the current device should not have
193201
# a location already set.
194202
# TODO: Do we do this if device location exists but is marked fuzzy?
195-
if add_existing and not device_location.location:
196-
existing_device_with_location = (
197-
Device.objects.select_related("device_location")
198-
.filter(last_ip=ip_address, device_location__location__isnull=False)
203+
current_location = device_location.location
204+
if add_existing and (not current_location or current_location.fuzzy):
205+
existing_device_location = (
206+
Device.objects.select_related("devicelocation")
207+
.filter(last_ip=ip_address, devicelocation__location__isnull=False)
208+
.exclude(pk=device_pk)
199209
.first()
200210
)
201-
if existing_device_with_location:
202-
location = existing_device_with_location.device_location.location
203-
device_location.location = location
211+
if existing_device_location:
212+
existing_location = existing_device_location.devicelocation.location
213+
if current_location and current_location.pk != existing_location.pk:
214+
# If current device already has a location, delete it
215+
# and set the existing location.
216+
current_location.delete()
217+
218+
device_location.location = existing_location
204219
device_location.full_clean()
205220
device_location.save()
206221
elif latitude and longitude:

openwisp_controller/config/whois/test_whois.py

Lines changed: 166 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import importlib
22
from unittest import mock
33

4+
from django.contrib.gis.geos import GEOSGeometry
45
from django.core.exceptions import ImproperlyConfigured, ValidationError
56
from django.db.models.signals import post_delete, post_save
67
from django.test import TestCase, TransactionTestCase, override_settings
@@ -14,6 +15,7 @@
1415
from .utils import CreateWHOISMixin
1516

1617
Device = load_model("config", "Device")
18+
Location = load_model("geo", "Location")
1719
WHOISInfo = load_model("config", "WHOISInfo")
1820
OrganizationConfigSettings = load_model("config", "OrganizationConfigSettings")
1921
Notification = load_model("openwisp_notifications", "Notification")
@@ -233,37 +235,45 @@ def setUp(self):
233235
super().setUp()
234236
self.admin = self._get_admin()
235237

236-
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
237-
@mock.patch("openwisp_controller.config.whois.tasks.fetch_whois_details.delay")
238-
def test_whois_task_called(self, mocked_task):
238+
@staticmethod
239+
def _mocked_client_response():
240+
mock_response = mock.MagicMock()
241+
mock_response.city.name = "Mountain View"
242+
mock_response.country.name = "United States"
243+
mock_response.continent.name = "North America"
244+
mock_response.postal.code = "94043"
245+
mock_response.traits.autonomous_system_organization = "Google LLC"
246+
mock_response.traits.autonomous_system_number = 15169
247+
mock_response.traits.network = "172.217.22.0/24"
248+
mock_response.location.time_zone = "America/Los_Angeles"
249+
mock_response.location.latitude = 2
250+
mock_response.location.longitude = 23
251+
return mock_response
252+
253+
def _task_called(self, mocked_task, task_name="WHOIS lookup"):
239254
org = self._get_org()
240255
connect_whois_handlers()
241256

242-
with self.subTest("task called when last_ip is public"):
257+
with self.subTest(f"{task_name} task called when last_ip is public"):
243258
device = self._create_device(last_ip="172.217.22.14")
244259
mocked_task.assert_called()
245260
mocked_task.reset_mock()
246261

247-
with self.subTest("task called when last_ip is changed and is public"):
262+
with self.subTest(
263+
f"{task_name} task called when last_ip is changed and is public"
264+
):
248265
device.last_ip = "172.217.22.10"
249266
device.save()
250267
mocked_task.assert_called()
251268
mocked_task.reset_mock()
252269

253-
with self.subTest("task not called when last_ip is private"):
270+
with self.subTest(f"{task_name} task not called when last_ip is private"):
254271
device.last_ip = "10.0.0.1"
255272
device.save()
256273
mocked_task.assert_not_called()
257274
mocked_task.reset_mock()
258275

259-
with self.subTest("task not called when last_ip has related WHOISInfo"):
260-
device.last_ip = "172.217.22.10"
261-
self._create_whois_info(ip_address=device.last_ip)
262-
device.save()
263-
mocked_task.assert_not_called()
264-
mocked_task.reset_mock()
265-
266-
with self.subTest("task not called when WHOIS is disabled"):
276+
with self.subTest(f"{task_name} task not called when WHOIS is disabled"):
267277
Device.objects.all().delete()
268278
org.config_settings.whois_enabled = False
269279
# Invalidates old org config settings cache
@@ -272,7 +282,9 @@ def test_whois_task_called(self, mocked_task):
272282
mocked_task.assert_not_called()
273283
mocked_task.reset_mock()
274284

275-
with self.subTest("task called via DeviceChecksumView when WHOIS is enabled"):
285+
with self.subTest(
286+
f"{task_name} task called via DeviceChecksumView when WHOIS is enabled"
287+
):
276288
org.config_settings.whois_enabled = True
277289
# Invalidates old org config settings cache
278290
org.config_settings.save(update_fields=["whois_enabled"])
@@ -290,7 +302,7 @@ def test_whois_task_called(self, mocked_task):
290302
mocked_task.reset_mock()
291303

292304
with self.subTest(
293-
"task called via DeviceChecksumView when a device has no WHOIS record"
305+
f"{task_name} task called via DeviceChecksumView when a device has no WHOIS record"
294306
):
295307
WHOISInfo.objects.all().delete()
296308
response = self.client.get(
@@ -302,6 +314,40 @@ def test_whois_task_called(self, mocked_task):
302314
mocked_task.assert_called()
303315
mocked_task.reset_mock()
304316

317+
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
318+
@mock.patch("openwisp_controller.config.whois.tasks.fetch_whois_details.delay")
319+
def test_whois_task_called(self, mocked_lookup_task):
320+
self._task_called(mocked_lookup_task)
321+
322+
Device.objects.all().delete() # Clear existing devices
323+
device = self._create_device()
324+
with self.subTest("WHOIS lookup task not called when last_ip has related WhoIsInfo"):
325+
device.last_ip = "172.217.22.14"
326+
self._create_whois_info(ip_address=device.last_ip)
327+
device.save()
328+
mocked_lookup_task.assert_not_called()
329+
mocked_lookup_task.reset_mock()
330+
331+
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
332+
@mock.patch("openwisp_controller.config.whois.tasks.manage_fuzzy_locations.delay")
333+
@mock.patch(_WHOIS_GEOIP_CLIENT)
334+
def test_fuzzy_location_task_called(
335+
self, mocked_client, mocked_fuzzy_location_task
336+
):
337+
mocked_client.return_value.city.return_value = self._mocked_client_response()
338+
self._task_called(mocked_fuzzy_location_task, task_name="Fuzzy location")
339+
340+
Device.objects.all().delete()
341+
device = self._create_device()
342+
with self.subTest(
343+
"Fuzzy location task called when last_ip has related WhoIsInfo"
344+
):
345+
device.last_ip = "172.217.22.14"
346+
self._create_whois_info(ip_address=device.last_ip)
347+
device.save()
348+
mocked_fuzzy_location_task.assert_called()
349+
mocked_fuzzy_location_task.reset_mock()
350+
305351
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
306352
@mock.patch("openwisp_controller.config.whois.tasks.fetch_whois_details.delay")
307353
def test_whois_multiple_orgs(self, mocked_task):
@@ -406,16 +452,7 @@ def _verify_whois_details(instance, ip_address):
406452
)
407453

408454
# mocking the response from the geoip2 client
409-
mock_response = mock.MagicMock()
410-
mock_response.city.name = "Mountain View"
411-
mock_response.country.name = "United States"
412-
mock_response.continent.name = "North America"
413-
mock_response.postal.code = "94043"
414-
mock_response.traits.autonomous_system_organization = "Google LLC"
415-
mock_response.traits.autonomous_system_number = 15169
416-
mock_response.traits.network = "172.217.22.0/24"
417-
mock_response.location.time_zone = "America/Los_Angeles"
418-
mock_client.return_value.city.return_value = mock_response
455+
mock_client.return_value.city.return_value = self._mocked_client_response()
419456

420457
with self.subTest("Test WHOIS create when device is created"):
421458
device = self._create_device(last_ip="172.217.22.14")
@@ -455,6 +492,109 @@ def _verify_whois_details(instance, ip_address):
455492
# WHOIS related to the device's last_ip should be deleted
456493
self.assertEqual(WHOISInfo.objects.filter(ip_address=ip_address).count(), 0)
457494

495+
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
496+
@mock.patch(_WHOIS_GEOIP_CLIENT)
497+
def test_fuzzy_location_creation_and_update(self, mock_client):
498+
connect_whois_handlers()
499+
500+
def _verify_location_details(device, mocked_response):
501+
location = device.devicelocation.location
502+
mocked_location = mocked_response.location
503+
formatted_address = ", ".join(
504+
[
505+
mocked_response.city.name,
506+
mocked_response.country.name,
507+
mocked_response.continent.name,
508+
mocked_response.postal.code,
509+
]
510+
)
511+
self.assertEqual(location.address, formatted_address)
512+
self.assertEqual(
513+
location.geometry,
514+
GEOSGeometry(
515+
f"POINT({mocked_location.longitude} {mocked_location.latitude})",
516+
srid=4326,
517+
),
518+
)
519+
520+
mocked_response = self._mocked_client_response()
521+
mock_client.return_value.city.return_value = mocked_response
522+
523+
with self.subTest("Test Fuzzy location created when device is created"):
524+
device = self._create_device(last_ip="172.217.22.14")
525+
526+
location = device.devicelocation.location
527+
self.assertEqual(location.fuzzy, True)
528+
self.assertEqual(location.is_mobile, False)
529+
self.assertEqual(location.type, "outdoor")
530+
_verify_location_details(device, mocked_response)
531+
532+
with self.subTest("Test Fuzzy location updated when last ip is updated"):
533+
device.last_ip = "172.217.22.10"
534+
mocked_response.location.latitude = 100
535+
mocked_response.location.longitude = 200
536+
mocked_response.city.name = "New City"
537+
mock_client.return_value.city.return_value = mocked_response
538+
device.save()
539+
device.refresh_from_db()
540+
541+
location = device.devicelocation.location
542+
self.assertEqual(location.fuzzy, True)
543+
self.assertEqual(location.is_mobile, False)
544+
self.assertEqual(location.type, "outdoor")
545+
_verify_location_details(device, mocked_response)
546+
547+
with self.subTest(
548+
"Test Location not updated if it is not fuzzy when last ip is updated"
549+
):
550+
device.last_ip = "172.217.22.11"
551+
device.devicelocation.location.fuzzy = False
552+
mock_client.return_value.city.return_value = self._mocked_client_response()
553+
device.devicelocation.location.save()
554+
device.save()
555+
device.refresh_from_db()
556+
557+
location = device.devicelocation.location
558+
self.assertEqual(location.fuzzy, False)
559+
self.assertEqual(location.is_mobile, False)
560+
self.assertEqual(location.type, "outdoor")
561+
_verify_location_details(device, mocked_response)
562+
563+
with self.subTest(
564+
"Test Location shared among devices with same last_ip when new device's location does not exist"
565+
):
566+
Device.objects.all().delete()
567+
device1 = self._create_device(last_ip="172.217.22.10")
568+
device2 = self._create_device(
569+
name="11:22:33:44:55:66",
570+
mac_address="11:22:33:44:55:66",
571+
last_ip="172.217.22.10",
572+
)
573+
574+
self.assertEqual(
575+
device1.devicelocation.location.pk, device2.devicelocation.location.pk
576+
)
577+
578+
with self.subTest(
579+
"Test Location shared among devices with same last_ip when new device's location exist"
580+
):
581+
Device.objects.all().delete()
582+
device1 = self._create_device(last_ip="172.217.22.10")
583+
device2 = self._create_device(
584+
name="11:22:33:44:55:66",
585+
mac_address="11:22:33:44:55:66",
586+
last_ip="172.217.22.11",
587+
)
588+
old_location = device2.devicelocation.location
589+
device2.last_ip = "172.217.22.10"
590+
device2.save()
591+
device2.refresh_from_db()
592+
593+
self.assertEqual(
594+
device1.devicelocation.location.pk, device2.devicelocation.location.pk
595+
)
596+
self.assertEqual(Location.objects.filter(pk=old_location.pk).count(), 0)
597+
458598
# we need to allow the task to propagate exceptions to ensure
459599
# `on_failure` method is called and notifications are executed
460600
@override_settings(CELERY_TASK_EAGER_PROPAGATES=False)

openwisp_controller/geo/base/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.contrib.gis.db import models
2+
from django.utils.translation import gettext_lazy as _
23
from django_loci.base.models import (
34
AbstractFloorPlan,
45
AbstractLocation,
@@ -10,6 +11,14 @@
1011

1112

1213
class BaseLocation(OrgMixin, AbstractLocation):
14+
fuzzy = models.BooleanField(
15+
default=False,
16+
help_text=_(
17+
"If true, the location is considered fuzzy and "
18+
"may not have precise coordinates."
19+
),
20+
)
21+
1322
class Meta(AbstractLocation.Meta):
1423
abstract = True
1524

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.2.1 on 2025-06-25 19:22
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("geo", "0003_alter_devicelocation_floorplan_location"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="location",
15+
name="fuzzy",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text=(
19+
"If true, the location is considered fuzzy and "
20+
"may not have precise coordinates."
21+
),
22+
),
23+
),
24+
]

0 commit comments

Comments
 (0)