Skip to content

Commit 6643b32

Browse files
committed
[tests] Added tests
Signed-off-by: DragnEmperor <[email protected]>
1 parent 9882ec4 commit 6643b32

File tree

4 files changed

+223
-32
lines changed

4 files changed

+223
-32
lines changed

openwisp_controller/config/whois/service.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ def fetch_whois_details(self, device_pk, initial_ip_address, new_ip_address):
272272
whois_obj.save()
273273
logger.info(f"Successfully fetched WHOIS details for {new_ip_address}.")
274274
location_address = whois_obj.formatted_address
275+
275276
WhoIsService.manage_fuzzy_locations.delay(
276277
device_pk,
277278
new_ip_address,
@@ -314,8 +315,15 @@ def manage_fuzzy_locations(
314315
"""
315316
Creates/updates fuzzy location for a device based on the latitude and longitude
316317
or attaches an existing location if `add_existing` is True.
317-
Existing location here means a location of a device whose last_ip matches
318+
Existing location here means a location of another device whose last_ip matches
318319
the given ip_address.
320+
321+
- If attaching existing location and current device has no location then the existing
322+
location is attached to the current device as well.
323+
- If attaching existing location and current device already has a location,
324+
then current device's location is deleted and set same as existing device's location.
325+
- If not attaching existing then new location is created if no location exists for
326+
current device, or existing one is updated if it is fuzzy.
319327
"""
320328
Device = load_model("config", "Device")
321329
Location = load_model("geo", "Location")
@@ -332,16 +340,22 @@ def manage_fuzzy_locations(
332340

333341
# if attaching an existing location, the current device should not have
334342
# a location already set.
335-
#TODO: Do we do this if device location exists but is marked fuzzy?
336-
if add_existing and not device_location.location:
337-
existing_device_with_location = (
338-
Device.objects.select_related("device_location")
339-
.filter(last_ip=ip_address, device_location__location__isnull=False)
343+
current_location = device_location.location
344+
if add_existing and (not current_location or current_location.fuzzy):
345+
existing_device_location = (
346+
Device.objects.select_related("devicelocation")
347+
.filter(last_ip=ip_address, devicelocation__location__isnull=False)
348+
.exclude(pk=device_pk)
340349
.first()
341350
)
342-
if existing_device_with_location:
343-
location = existing_device_with_location.device_location.location
344-
device_location.location = location
351+
if existing_device_location:
352+
existing_location = existing_device_location.devicelocation.location
353+
if current_location and current_location.pk != existing_location.pk:
354+
# If current device already has a location, delete it
355+
# and set the existing location.
356+
current_location.delete()
357+
358+
device_location.location = existing_location
345359
device_location.full_clean()
346360
device_location.save()
347361
elif latitude and longitude:

openwisp_controller/config/whois/test_whois.py

Lines changed: 167 additions & 23 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")
@@ -192,11 +194,22 @@ class TestWhoIsTransaction(CreateWhoIsMixin, TransactionTestCase):
192194
def setUp(self):
193195
self.admin = self._get_admin()
194196

195-
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
196-
@mock.patch(
197-
"openwisp_controller.config.whois.service.WhoIsService.fetch_whois_details.delay" # noqa: E501
198-
)
199-
def test_task_called(self, mocked_task):
197+
@staticmethod
198+
def _mocked_client_response():
199+
mock_response = mock.MagicMock()
200+
mock_response.city.name = "Mountain View"
201+
mock_response.country.name = "United States"
202+
mock_response.continent.name = "North America"
203+
mock_response.postal.code = "94043"
204+
mock_response.traits.autonomous_system_organization = "Google LLC"
205+
mock_response.traits.autonomous_system_number = 15169
206+
mock_response.traits.network = "172.217.22.0/24"
207+
mock_response.location.time_zone = "America/Los_Angeles"
208+
mock_response.location.latitude = 2
209+
mock_response.location.longitude = 23
210+
return mock_response
211+
212+
def _task_called(self, mocked_task):
200213
org = self._get_org()
201214
OrganizationConfigSettings.objects.create(organization=org, whois_enabled=True)
202215
connect_whois_handlers()
@@ -218,13 +231,6 @@ def test_task_called(self, mocked_task):
218231
mocked_task.assert_not_called()
219232
mocked_task.reset_mock()
220233

221-
with self.subTest("task not called when last_ip has related WhoIsInfo"):
222-
device.last_ip = "172.217.22.10"
223-
self._create_whois_info(ip_address=device.last_ip)
224-
device.save()
225-
mocked_task.assert_not_called()
226-
mocked_task.reset_mock()
227-
228234
with self.subTest("task not called when WHOIS is disabled"):
229235
Device.objects.all().delete() # Clear existing devices
230236
org.config_settings.whois_enabled = False
@@ -250,11 +256,52 @@ def test_task_called(self, mocked_task):
250256
mocked_task.assert_called()
251257
mocked_task.reset_mock()
252258

259+
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
260+
@mock.patch(
261+
"openwisp_controller.config.whois.service.WhoIsService.fetch_whois_details.delay" # noqa: E501
262+
)
263+
def test_lookup_task_called(self, mocked_lookup_task):
264+
self._task_called(mocked_lookup_task)
265+
266+
Device.objects.all().delete() # Clear existing devices
267+
device = self._create_device()
268+
with self.subTest("Lookup task not called when last_ip has related WhoIsInfo"):
269+
device.last_ip = "172.217.22.14"
270+
self._create_whois_info(ip_address=device.last_ip)
271+
device.save()
272+
mocked_lookup_task.assert_not_called()
273+
mocked_lookup_task.reset_mock()
274+
275+
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
276+
@mock.patch(
277+
"openwisp_controller.config.whois.service.WhoIsService.manage_fuzzy_locations.delay" # noqa: E501
278+
)
279+
@mock.patch(_WHOIS_GEOIP_CLIENT)
280+
def test_fuzzy_location_task_called(
281+
self, mocked_client, mocked_fuzzy_location_task
282+
):
283+
"""
284+
Test that the fuzzy location task is called alongside the lookup task
285+
"""
286+
mocked_client.return_value.city.return_value = self._mocked_client_response()
287+
self._task_called(mocked_fuzzy_location_task)
288+
289+
Device.objects.all().delete() # Clear existing devices
290+
device = self._create_device()
291+
with self.subTest(
292+
"Fuzzy location task called when last_ip has related WhoIsInfo"
293+
):
294+
device.last_ip = "172.217.22.14"
295+
self._create_whois_info(ip_address=device.last_ip)
296+
device.save()
297+
mocked_fuzzy_location_task.assert_called()
298+
mocked_fuzzy_location_task.reset_mock()
299+
253300
# mocking the geoip2 client to return a mock response
254301
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
255302
@mock.patch(_WHOIS_TASKS_INFO_LOGGER)
256303
@mock.patch(_WHOIS_GEOIP_CLIENT)
257-
def test_whois_info_tasks(self, mock_client, mock_info):
304+
def test_whois_info_creation(self, mock_client, mock_info):
258305

259306
# helper function for asserting the model details with
260307
# mocked api response
@@ -282,16 +329,7 @@ def _verify_whois_details(instance, ip_address):
282329
OrganizationConfigSettings.objects.create(organization=org, whois_enabled=True)
283330

284331
# mocking the response from the geoip2 client
285-
mock_response = mock.MagicMock()
286-
mock_response.city.name = "Mountain View"
287-
mock_response.country.name = "United States"
288-
mock_response.continent.name = "North America"
289-
mock_response.postal.code = "94043"
290-
mock_response.traits.autonomous_system_organization = "Google LLC"
291-
mock_response.traits.autonomous_system_number = 15169
292-
mock_response.traits.network = "172.217.22.0/24"
293-
mock_response.location.time_zone = "America/Los_Angeles"
294-
mock_client.return_value.city.return_value = mock_response
332+
mock_client.return_value.city.return_value = self._mocked_client_response()
295333

296334
# creating a device with a last public IP
297335
with self.subTest("Test WHOIS create when device is created"):
@@ -332,6 +370,112 @@ def _verify_whois_details(instance, ip_address):
332370
# WHOIS related to the device's last_ip should be deleted
333371
self.assertEqual(WhoIsInfo.objects.filter(ip_address=ip_address).count(), 0)
334372

373+
@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
374+
@mock.patch(_WHOIS_GEOIP_CLIENT)
375+
def test_fuzzy_location_creation_and_update(self, mock_client):
376+
connect_whois_handlers()
377+
378+
def _verify_location_details(device, mocked_response):
379+
location = device.devicelocation.location
380+
mocked_location = mocked_response.location
381+
formatted_address = ", ".join(
382+
[
383+
mocked_response.city.name,
384+
mocked_response.country.name,
385+
mocked_response.continent.name,
386+
mocked_response.postal.code,
387+
]
388+
)
389+
self.assertEqual(location.address, formatted_address)
390+
self.assertEqual(
391+
location.geometry,
392+
GEOSGeometry(
393+
f"POINT({mocked_location.longitude} {mocked_location.latitude})",
394+
srid=4326,
395+
),
396+
)
397+
398+
org = self._get_org()
399+
OrganizationConfigSettings.objects.create(organization=org, whois_enabled=True)
400+
401+
mocked_response = self._mocked_client_response()
402+
mock_client.return_value.city.return_value = mocked_response
403+
404+
with self.subTest("Test Fuzzy location created when device is created"):
405+
device = self._create_device(last_ip="172.217.22.14")
406+
407+
location = device.devicelocation.location
408+
self.assertEqual(location.fuzzy, True)
409+
self.assertEqual(location.is_mobile, False)
410+
self.assertEqual(location.type, "outdoor")
411+
_verify_location_details(device, mocked_response)
412+
413+
with self.subTest("Test Fuzzy location updated when last ip is updated"):
414+
device.last_ip = "172.217.22.10"
415+
mocked_response.location.latitude = 100
416+
mocked_response.location.longitude = 200
417+
mocked_response.city.name = "New City"
418+
mock_client.return_value.city.return_value = mocked_response
419+
device.save()
420+
device.refresh_from_db()
421+
422+
location = device.devicelocation.location
423+
self.assertEqual(location.fuzzy, True)
424+
self.assertEqual(location.is_mobile, False)
425+
self.assertEqual(location.type, "outdoor")
426+
_verify_location_details(device, mocked_response)
427+
428+
with self.subTest(
429+
"Test Location not updated if it is not fuzzy when last ip is updated"
430+
):
431+
device.last_ip = "172.217.22.11"
432+
device.devicelocation.location.fuzzy = False
433+
mock_client.return_value.city.return_value = self._mocked_client_response()
434+
device.devicelocation.location.save()
435+
device.save()
436+
device.refresh_from_db()
437+
438+
location = device.devicelocation.location
439+
self.assertEqual(location.fuzzy, False)
440+
self.assertEqual(location.is_mobile, False)
441+
self.assertEqual(location.type, "outdoor")
442+
_verify_location_details(device, mocked_response)
443+
444+
with self.subTest(
445+
"Test Location shared among devices with same last_ip when new device's location does not exist"
446+
):
447+
Device.objects.all().delete()
448+
device1 = self._create_device(last_ip="172.217.22.10")
449+
device2 = self._create_device(
450+
name="11:22:33:44:55:66",
451+
mac_address="11:22:33:44:55:66",
452+
last_ip="172.217.22.10",
453+
)
454+
455+
self.assertEqual(
456+
device1.devicelocation.location.pk, device2.devicelocation.location.pk
457+
)
458+
459+
with self.subTest(
460+
"Test Location shared among devices with same last_ip when new device's location exist"
461+
):
462+
Device.objects.all().delete()
463+
device1 = self._create_device(last_ip="172.217.22.10")
464+
device2 = self._create_device(
465+
name="11:22:33:44:55:66",
466+
mac_address="11:22:33:44:55:66",
467+
last_ip="172.217.22.11",
468+
)
469+
old_location = device2.devicelocation.location
470+
device2.last_ip = "172.217.22.10"
471+
device2.save()
472+
device2.refresh_from_db()
473+
474+
self.assertEqual(
475+
device1.devicelocation.location.pk, device2.devicelocation.location.pk
476+
)
477+
self.assertEqual(Location.objects.filter(pk=old_location.pk).count(), 0)
478+
335479
# we need to allow the task to propagate exceptions to ensure
336480
# `on_failure` method is called and notifications are executed
337481
@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)