Skip to content

Commit 95efe7c

Browse files
committed
[tests] Added tests
Signed-off-by: DragnEmperor <[email protected]>
1 parent 1d4cc08 commit 95efe7c

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,41 +235,49 @@ 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
with mock.patch("django.core.cache.cache.set") as mocked_set:
244259
device = self._create_device(last_ip="172.217.22.14")
245260
mocked_task.assert_called()
246261
mocked_set.assert_called_once()
247262
mocked_task.reset_mock()
248263

249-
with self.subTest("task called when last_ip is changed and is public"):
264+
with self.subTest(
265+
f"{task_name} task called when last_ip is changed and is public"
266+
):
250267
with mock.patch("django.core.cache.cache.get") as mocked_get:
251268
device.last_ip = "172.217.22.10"
252269
device.save()
253270
mocked_task.assert_called()
254271
mocked_get.assert_called_once()
255272
mocked_task.reset_mock()
256273

257-
with self.subTest("task not called when last_ip is private"):
274+
with self.subTest(f"{task_name} task not called when last_ip is private"):
258275
device.last_ip = "10.0.0.1"
259276
device.save()
260277
mocked_task.assert_not_called()
261278
mocked_task.reset_mock()
262279

263-
with self.subTest("task not called when last_ip has related WHOISInfo"):
264-
device.last_ip = "172.217.22.10"
265-
self._create_whois_info(ip_address=device.last_ip)
266-
device.save()
267-
mocked_task.assert_not_called()
268-
mocked_task.reset_mock()
269-
270-
with self.subTest("task not called when WHOIS is disabled"):
280+
with self.subTest(f"{task_name} task not called when WHOIS is disabled"):
271281
Device.objects.all().delete()
272282
org.config_settings.whois_enabled = False
273283
# Invalidates old org config settings cache
@@ -276,7 +286,9 @@ def test_whois_task_called(self, mocked_task):
276286
mocked_task.assert_not_called()
277287
mocked_task.reset_mock()
278288

279-
with self.subTest("task called via DeviceChecksumView when WHOIS is enabled"):
289+
with self.subTest(
290+
f"{task_name} task called via DeviceChecksumView when WHOIS is enabled"
291+
):
280292
org.config_settings.whois_enabled = True
281293
# Invalidates old org config settings cache
282294
org.config_settings.save(update_fields=["whois_enabled"])
@@ -294,7 +306,7 @@ def test_whois_task_called(self, mocked_task):
294306
mocked_task.reset_mock()
295307

296308
with self.subTest(
297-
"task called via DeviceChecksumView when a device has no WHOIS record"
309+
f"{task_name} task called via DeviceChecksumView when a device has no WHOIS record"
298310
):
299311
WHOISInfo.objects.all().delete()
300312
response = self.client.get(
@@ -306,6 +318,40 @@ def test_whois_task_called(self, mocked_task):
306318
mocked_task.assert_called()
307319
mocked_task.reset_mock()
308320

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

412458
# mocking the response from the geoip2 client
413-
mock_response = mock.MagicMock()
414-
mock_response.city.name = "Mountain View"
415-
mock_response.country.name = "United States"
416-
mock_response.continent.name = "North America"
417-
mock_response.postal.code = "94043"
418-
mock_response.traits.autonomous_system_organization = "Google LLC"
419-
mock_response.traits.autonomous_system_number = 15169
420-
mock_response.traits.network = "172.217.22.0/24"
421-
mock_response.location.time_zone = "America/Los_Angeles"
422-
mock_client.return_value.city.return_value = mock_response
459+
mock_client.return_value.city.return_value = self._mocked_client_response()
423460

424461
with self.subTest("Test WHOIS create when device is created"):
425462
device = self._create_device(last_ip="172.217.22.14")
@@ -459,6 +496,109 @@ def _verify_whois_details(instance, ip_address):
459496
# WHOIS related to the device's last_ip should be deleted
460497
self.assertEqual(WHOISInfo.objects.filter(ip_address=ip_address).count(), 0)
461498

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